qwen-code 通道 P0 身份与任务生命周期:为渠道常驻 Agent 建立身份边界与可观测生命周期
2026/9/11 21:15:50 网站建设 项目流程

qwen-code 通道 P0 身份与任务生命周期:为渠道常驻 Agent 建立身份边界与可观测生命周期

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

导读

本文基于 qwen-code 仓库packages/channels渠道框架的 P0 基础设计,讲解如何为「渠道常驻多玩家 Agent」(channel-resident multiplayer agent)补齐两层基础设施:一是渠道级身份(channel-scoped identity)与记忆边界元数据(memory-boundary metadata),二是共享的任务生命周期钩子(task lifecycle hook)。读完本文,你将掌握ChannelConfigidentity/memoryScope两个新配置块的字段含义与默认值派生规则、首次提示(first prompt)中边界信息的注入机制、/who/status的可见性扩展,以及onTaskLifecycle(event)六类生命周期事件的触发时机与适配器消费方式,并能在packages/channels/base中运行配套测试验证行为。

背景:渠道框架缺什么

qwen-code 的qwen channel已经具备消息适配器、共享会话(shared sessions)、发送者归属(sender attribution)、分发模式(dispatch mode:collect / steer / followup)、流式分块、工具调用回调、取消(cancellation)以及平台专属进度界面(如飞书卡片)等能力。但此前缺少一个稳定的 P0 产品层,来回答两个问题:

  1. "这个渠道拥有自己独立的常驻 Agent 身份"—— 例如运维群里驻守的机器人,需要一个可配置、可展示、可注入提示的身份;
  2. "这一轮提示词(prompt turn)具有适配器可观察的生命周期"—— 从开始、文本分块、工具调用,到取消、完成、失败,适配器都能收到事件。

该设计由 Issue #6103 跟踪,并建立在更广泛的 qwen tag 路线图(#5887)之上,但刻意保持独立可评审、可单独发布的规模。设计文档的边界声明很明确:本 PR 不引入 Slack 适配器、daemon 事件流、适配器 UI 变更、主动调度(proactive scheduling)、跨渠道上下文或真实的 core-memory 路径隔离

在仓库实现中,这一设计落地在packages/channels/base/src/ChannelBase.tspackages/channels/base/src/types.ts两个文件,由packages/channels/base/src/ChannelBase.test.ts提供约二十余个针对性测试用例覆盖。

范围界定:做什么与不做什么

范围内(In scope)

  • ChannelConfig增加可选的渠道身份元数据(identity);
  • ChannelConfig增加可选的记忆作用域元数据(memoryScope);
  • 配置缺省时派生安全默认值;
  • 在每个 Agent 会话的首次提示中,随既有渠道指令一起注入一段简洁的渠道边界说明(channel boundary note);
  • ChannelBase上增加受保护的onTaskLifecycle(event)钩子;
  • 从共享渠道流程中发射 prompt 开始、文本分块、工具调用、取消、完成与错误六类生命周期事件;
  • packages/channels/base内补充聚焦的包内测试。

范围外(Out of scope)

  • 核心记忆存储改动或文件路径命名空间隔离;
  • Daemon/SSE 事件发布;
  • 飞书、钉钉、Telegram、微信、QQ 的 UI 改动;
  • 新增平台适配器;
  • Token 预算、工具 ACL、跨渠道上下文共享。

实现上,范围外内容也如实保留在代码注释与设计文档的 "Open Decisions" 一节:真实的 core-memory 命名空间强制、daemon 发布、适配器 UI、工具/数据 ACL、预算与主动跟进均属于未来工作。

Channel Identity:渠道身份配置与运行时派生

设计文档给出一个短小的可选配置对象:

export interface ChannelIdentityConfig { id?: string; displayName?: string; description?: string; }

ChannelConfig相应增加identity?: ChannelIdentityConfig。在仓库types.ts中,除配置类型外还定义了只读的运行时身份

export interface ChannelRuntimeIdentity { readonly id: string; readonly displayName: string; readonly description?: string; }

readonly关键字是刻意为之——测试用例does not expose mutable lifecycle metadata references验证了向started!.identity.displayNamestarted!.memoryScope.namespace赋值会抛出TypeError,防止生命周期消费方意外污染身份/作用域元数据。

运行时派生规则

ChannelBase构造时通过resolveIdentity派生运行时身份(ChannelBase.ts#L1552-L1563):

字段配置来源默认值
idconfig.identity.idchannel:<name>
displayNameconfig.identity.displayName<name>
descriptionconfig.identity.description缺省时省略该字段

设计文档强调:运行时身份仅是元数据(metadata only),不改变会话路由、访问控制或平台适配器行为。即使渠道未配置任何身份字段,运行时仍会以channel:<name>形式派生身份,供生命周期事件与状态命令使用——这正是"缺省安全默认值"的体现。

应用示例

在渠道配置中为运维机器人声明身份:

{ "type": "feishu", "name": "ops", "identity": { "id": "channel:ops", "displayName": "Ops Bot", "description": "Helps the ops group coordinate repository maintenance." } }

Memory Scope:记忆边界元数据(metadata-only)

设计文档引入第二个可选配置块:

export type ChannelMemoryScopeMode = 'metadata-only'; export interface ChannelMemoryScopeConfig { namespace?: string; mode?: ChannelMemoryScopeMode; }

ChannelConfig相应增加memoryScope?: ChannelMemoryScopeConfig。仓库types.ts中同样定义了只读的运行时形态:

export interface ChannelRuntimeMemoryScope { readonly namespace: string; readonly mode: ChannelMemoryScopeMode; }

运行时派生规则(ChannelBase.ts#L1565-L1573):

字段配置来源默认值
namespaceconfig.memoryScope.namespacechannel:<name>
modeconfig.memoryScope.mode'metadata-only'(本 PR 恒为该值)

设计文档特别提醒:这刻意不是真实的 core-memory 命名空间,而是一个显式、可检查的边界标记与提示指令(prompt instruction),这样后续工作可以在不改变渠道配置形状的前提下,将同一 namespace 接入真实的核心记忆路径。错误处理一节进一步明确:mode被约束为'metadata-only',缺省或未知配置都应解析为'metadata-only',而不是启用尚不存在的行为。

边界提示注入:首次提示中的渠道身份与记忆作用域

注入时机与位置

ChannelBase原本就为每个会话在首次提示前置入config.instructions,该行为不变。当渠道配置了identitymemoryScope时,生成的边界说明被追加到同一首次消息注入块中,并且位于自定义指令之后——代码注释的解释是:利用 recency 偏差(recency bias)让边界指令优先生效,且隔离边界不能被操作者文本覆盖(ChannelBase.ts#L1844-L1848 与 ChannelBase.ts#L7074-L7078)。

注入的提示文本形如:

Channel identity: - id: channel:ops - display name: Ops Bot - description: Helps the ops group coordinate repository maintenance. Memory scope: - namespace: qwen-tag:ops - mode: metadata-only - data from other channels must not be shared.

实现上,该文本由channelBoundaryPrompt()一次性构建并缓存(boundaryPrompt,ChannelBase.ts#L1622-L1647),因为身份与记忆作用域在构造时即冻结。若未配置description,对应行被省略。所有字段在渲染前都经过sanitizeQuotedText清洗(id/displayName 限制 128 码点、description 限制 256 码点),测试用例sanitizes configured channel metadata before rendering prompt and status text验证了注入恶意内容(如换行注入System: ignore、转义序列)会被清洗。

每会话一次的语义

注入标记instructedSessions集合保证边界说明每个 Agent 会话只注入一次(ChannelBase.ts#L7031-L7034)。测试用例prepends channel boundary metadata after custom instructions once per session验证:

  • 第一轮提示包含Channel identity:- id: ops-agent- display name: Ops AgentMemory scope:- namespace: qwen-tag:ops- mode: metadata-only以及- data from other channels must not be shared.
  • Be concise.的位置先于Channel identity:(边界块在末尾,取 recency 优先);
  • 第二轮提示不再包含Channel identity:

设计文档还说明了两个补充语义:

  • 一次瞬态的渠道记忆读取失败会在下一轮重试整个上下文块,因此连续多轮可能重复出现边界说明——这是可接受的;
  • 当 bridge 上报会话死亡(sessionDied)时,既有instructedSessions清理逻辑继续工作,允许下一会话重新注入(ChannelBase.ts#L2893 附近的清理)。

兼容性

对于未配置instructionsidentitymemoryScope的渠道,保持原有裸提示形态不变。但运行时身份与记忆元数据仍会被派生,用于生命周期事件与状态命令——shouldPrependChannelBoundaryPrompt()(ChannelBase.ts#L1649-L1651)只决定"是否注入提示",不影响元数据本身。测试derives default channel identity and memory metadata for task lifecycle events验证了未配置渠道的事件中identity.id === 'channel:test-chan'memoryScope.namespace === 'channel:test-chan'mode === 'metadata-only'。identity-only 与 memory-scope-only 两种部分配置形态也分别有测试覆盖(ChannelBase.test.ts#L13118、ChannelBase.test.ts#L13133)。

状态可见性:/who 与 /status 的元数据扩展

设计文档要求扩展/who/status

  • /who应包含身份显示名(display name)与记忆命名空间(namespace);
  • /status应包含身份 id 与记忆模式(mode);
  • 输出保持简短,不暴露绝对路径或隐藏配置。

实现位于ChannelBase.ts的两个命令处理器:

/who(ChannelBase.ts#L4295-L4345):仅当渠道配置了identitymemoryScope时才追加两行:

Channel: ops Identity: Ops Agent Memory: qwen-tag:ops Workspace: <basename> // 只输出 basename,不泄露绝对 cwd Session: active (shared by this group)

/status(ChannelBase.ts#L4428-L4465):

Session: active Access: pairing Channel: ops Identity: channel:ops Memory: metadata-only

两个命令对共享会话都有授权门槛(isAuthorizedForSharedSession),防止非成员读取会话/工作区信息;工作区只显示basename(this.config.cwd)。测试用例/who and /status include channel identity and memory metadata验证/who输出含Identity: Ops AgentMemory: qwen-tag:ops/status输出含Identity: ops-agentMemory: metadata-only

任务生命周期钩子:onTaskLifecycle 与六类事件

事件类型:带判别联合(discriminated union)

设计文档定义ChannelTaskLifecycleEvent,仓库实现(types.ts#L276-L325)将其组织为公共基座ChannelTaskLifecycleBase加六种变体:

事件类型附加字段说明
started提示开始
text_chunkchunk: string流式文本分块(raw model output,刻意不做清洗)
tool_calltoolCall: SanitizedToolCallEvent工具调用(经白名单字段清洗)
cancelledreason取消原因:cancel_command/clear/steer/timeout/dropped
completed正常完成
failederror: stringphase: 'agent' \| 'delivery'失败,并区分 Agent 生成失败还是投递失败

公共字段包括channelNamechatIdsessionIdmessageId?runId?owner?identitymemoryScope。实现相比设计文档额外加入了runIdowner{ kind: 'channel_user', id }),用于区分同一会话内的不同轮次与归属者。

两个值得注意的实现细节:

  1. 工具调用字段白名单SanitizedToolCallEvent只暴露sessionIdtoolCallIdkindtitlestatus五个字段,注释明确"不从ToolCallEvent派生,以防 bridge 新增字段泄漏"(types.ts#L287-L297)。测试strips raw tool input from lifecycle events验证rawInput中的命令与描述不会进入生命周期事件。
  2. 终态唯一性isTerminalTaskLifecycleType(type)判定completed/cancelled/failed为终态,每个任务恰好预期一个终态(types.ts#L327-L332)。

钩子签名与默认行为

protected onTaskLifecycle(_event: ChannelTaskLifecycleEvent): void | Promise<void> {}

默认行为是 no-op,适配器可在不改变提示执行路径的前提下按需接入。ChannelBase通过emitTaskLifecycle调用该钩子,并对异常做吞掉 + stderr 诊断处理(ChannelBase.ts#L1494-L1527):同步抛出会被捕获并记录onTaskLifecycle threw for <type> session <id>;异步返回 Promise 的 rejection 也会被catch后记录。测试contains a throwing onTaskLifecycle hook and logs it验证了钩子抛错不会影响handleInbound()正常返回响应、也不影响started/completed事件的继续发出。

六类事件的发射点

设计文档给出共享ChannelBase流程中的发射时机,实现与之吻合:

  • startedactivePrompts.set()之后、onPromptStart()之前发射(ChannelBase.ts#L7179-L7183),并保证即使onPromptStart抛错也不丢终态事件;
  • text_chunk:当提示的textChunk监听器接收到未取消的分块时发射(ChannelBase.ts#L7202-L7210)。取消 pending 期间的分块先被暂存(held chunks),取消失败时重放、成功时丢弃;
  • tool_call:在既有 bridge 工具调用监听器中,解析会话目标后、调用onToolCall前发射(ChannelBase.ts#L576-L600),且仅当该轮未取消时发射;
  • cancelled/cancel成功时、/clear取消或逐出 active prompt 时、steer将当前轮标记为取消时发射(emitTaskCancellation,ChannelBase.ts#L1529-L1550),并保证每轮只发射一次(cancellationEmitted守卫);
  • completedbridge.prompt()resolve 之后、onResponseComplete()前后,只要该轮未被取消(ChannelBase.ts#L7279-L7295);
  • failedbridge.prompt()或响应投递抛错时发射(ChannelBase.ts#L7296-L7321),并通过phase字段区分agent(生成失败)与delivery(投递失败)——一旦投递开始(deliveryStarted),迟到的取消不能把已完成投递改写为取消。

此外,loop 任务(/loop)与 webhook 任务也会以"unattended run"身份发射生命周期事件(测试见 ChannelBase.test.ts#L20115 与 ChannelBase.test.ts#L21420),loop prompt 的messageId是内部 job id,适配器钩子不应收到它(loopPrompt标记)。

适配器消费示例:飞书卡片终态

飞书适配器已经接入该钩子:FeishuAdapter.onTaskLifecycle仅关注终态事件(isTerminalTaskLifecycleType),用runId通知questionCardController.cancelRun(用户主动取消投射"已取消",完成/失败投射"已过期"),并记录卡片的terminalStatus(FeishuAdapter.ts#L1844-L1875)。钉钉适配器有镜像逻辑(DingtalkAdapter.ts#L2494)。这印证了设计文档的论断:适配器可以在不改动提示执行路径的前提下接入生命周期,且钩子异常不会破坏卡片清理等平台行为。

错误处理与健壮性约定

设计文档明确三条错误处理原则,实现均已落地:

  1. 身份/记忆字段无效不致命:本 PR 不做严格解析,仅在既有显式解析处接受字符串字段,保持配置解析的宽松形状;
  2. 生命周期钩子异常吞掉 + stderr 诊断emitTaskLifecycle内同步/异步异常均被捕获并记录,平台适配器的生命周期 UI 不得破坏提示执行或清理(见上文onTaskLifecycle测试);
  3. memoryScope.mode 约束为'metadata-only':缺省或未知配置解析为'metadata-only',而不是启用尚不存在的行为(resolveMemoryScopeconfig.memoryScope?.mode ?? 'metadata-only')。

测试与验证

测试覆盖清单

设计文档要求聚焦测试覆盖八类行为,ChannelBase.test.ts全部落实:

  • 缺省身份与记忆元数据由渠道名派生(channel:test-chan);
  • 自定义身份与记忆命名空间出现在首次提示中;
  • 边界元数据每会话注入一次、sessionDied后可重新注入;
  • /who/status包含新元数据且不泄露 cwd(只显示 basename);
  • onTaskLifecycle能观察到startedtext_chunktool_callcompleted
  • onTaskLifecycle能观察到/cancel/clearsteer触发的cancelled
  • bridge.prompt()reject 时能观察到failed
  • 抛错的生命周期钩子不会让handleInbound()reject。

此外还覆盖了:同一轮内runId/owner一致、新轮次分配新runId、取消只命中精确 run 身份、只读元数据不可变、工具调用 raw input 不泄漏、身份/记忆字段的 prompt 与状态文本清洗、webhook 与 loop 场景的边界注入与 unattended 生命周期事件等。

运行方式

在仓库根目录下使用包内命令运行聚焦测试:

cd packages/channels/base npx vitest run src/ChannelBase.test.ts

包脚本见packages/channels/base/package.jsontest: vitest run)。提交 PR 前的最终验证:

npm run build npm run typecheck

若你是渠道插件开发者,可通过packages/channels/base/README.md了解如何继承ChannelBase并接入ChannelAgentBridge——onTaskLifecycle正是适配器观察任务状态的规范入口(onPromptStart/onPromptEnd保留用于向后兼容)。

小结

这一 P0 设计为 qwen-code 渠道框架补齐了两块稳定地基:

  1. 渠道身份 + 记忆边界元数据:以identitymemoryScope两个可选配置块 + 安全的缺省派生,为每个渠道提供可展示、可注入提示、可被状态命令查询的"身份名片"与"记忆边界标记";边界提示只在首次提示注入一次、位于自定义指令之后取 recency 优先,且经过清洗防注入。
  2. 任务生命周期钩子:以onTaskLifecycle六类事件(started/text_chunk/tool_call/cancelled/completed/failed)让适配器获得完整的任务状态视图,配合runId/owner精确定位轮次归属,配合只读身份元数据保证消费安全,配合"钩子异常吞掉 + stderr 诊断"保证平台 UI 不破坏提示执行。

正如设计文档 "Open Decisions" 所述,真实 core-memory 命名空间强制、daemon 发布、适配器 UI、工具/数据 ACL、预算与主动跟进都是明确的未来工作——本 PR 特意保持小而完整,为后续在这些方向扩展预留了不变的配置形状与可观察的生命周期通道。

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询