Skip to content

feat(channel): implement channel - #1997

Merged
DavdGao merged 47 commits into
agentscope-ai:mainfrom
qbc2016:bc/channel
Aug 6, 2026
Merged

DavdGao merged 47 commits into
agentscope-ai:mainfrom
qbc2016:bc/channel

Conversation

@qbc2016

@qbc2016 qbc2016 commented Jul 3, 2026

Copy link
Copy Markdown
Member

AgentScope Version

2.0.4dev

Description

  1. Implement channel
  2. Add feishu channel

Checklist

Please check the following items before code is ready to be reviewed.

  • An issue has been created for this PR
  • I have read the CONTRIBUTING.md
  • Docstrings are in Google style
  • Related documentation has been updated (e.g. links, examples, etc.) in documentation repository
  • Code is ready for review

@DavdGao DavdGao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qbc2016 Please see my inline comments. I'll start by reviewing the core abstractions in this PR — once we've aligned on those, we can move on to discussing the rest of the implementation.

Comment thread src/agentscope/app/storage/_model/_channel.py Outdated
Comment thread src/agentscope/app/channel/_base.py Outdated
Comment thread src/agentscope/app/channel/_base.py Outdated
Comment thread src/agentscope/app/channel/_base.py Outdated
Comment thread src/agentscope/app/channel/_base.py Outdated
Comment thread src/agentscope/app/_router/_channel.py Outdated
Comment thread src/agentscope/app/channel/_manager.py Outdated
Comment thread src/agentscope/app/channel/_manager.py Outdated
Comment thread src/agentscope/app/channel/_registry.py
Comment thread src/agentscope/app/channel/_repository.py Outdated
@DavdGao DavdGao linked an issue Jul 8, 2026 that may be closed by this pull request
Comment thread src/agentscope/app/channel/_base.py Outdated
Comment thread src/agentscope/app/channel/_manager.py Outdated
Comment thread src/agentscope/app/channel/_repository.py Outdated
Comment thread src/agentscope/app/channel/_gateway.py Outdated
DavdGao and others added 15 commits July 28, 2026 20:28
…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>
DavdGao and others added 17 commits August 4, 2026 21:51
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

@DavdGao DavdGao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

DavdGao and others added 4 commits August 6, 2026 16:00
- 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>
@DavdGao
DavdGao merged commit efba74e into agentscope-ai:main Aug 6, 2026
7 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(channel): support app channels in agent service

2 participants