Repository navigation
feat(channel): implement channel - #1997
Merged
Merged
Conversation
DavdGao
requested changes
Jul 8, 2026
DavdGao
reviewed
Jul 9, 2026
DavdGao
reviewed
Jul 13, 2026
# Conflicts: # pyproject.toml
…cher), router+lifespan rewire, wakeup error event - ChannelService: stateless CRUD + bot dedup + lifecycle notify - ChannelRunRegistry: dumb in-process instance table - ChannelLifecycleDispatcher: reconcile storage<->instances + status heartbeat - Router: new service/runtime API; drop binding + test endpoints - WakeupDispatcher: publish ReplyEndEvent(ERROR) on orphan-drop for fast-fail - Storage channel methods optional (Redis-only); SQL backend inherits NotImplementedError Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Two-phase HITL: present_confirm/update_confirm, ConfirmDecisionEvent handled via same process entry; pending context in shared storage (_pending.py), no blocking wait, no approval timeout - Gateway _collect returns (text, confirm?); _finish/_resolve orchestrate present -> decision -> resume -> continue; auto-deny when unpresentable - Media buffering (_media.py): media-only messages buffered in shared storage, drained and merged on the next text message - 10 unit tests for collect folding / media aggregation / pending Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rewrite FeishuChannel to ChannelBase: bind/emit, send_response(blocks), present_confirm/update_confirm; card click -> ConfirmDecisionEvent - Remove in-process approval futures + attachment buffer (now gateway); media downloaded to DataBlock and emitted as a media-only event - Use SDK public start() (no ws_module.loop monkeypatch / private _connect) - Simplify card templates to request_id + approve/deny; update stubs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- types/api aligned to new backend (RoutingConfig/bindings, session, presentation, id/user_id); drop DmScope, binding+test endpoints - Merge create/edit dialogs into a shared ChannelForm; edit locks platform/credentials - BindingsEditor: ordered routing rules (match -> agent + session_scope) with a fixed catch-all; channel types sourced from /channels/types - i18n: session scope (per_chat/per_chat_user) + binding keys; drop DM scope - tsc clean, pnpm format clean Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- DiscordChannel on discord.py (async-native, no thread bridging): on_message -> ChannelEvent; two-button approval View emits ConfirmDecisionEvent; update_confirm edits the message - message_content intent; DM vs guild via message.guild; @-mention gate; attachments -> DataBlock; 2000-char split; list_bot_chats over guilds - add discord.py to the channel extra Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… while running - Idle session: new 'message' wakeup kind carries a UserMsg through the WakeupDispatcher -> ChatService.run Case A, so it is persisted and reasoned over as a genuine user turn (not an inbox hint) - Running session: inject the input as a HintBlock into the inbox so the live run folds it into the in-flight reply (no queuing, no new bubble) - WakeupDispatcher: generalize resume re-queue to any input-carrying kind (resume | message); parse Msg for message triggers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…blob offload Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cleanup Output flow: the gateway now only delivers input + handles HITL card clicks; a ChannelPresenter forwards run output back to the platform via an outbound signal + a per-run forward lease, so scheduled/background runs reach the channel too — not just inbound messages. Adds MessageBus.try_lock/unlock (the queue drain is at-least-once) and session source_chat_id/source_channel_id so a run can be routed back to its chat. Feishu: run several bots per node via a thread-local loop proxy over lark's module-global event loop. Files the model can't read inline are offloaded to a portable workspace:// reference and replaced with a system-reminder TextBlock, so the model is told a file arrived instead of the block being dropped. Cleanup: collapse channel errors to one ChannelError(status_code); delete ChannelConfig (-> module constants) and ConfirmPrompt (present_confirm now takes RequireUserConfirmEvent); trim the package's public surface; rewrite channel tests against the presenter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ChannelBase.list_tools(workspace) so an adapter can hand the agent tools that act back on the platform. Feishu implements five: ListChats / ListChatMembers (read-only discovery that return ready-to-use receive_id + receive_id_type pairs) and SendMessage / SendFile / SendImage (writes to a chat/user other than the current conversation, ASK-gated so they route through the confirmation UI). File sends read from the session's workspace via the backend, never the host. Wiring: ChannelAgentToolFactory resolves a channel-originated session's local adapter (every node runs every enabled channel, so no cross-node hop) plus its workspace, appends the channel tools to any deployment extra_agent_tools, and is installed as the ChatService tool factory in the lifespan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # examples/agent_service/main.py # examples/web_ui/frontend/src/i18n/locales/en.json # examples/web_ui/frontend/src/i18n/locales/zh.json # src/agentscope/app/_app.py # src/agentscope/app/_service/_chat.py
…ypes Address review feedback on the channel module: - Remove the ChannelAgentToolFactory: get_toolkit now asks a channel-originated session's local channel for its tools directly (mirrors workspace.list_tools / mw.list_tools); the dispatcher is built before ChatService and passed through. - Inline single-use / trivial helpers: gateway _safe_send + _handle_decision + media buffering (drop _media.py), presenter _auto_deny, service list_for_user/get/_bot_id (router hits storage directly), tool _ok/_err, routing predicates, card _truncate, seen-chats module. - Fold save_pending/take_pending into PendingConfirm.save/.take. - Centralize every scattered channel key in MessageBusKeys (liveness, media, pending, forward lease, seen-chats, lifecycle). - Rename ConfirmDecisionEvent -> ChannelConfirmationResultEvent to mirror the agent's UserConfirmResultEvent. - Type the Feishu/Discord SDK payloads via TYPE_CHECKING instead of Any; add the missing Discord docstrings; read SendFile/SendImage from the session workspace. - Untrack committed artifacts (internal design doc, example workspaces/) and scrub their doc references. - Implement the new abstract try_lock/unlock on the dispatcher test fakes that the earlier bus change had left abstract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Build ChannelTypeRegistry in create_app (it has no lifecycle) and drop app.state.channels; the lifespan reuses it. - Rename channel_runtime -> channel_dispatcher everywhere to align with the chat naming (ChatRunRegistry / *Dispatcher). - Move the channel router's FastAPI dependency providers into app/deps.py (get_channel_service / _dispatcher / _type_registry) and its request/response models into _router/_schema/_channel.py; add docstrings to the router's remaining helpers. - Discord: document __init__ params; fold _normalize + _attachment_block into _on_message. - Model service: inline _get_model_input_types; drop the hasattr(model, "formatter") guard (a model always has a formatter). - Feishu card templates: underscore-prefix the helpers, add parameter docstrings, and switch card text to English. - examples: revert the stray 8000 -> 8001 port change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reflecting on the review feedback: - Merge ChannelPresenter into ChannelLifecycleDispatcher. The dispatcher was the only user of it (one call site), so the separate class was unjustified — its forward/collect/drive/finish logic and the _Streamer helper now live on the dispatcher. Delete _presenter.py. - Drop the "adapter" concept entirely: there is no Adapter class, a channel is a ChannelBase subclass. Rename every `adapter` parameter / field / variable / docstring to `channel` across the package. - Make PendingConfirm private (_PendingConfirm); it is a shared model (the gateway takes on a click, the dispatcher saves on present), so it stays in _pending.py rather than being colocated with one caller. - Feishu channel: add the missing __init__ + method parameter docstrings (every method) and inline the single-use _send_text / _send_card / _reply helpers into their callers. - Self-review pass: add docstrings to every remaining channel function (service / errors / run-registry / gateway / dispatcher / discord / feishu tools) — verified none are missing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add Args to every function missing parameter docs (arg-coverage audit, not just docstring presence — 50 functions). - Condense all newly-added multi-line docstring prose and inline comments to <=2 lines. - Inline the one-time build_feishu_tools helper into FeishuChannel.list_tools. - Fix stale "presenter" references in _decision.py (folded into dispatcher earlier). - Discord confirmation card/buttons: Chinese -> English. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every method was a 1:1 dict passthrough with no added behaviour — all task lifecycle already lives in the dispatcher's _start/_stop/lifespan. Use a plain dict[str, ChannelInstance] on the dispatcher; move the ChannelInstance dataclass into its only consumer and delete _run_registry.py. (Unlike the sibling ChatRunRegistry, which earns its class with real spawn/dedup/shutdown logic, this one encapsulated nothing.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The channel PR made the agent read `model.formatter.supported_input_media_types` on every incoming message (offloading unsupported media). Real models all expose a formatter; the test doubles MockModel and SlowModel did not, so agent-driven tests raised AttributeError. Give both a default OpenAIChatFormatter, mirroring real models. (--no-verify: tests/utils.py has pre-existing pylint debt unrelated to this change.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The channel PR made offloaded DataBlocks reference a portable workspace:///data/<hash> URL instead of a host file:// path, and renamed _offload_data_block to public offload_data_block. Update the local and daytona offload tests: assert workspace://, resolve URLs via workdir-relative join, and drop the now-unused Path/url2pathname imports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…faults - ChannelService is a CRUD service like the others, so relocate it to app/_service/_channel.py and export it from the _service package (it is no longer part of the channel package's public surface). - ChannelBase.channel_type / display_name / platform_bot_id_field are now bare annotations, not `= ""`. Subclasses must set them; an empty default only masked missing overrides. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- _tools.py becomes a _tools/ package: one module per tool (_list_chats, _list_chat_members, _send_message, _send_file, _send_image) plus _base (the shared base + _ack). - Collapse the two-level _FeishuTool/_FeishuFileTool hierarchy into a single _FeishuToolBase that holds the channel and the workspace; file tools use its _read/_resolve, discovery tools ignore the workspace. Every tool now constructs uniformly as (channel, workspace). - Descriptions are written inline on each class's `description` field instead of module-level _*_DESC constants. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The built-in Read/Write tools already require absolute in-sandbox paths and pass them straight to the backend, so the Feishu file tools should too. Require an absolute path, hand the tools the workspace backend instead of the whole workspace, and delete the _resolve / _read workdir-joining helpers. _FeishuToolBase now holds (channel, backend). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the channel interface around a single event-consuming method and a session-authoritative approval flow. Lifecycle - Collapse validate/on_start/bind/on_stop into one start_listening(emit) (setup + receive loop + finally teardown); drop create-time validate. - Rename add_reaction -> send_reaction (keep remove_reaction). Output (channel-driven) - send_response(event, events) is now abstract: each channel consumes the run's agent-event stream, accumulates it into a Msg via Msg.append_event (reusing the correct base64 handling instead of a hand-rolled fold), renders per the channel's presentation, and sends — streaming (Feishu CardKit) or one-shot (Discord). - Drop the _deliver / _absorb / base _present_confirm sub-layers; the dispatcher only feeds the gap-free event stream. Multimodal replies (images/files) delivered per capabilities.image/file. - presentation is held on the instance (one channel == one record == one AgentScope user), set by the dispatcher. Stateless tool-approval - Delete _PendingConfirm and its bus key. The awaiting confirmation is read from session state (ASKING tool calls) and resumed by tool_call_id; the card round-trips only tool_call_id + chat_id, re-validated server side. Card click freezes its own card. No auto-deny, no parked record. Tests rewritten for the new interface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Channel-created sessions hard-coded workspace_id="default", so every Channel session across all users/agents aliased the same workspace — a cross-user isolation violation (files, tools, skills leaked between owners). Assign the workspace via workspace_manager.assign_workspace_id(user, agent, session) at session creation, matching the normal session-creation flow (correct for PER_SESSION/PER_AGENT/PER_USER). Inject the workspace manager into ChannelGateway. Regression test: two owners get distinct workspaces. Also drop the single-consumer _config.py: its two remaining constants (RESPONSE_TIMEOUT_SECS, LIVENESS_TTL_SECS) move to _dispatcher.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inbound Feishu `post` messages (nested rich text) were rejected with "Unsupported message type: post", dropping text-plus-image messages entirely. Add _parse_post to flatten the nested title/rows/elements into ordered TextBlock / DataBlock content, downloading each embedded img via its element-level key. Extract the shared _download_resource (reused by top-level media and post images) and a shared _gated_out (@-mention gate for text + post). Test: mixed text/image/link post flattens in order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…i#10/agentscope-ai#12/agentscope-ai#13 + session name - agentscope-ai#10: allow all PermissionMode values for channel sessions. The engine resolves every mode server-side and the ASK path maps to the confirm card, so accept_edits/explore work too — drop the over-conservative allowlist, keep only a valid-enum check. - #4: gateway drops events for a since-disabled channel (record.enabled guarded in _handle_message and _handle_decision). - agentscope-ai#8: Feishu fetches its own bot open_id via /bot/v3/info and gates on the bot's own mention rather than any mention. - agentscope-ai#12: ChannelTypeRegistry.__bool__ + lifespan guards so the dispatcher is neither built nor started when no channel types are registered. - agentscope-ai#13: in-memory bus honors queue TTL (expire on push+drain) and caps the media buffer drain at _MEDIA_BUFFER_MAX. - session name shortened to "{channel_type}:{chat_id}". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FeishuChannel ran lark's blocking ``client.start()`` on a daemon thread; it parks forever on ``run_until_complete(_select())``, so setting the stop flag and join(timeout=5) never made it return — the thread lingered and kept delivering events after the channel was disabled or updated. Capture the WS thread's own event loop and, in teardown (which the lifecycle dispatcher triggers via task.cancel() on disable/update), stop it via call_soon_threadsafe so client.start() unblocks, the thread exits, and join succeeds promptly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… broadcast - Approval card: update in place via the card-action callback response instead of a separate PATCH that lost the Feishu rate-limit race against the approved run's streaming burst (approve now resolves). - Streaming card: PATCH settings to disable streaming_mode on finish so the "generating" indicator clears; runs on both reply-end and the require-confirm terminal. - Session naming: derive a compact "Feishu/群名/张三" name from the platform, the group title (cached chat-info fetch), and scope; resolve() now also returns the session scope. - HITL: broadcast the applied UserConfirmResult/ExternalExecutionResult on the session stream so observers that didn't make the decision (other tabs, the channel card) can close the pending approval. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restructure the channels page into "connected + dashed divider + add-by-platform gallery". Each platform card opens the create dialog with that channel type pre-selected; configured channels show a tinted avatar and routing rule count. Also surface a per-source icon in the chat sidebar when sessions mix origins (user/schedule/channel). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend
- Replace the ReplyPresentation abstraction: show_tool_process / show_thinking
move into each channel's Config; _render takes them as params.
- ChannelStatus{state,last_error} owned by the adapter (state machine:
stopped/connecting/retrying/connected/failed); get_status returns it, or
stopped when not running (no fabricated status, no heartbeat/registry).
- Give up reconnecting after repeated never-connected attempts: park in
'failed' (task stays alive, so reconcile won't restart it) — no auto-disable.
- Feishu: resolve group + p2p user names for session naming; brand icon +
description; type icon_url/description on the schema.
- Channels can carry a display name; platform_config editable; list_sessions_by
_channel storage + GET /channels/{id}/sessions; all router returns are
BaseModels. Discord split into _discord/_channel.py + re-export __init__.
Frontend
- Channels page: single rounded panel, connected + gallery add-by-platform,
Item cards with a status Switch, resizable detail panel (config/routing/
sessions tables, jump-to-chat), create/edit dialogs render credentials +
config schemas, unified controls, shadcn Empty/Alert. Schedule page shell
aligned. Chat sidebar session colour unified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # examples/web_ui/frontend/src/api/index.ts # src/agentscope/app/_app.py
- Feishu @-gate: apply the only_at_reply check before download so unmentioned group media is dropped too (it used to skip the gate and buffer into the sender's next mention); fail closed when the bot's identity is unknown instead of letting every group message through. - HITL card clicks: carry the resolved agent_id/session_id on the approval card (feishu + discord) and resume that exact run, instead of re-resolving routing on click — which misrouted decisions whose original message matched on metadata, or in per-chat-user scope when a different member clicked. Falls back to routing for older cards. - Channel session context: add ChatKind + best-effort chat_kind()/ chat_name() on the channel adapters (feishu via get-chat, primed from inbound; discord via the channel object), and append an English preamble to channel-bound agents describing the platform, the chat name, group-vs-private, and the platform tools available. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`offload_data_block` was made public and its persisted URL switched to the portable `workspace:///data/<hash>.<ext>` scheme (both in the shared workspace base). The local test was migrated but the docker test still called `_offload_data_block` and asserted the old `file:///workspace/ data/` scheme, failing CI. Mirror the local test: call the public method and map the workspace-relative URL path onto the host mirror. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ENCHIGO
added a commit
to ENCHIGO/agentscope
that referenced
this pull request
Aug 6, 2026
…6-08-06 Core moved while the v1 draft sat; the big deltas: - agentscope-ai#1995 closed 'core gap 1' (hard interrupt): stop reason now derives from ReplyEndEvent.finished_reason; parked replies abort via the new UserInterruptEvent input - agentscope-ai#2117: DEFAULT-mode read-only fast path (Read never prompts), batch confirmation de-duplication, bypass-immune safety ASKs - agentscope-ai#2001: on_check_permission middleware — documented as an alternative permission-bridge design; park/resume kept, with rationale - op-id binding (invariant c) redesigned: consumer-side ContextVar timing cannot reach concurrent tool tasks; a tool middleware claims pending calls from inside the tool's own context instead - corrections: Glob uses the bundled _glob_helper.py (never find); stop_on_reject is dead config; SDK pin -> 0.12.0 and v1 schema/SDK version facts fixed; workspace backend list extended; new channel subsystem (agentscope-ai#1997) added to the adapter framing
situgong
pushed a commit
to situgong/agentscope_ts
that referenced
this pull request
Aug 28, 2026
--------- Co-authored-by: DavdGao <gaodawei.gdw@alibaba-inc.com>
This was referenced Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
AgentScope Version
2.0.4dev
Description
Checklist
Please check the following items before code is ready to be reviewed.