Skip to Content
设计声明式 Agent 定义 — 移植自 Claude Code 2.1.168

声明式 Agent 定义 — 移植自 Claude Code 2.1.168

内部设计文档,用于将 Claude Code 的声明式 agent(markdown + YAML frontmatter)schema 移植到 qwen-code,解决 issue #4821 ,并与 issue #4721  / PR #4732  中的 workflow 移植工作进行协调。

实现状态(垂直切片)

PR #4842  当时发布了这些字段,并提供了端到端的运行时路径。随后,PR #4870  替换了 YAML 解析器以支持块标量(block scalars)。本后续 PR 建立在这两者的基础之上:它替换了 YAML stringifier(PR #4870 保留了手工实现的版本——参见 docs/design/yaml-parser-replacement.md),在 SubagentConfig 上暴露了 mcpServers + hooks,并将它们连接到运行时,以便在 subagent 运行时实际触发每个 agent 的 MCP servers 和 hooks。

字段状态备注
permissionModeshipped (#4842)在解析时桥接到现有的 qwen approvalMode
maxTurnsshipped (#4842)接入现有的 runConfig.max_turns 运行时路径
color allowlistshipped (#4842)将现有字段收紧为 CC 的 _Y 集合 + auto 遗留哨兵值处理
mcpServersshipped (follow-up)通过 eemeli/yaml stringify 实现嵌套 YAML 往返安全;运行时覆盖通过 subagent Config 包装器合并 session + agent servers,并强制重建 tool-registry
hooksshipped (follow-up)在 subagent 生成时注册临时 HookRegistry 条目,通过 onStop 移除;v1 全局触发(无 agent 作用域过滤)
effortdeferredqwen providers 中尚不存在模型层的 effort 参数
memorydeferredqwen 的 auto-memory 尚无 user/project/local 作用域区分
isolationdeferredworkflow PR #4732 负责运行时;每个 agent 的默认值将在该 PR 落地时一并落地
initialPromptdeferred需要 --agent CLI flag(qwen 中尚无 main-session-agent 基础设施)
skillsdeferred需要 SkillManager 消费 config.skills

下面的完整逆向工程记录被保留作为延迟字段的参考设计——schema 常量、DL7/Ig5 语义、错误信息以及与 workflow 的协调矩阵对于该项工作仍然至关重要。


阶段 0 — 边界

项目
已验证的最新上游版本Claude Code 2.1.168(issue #4821 引用 ≥ 2.1.167,我们高一个版本)
原生二进制文件/private/tmp/cc-2.1.168/package/claude (220 MB)
提取的字符串/private/tmp/cc-2.1.168/claude.strings(约 342k 行)
Worktree.claude/worktrees/gifted-hamilton-684741
分支基于 main @ 45efb1d3alazzy/gifted-hamilton-684741
超出范围PR #4732 workflow 代码(独立的 worktree lazzy/lucid-pare-974192)—— 仅通过接口进行协调
编写规则作者为 LaZzyMan;根据 ~/.claude/CLAUDE.md 的规定,在 commits、PRs、issues 或 comments 中 禁止 使用 Co-Authored-By 或 AI 工具相关的 trailers

阶段 1 — 逆向工程发现

此处的所有声明均已针对 claude.strings 进行了独立的 grep 验证,并经受住了对抗性反驳。置信度级别:C = 已确认(直接的二进制证据),I = 已推断(从多个已确认的事实中综合得出),O = 待定(仍不确定)。

Schema — 15 个字段,经过反驳与重新确认

agent frontmatter 的 shadow schema 是 Ig5,在 ug5.agent 内部用于 tengu_frontmatter_shadow_unknown_key / _mismatch 遥测。生产环境的 loader 是 DL7parseAgentFromMarkdown),它执行手工实现的逐字段验证并带有自定义错误信息。另一个独立的 JSON 格式 schema JL7(由 fL7 / parseAgentFromJson 使用)更为严格,但属于不同的代码路径(用于 --agents <json>settings.agents)。

#字段类型 (Ig5 / DL7)是否必填默认值枚举 / 约束置信度
1name字符串,非空无 — DL7: if(!T||typeof T!=="string")return nullC strings:308120, 309074
2description字符串,非空JL7: .min(1, "Description cannot be empty")C strings:308120, 309074, 309076
3model字符串undefinedinherit(不区分大小写)规范化为字面量 "inherit";否则直接传递并去除首尾空格C strings:308120, 309075, 309076
4toolsstring|array (MDH union)undefined单个 token *undefined(表示“继承所有”);通过 AXH/FbK 复制C strings:308120 (MDH/AXH), 309075
5disallowedToolsstring|array (MDH)undefined“如果设置了 tools 则忽略”(根据 describe 文本);由调用方强制执行C strings:308120, 309075
6effortstring|integerundefined枚举 GN=["low","medium","high","xhigh","max"]int;别名 P37={med:"medium"}C strings:308120, 309075, GN/P37 inline
7permissionMode字符串undefined枚举 $E = Gmq = [...kc],其中 kc=["acceptEdits","auto","bypassPermissions","default","dontAsk","plan"](6 个值)C strings:307649 (kc), 308120, 309075
8mcpServersz.unknown() (Ig5); array(jL7) (JL7)undefined每个元素:string 或 record(string, MCPServerSpec);在 DL7 中逐元素进行 safeParseC strings:308120, 309075, 309076
9hooksz.unknown() (Ig5); _u() (JL7)undefined在运行时通过 TKO_u().safeParse 进行延迟验证(settings.json hooks 结构)C strings:308120, 309073 (TKO), 309076
10maxTurnsunion(number, string, null)undefined正整数(由 W46 解析 — 接受数字或数字字符串)C strings:308120, 309075 (W46), 309076
11skillsstring|array (MDH)[] (输出)通过 ml(q.skills) = FbK(H) ?? [] 规范化;无 * 通配符(与 tools 不同)C strings:308120, 309075
12initialPrompt字符串undefined仅包含空白 → undefined;仅当 agent 是 main session 时自动提交(通过 --agent / settings),作为 subagent 时忽略C strings:308120, 309075
13memory字符串undefined枚举 ["user","project","local"]C strings:308120, 309075, 309076
14backgroundstring|bool (eiH=EL8)undefined接受 true / false / "true" / "false";仅 truthy 值规范化为 true,否则为 undefinedC strings:308120, 309075
15isolation字符串undefined枚举 仅限 ["worktree"]不是 ["none","worktree"] — 那是 strings:313284 处用于 background-session 设置的另一个 schema)C strings:308120, 309075, 309076

经受住反驳的细微观察:尽管 skills 是“可选的”,但 DL7 的输出子句是 ...I !== void 0 && {skills: I},且 ml(undefined) 返回 [](非 undefined),因此即使 frontmatter 中省略了该字段,最终输出的记录也会包含 skills: []。这会影响下游的相等性检查——在 qwen-code 移植中需要注意标记此问题。

15 个字段之外可能的额外字段

#字段类型默认值枚举 / 约束置信度
16color字符串undefined枚举 _Y = ["red","blue","green","yellow","purple","orange","pink","cyan"];描述为 "@internal — display color in the agents UI";在解析时,_Y 之外的值会被静默丢弃(DL7 输出 ...z && typeof z === "string" && _Y.includes(z) && {color: z}C strings:308120, 309075, _Y inline
这是 #4821 列表之外唯一新增的 agent frontmatter 字段。在 Ig5 / JL7 上搜索但找到的字段包括:versiontagslabelscategoryiconalias / aliasesexperimentaldeprecatedownerauthorhomepagedisplayNameshortDescription(这些字段仅出现在 skill schema bg5 或不相关的标识符中)。

Loader — 文件与函数映射

职责函数位置置信度
顶层注册表组装器QL(导出名称 getAgentDefinitionsWithOverridesstrings:309076C
文件系统遍历器(与 skills/commands/output-styles 共享)Gm(通过 h6 进行记忆化)strings:312887C
每个 .md 文件的发现d_q(即 loadMarkdownFiles,使用 ripgrep 并带有 --files --hidden --follow --no-ignore --glob *.md 参数,3 秒 AbortSignal.timeout,当 __("true") 时回退到 wY3strings:312887C
单文件解析器(Markdown)DL7(即 parseAgentFromMarkdownstrings:309074C
单文件解析器(JSON)fL7(即 parseAgentFromJson),使用 JL7 schemastrings:309073C
插件 agent 加载器b0_ → 每目录 oR7 → 每文件 sR7strings:308780, 308779C
内置项naH() — 发出 [JqH=general-purpose, KL7=statusline-setup, …] 以及隐式的 YI=forkstrings:309073, 308663C
覆盖解析器DS()(即 getActiveAgentsFromList)— 参见解析顺序strings:309073C
缓存失效u0_()(即 clearAgentDefinitionsCache)— 清除 QL.cache + Gm.cachestrings:309073C
文件系统监听器(chokidar)s_T() → 模块初始化时 Q4_=s_T()WB6strings:316417C

Gm("agents", _) 读取三个 baseDirs(policySettingsuserSettingsprojectSettings),每个都标记在记录上,然后按 inode 去重(丢弃来自符号链接/硬链接的相同 inode 重复项,并记录日志 Skipping duplicate file '<path>' from <source> (same inode already loaded from <firstSource>))。遥测:tengu_dir_search 包含 managedFilesFounduserFilesFoundprojectFilesFoundprojectDirsSearchedsubdir

解析顺序 — 最终优先级

函数 DS() 根据 source 过滤输入,然后将一个固定顺序的数组迭代到一个以 agentType 为键的 Map 中。由于 Map.set 会覆盖,最后被处理的桶获胜

[built-in, plugin, userSettings, projectSettings, flagSettings, policySettings] ^ 最高优先级
来源出处覆盖优先级置信度
built-innaH()(硬编码在二进制文件中)1(最低)C strings:309073
pluginb0_ → 每个插件的 agentsPath/agentsPaths2C strings:308780
userSettings~/.claude/agents/CLAUDE_CONFIG_DIR~/.claude3C strings:312887, 307489
projectSettings<cwd>/.claude/agents/ 加上 iV_() 向上遍历至主目录 / git 根目录4C strings:312887, iV_ inline
flagSettings--agents <json> CLI 标志(schema qKO = h.record(h.string(), JL7())5C strings:330190, 309076
policySettings系统管理的目录:macOS /Library/Application Support/ClaudeCode/.claude/agents,Linux /etc/claude-code/.claude/agents,Windows C:\Program Files\ClaudeCode\.claude\agents6(最高)C strings:307649 (H2), 312887

冲突会被静默解决 — 仅触发 tengu_plugin_name_collision 遥测事件(winner_source: T.at(-1));不会向用户显示“X 覆盖了内置项”的警告。(strings:308742 hMH。)

细微行为:iV_()cwd 向上遍历时采用最内层优先,但由于 Map.set 是最后写入者胜出,因此在 projectSettings 中,外层树的 .claude/agents/ 会覆盖内层树。这有些出人意料 — 在开放问题中标记。

Frontmatter 解析器

问题答案置信度
使用了什么库? — 手工编写的分割器 lz 调用 Bun.YAML.parse(通过包装器 l5H)。二进制文件中没有 gray-matterjs-yamlfront-matterC strings:307902 (l5H), 307905 (lz), 110303 (Bun.YAML errors)
正则表达式n5H = /^---\s*\n([\s\S]*?)---\s*\n?/C strings:307905
失败处理YAML 解析失败 → 使用 tab 转 2 空格规范化进行重试;如果仍然失败,在 warn 级别记录日志 Failed to parse YAML frontmatter in <file>: <err> 并返回 {frontmatter: {}, content: body}绝不抛出异常)C strings:307905, 151839
正文提取在闭合的 --- 之后进行纯字符串切片 H.slice(K[0].length);随后由 v$H 规范化(可能是去除前导换行符)C strings:307905
在 agents / skills / commands / output-styles 之间共享? — 相同的 lzIq_(skill 加载器)、f13(已弃用的 commands 加载器)以及通过 Gmd_q 的 agent 加载器复用C strings:312690
Schema 验证器Zod v4(已打包)。存在 v4 专属标记 looseObjecttreeifyErrorprettifyErrortoJSONSchemaC strings:141270-141395, 141586
验证模式影子模式ahH("agent", frontmatter) 为遥测运行 ug5.agent().strict().safeParse();DL7 忽略该结果并继续执行其自身的逐字段验证。宽松的 frontmatter 对象是运行时的真实数据源。C strings:308120 (ahH/ug5), 309074 (DL7 calls but ignores)
遥测事件tengu_frontmatter_shadow_unknown_keytengu_frontmatter_shadow_mismatch(通过进程内 Set A37 去重)C strings:154634, 154636

连接 — Agent 工具 + CLI 标志

层级功能置信度
Task/Agent 工具 schema ($_3)声明 subagent_type: string.optional();省略时回退到 general-purpose(如果 AI() 返回 true 则回退到 forkC strings:~309220
子 agent 查找针对 toolUseContext.options.agentDefinitions.activeAgents 执行 activeAgents.find(a => a.agentType === requestedType)C strings:~309220
模糊回退MWK(s) = s.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, "");模糊匹配 → AgentTypeError;清晰重新匹配 → tengu_subagent_type_normalizedC strings:~309220
权限门控lV_(toolPermissionContext, "Task", agentType) — 拒绝时 → Agent type '<x>' has been denied by permission rule 'Task(<x>)' from <source>.C strings:~309220
System-prompt 来源Markdown 正文变为 getSystemPrompt: () => body + ('\n\n' + UVH(agentType, memoryScope) when memory enabled) — 在解析时捕获闭包C strings:309074-6 (DL7)
主线程渲染Pp({mainThreadAgentDefinition, …}) — 如果 agent 具有 appendSystemPrompt: true(兜底的 claude 内置项),正文将追加到默认值;否则替换默认值C strings:311015
--agent <name> CLI通过 Commander 声明;action 处理器 if(I) process.env.CLAUDE_CODE_AGENT = I; — 塞入环境变量,在其他地方读取到 appState.agent 中。同时记录在 pid 文件中。C strings:330190, 142138
--agents <json> CLI独立的标志;JSON 记录 {name: {description, prompt, …}}qKO = h.record(h.string(), JL7()) 验证;以 source: flagSettings 加入相同的 activeAgents 注册表C strings:330190, 309076

生命周期 — 冷加载 + 热重载

方面行为配置
冷加载懒加载 — QL 通过 h6(缓存包装器)进行记忆化;首次访问读取文件系统 + 插件,后续访问返回缓存C 字符串:309076
热重载机制chokidar watcher s_T() 在模块初始化时注册(WB6);监听 .claude/agents(用户 + 项目)以及 skills + commands 目录C 字符串:316417
Watcher 配置项persistent:true, ignoreInitial:true, depth:2, awaitWriteFinish:{stabilityThreshold,pollInterval}, ignored:(p,s) => s?.isFile() ? !p.endsWith(".md") : false, usePolling:kZ4 (macOS true), events add/change/unlinkC 字符串:316417
防抖300 ms (l_T = 300);处理函数调用 RIH(), Vv(), u0_(), …u0_() 使 agent 缓存失效C 字符串:316417, 309073
自适应轮询活跃状态 = n_T = 2000 ms 间隔;空闲状态(r_T = 60000 ms 内无交互)→ i_T = 30000 ms;切换时重新创建 chokidar 实例C 字符串:316417
/agents 斜杠命令用于管理 agents 的 local-jsx UI(Library/create/edit/delete/run)— 不是重新扫描命令C 字符串:314593
/reload-plugins 斜杠命令重新运行 QL(W8()),重新计算 agents 数量;覆盖来自插件的 agents(chokidar 监听这些)C 字符串:314595, 190948
其他失效路径clearSessionCaches(由 /clear 使用)也会调用 u0_()C 字符串:313246

待确认问题(Phase 1)

#问题配置解决路径
Q1#4821 中遗漏 color 是有意为之(因为它是 @internal)还是疏忽?O视为有意为之 — 移植该字段但标记为 internal/UI-only
Q2宽松的 DL7 行为(background 接受字符串,maxTurns 接受字符串)是文档中记录的用户可见特性,还是向后兼容的 hack?O保持一致进行镜像处理,但在移植文档中发出警告
Q3为什么 isolation 枚举 ["worktree"] 仅用于 agents,而 background-session 设置 schema 却接受 ["none","worktree"]O可能是“无隔离” = 省略字段;需明确记录
Q4--agents <json>(flagSettings)有意设置在优先级 5(高于 project,低于 policy)吗?Oqwen-code 可以在 v1 中跳过该 flag,推迟决定
Q5iV_ 的最内层优先 push + Map.set 后发制人 → 对于 projectSettings 冲突,外层树获胜。是坑还是有意为之?Oqwen-code 应选择最内层获胜语义以避免该坑

Phase 2 — qwen-code 实现计划

当前状态 — 一段式概览

qwen-code 已经提供了大量的 subagent 基础设施: SubagentManagerpackages/core/src/subagents/subagent-manager.ts)实现了对 .qwen/agents/(项目级)和 ~/.qwen/agents/(用户级)中 markdown+YAML frontmatter 文件的 CRUD 操作,底层由自定义 YAML 解析器(packages/core/src/utils/yaml-parser.ts — 无 gray-matter / yaml 依赖,已由 package.json 确认)支持。SubagentConfigpackages/core/src/subagents/types.ts:41-122)已包含 namedescriptiontoolsdisallowedToolsapprovalModesystemPromptmodelrunConfigcolorbackgroundSubagentLevel 已支持五个作用域(session、project、user、extension、builtin),优先级为 session > project > user > extension > builtinsubagent-manager.ts:189-220)。Agent tool(packages/core/src/tools/agent/agent.ts)声明了 subagent_type,并通过 subagentManager.changeListener 动态刷新其 schema 枚举。packages/core/src/extension/claude-converter.ts:162-220 中已存在一个 convertClaudeAgentConfig() 桥接函数,包含 tool-name 映射和 permissionMode → approvalMode 映射。差距在于:(a) schema 缺少 #4821 中的 8 个字段(effort、作为一等公民的 permissionModemcpServershooks、作为顶层的 maxTurnsskillsinitialPromptmemoryisolation);(b) 没有 --agent <name> CLI flag;(c) 没有 chokidar 风格的热重载(存在 extension 风格的失效机制,但不适用于文件系统 agents);(d) maxTurns 目前嵌套在 runConfig.max_turns 下 — 需要根据 #2409 将其提升为顶层。

架构决策

D1. 复用现有的 yaml-parser 处理 frontmatter

决策: 复用 packages/core/src/utils/yaml-parser.ts(已被 SubagentManager.parseSubagentContent 和 skill loader 使用)。 理由: Claude Code 的 lz 是用于 skills + commands + agents 的同一个共享解析器;qwen-code 已经镜像了该模式。引入 gray-matterjs-yaml 是不必要的改动。现有解析器处理 --- … --- 分割,并对格式错误的输入保持静默(符合 lzwarn-and-return-empty 策略)。

D2. 解析 / 优先级顺序

决策: 使用 `session > project (.qwen/agents/) > user (~/.qwen/agents/)

extension > builtin— 即**保留现有的 qwen-code SubagentLevel 顺序,在 v1 中不要镜像 Claude Code 的flagSettings/policySettings 桶**。 **理由:** Claude Code 的 policySettings(托管目录)是企业级部署场景,qwen-code 目前没有。通过 flag 注入的 agents(—agents )是高级用户特性,可以放到 P4。现有的五级 qwen-code 优先级已经涵盖了 #4821 关注的场景:project 覆盖 user 覆盖 built-in。extension` 级别可以干净地插入到 user 和 > builtin 之间。

D3. 校验 — 保留现有的 SubagentValidator

决策: 扩展 SubagentValidatorpackages/core/src/subagents/)以校验这 8 个新字段。不要引入 zod,除非 skillManager 的 pipeline 已经在使用它;如果现有的校验器是手写的,就保持手写。 理由: Claude Code 的 Ig5 仅在 shadow 模式下使用 — 运行时校验是手写的 DL7。匹配该模式可以保持错误信息易读(例如 Agent file <path> has invalid permissionMode '<x>'. Valid options: …),而无需引入另一个依赖。如果 skillManager 已经使用了 zod,则为了保持一致性遵循该选择 — 待 P1 准备阶段阅读 skill 代码后确定(TBD)。

D4. 热重载 — 推迟;依赖冷加载 + 显式重载

决策: v1 提供 chokidar watcher。缓存失效钩子已存在(subagentManager 具有 changeListener 和显式的 CRUD 驱动刷新)。项目级重载在 session 启动时发生;session 内通过 /agents UI 的编辑会触发失效。如果存在用户需求,/reload-agents(或搭载在 /reload-plugins 上)斜杠命令可以放到 P4。 理由: 通过 FS watcher 实现热重载成本很高(chokidar 增加了一个带有自适应调度的轮询循环 — 仅 Claude Code 的实现就有约 150 行状态管理代码)。启动时冷加载对 v1 来说完全足够,并且符合当前 SubagentManager 的接线方式。为 P4 敞开大门。

D5. 接入 --agent <name> CLI flag — v1 范围内

决策:--agent <name> 添加到 packages/cli/src/config/config.ts 的 CliArgs 中。行为:根据已解析的 registry 进行查找,将该 agent 设置为主线程 agent,如果名称无法解析则抛出明确的错误。匹配 Claude Code 语义(替换默认 system prompt,除非 agent 具有 appendSystemPrompt: true)。不要使用 CLAUDE_CODE_AGENT 环境变量间接传递 — qwen-code 的 Config 对象可以直接携带它。 理由: 这是 #4821 面向用户的入口 — 没有它,声明式 agents 只能通过 Agent tool 的 subagent_type 参数访问,这对于“设置我的默认 agent”用例来说太间接了。--agents <json>(复数)可以推迟到 P4。

D6. Workflow.agentType 协调 — 接口契约

决策: 暴露一个稳定的 resolver 接口,以便 PR #4732 的 createProductionDispatch 在合入时可以调用。具体而言:

契约负责方
Frontmatter 的 name 就是 workflow 的 agentType 字符串(键相等,区分大小写)本 PR
Workflow 硬编码的 disallowedTools 底线([SEND_MESSAGE, EXIT_PLAN_MODE],镜像自上游 Tg8;在 PR #4732 中验证为 ToolNames.SEND_MESSAGEToolNames.EXIT_PLAN_MODE)与 agent 级别的 disallowedTools并集 — 底线始终应用,即使 agent 定义中设置了 toolsworkflow PR 消费
每次调用的 opts.isolation 覆盖每个 agent 的 isolation: 'worktree' 默认值workflow PR 消费
设置时,agent 定义中的 modeleffortpermissionModemaxTurns 覆盖 workflow 默认值workflow PR 消费
Agent body 成为 subagent 的 systemPrompt;当 agentType 无法解析时,workflow 的 WORKFLOW_SUBAGENT_SYSTEM_PROMPT 作为回退workflow PR 消费
agentType 未设置或解析失败时,workflow 回退到内置的 workflow subagent(优雅降级,不抛出异常)workflow PR 消费
#4721 / #4821 冲突的解决toolsdisallowedTools 的优先级):此次移植对 agent registry 进行了改写,使得 disallowedTools 始终与 tools 分开携带。#4821 表格中“如果设置了 tools 则忽略”的规则由 Agent-tool 的调用方(即在构建 subagent 的 ToolConfig 时)来强制执行,而不是在解析时执行。这使得 workflow 始终能将其基础集合(floor)与 disallowedTools 取并集,而无需考虑 agent 是否设置了 tools。agent registry 只是一个纯粹的数据载体;优先级规则存在于调度点(dispatch site)。这解决了 #4821 的“忽略”规则与 #4721 的“并集”规则之间表面上的冲突。

Tool 名称规范化: 使用 ToolNames.SEND_MESSAGEToolNames.EXIT_PLAN_MODE(已对照 PR #4732 的 diff 验证),一旦合入,它们将作为命名常量从 packages/core/src/agents/runtime/workflow-orchestrator.ts 导出。declarative-agents 的移植本身不需要导入这些——它们是 workflow 的基础集合,在 workflow 调度点应用。

模块布局

路径新增 / 修改用途
packages/core/src/subagents/types.ts修改SubagentConfig 中添加 8 个新字段:effortpermissionMode(已通过 approvalMode 映射——是否保留两者?见下文 D7)、mcpServershooksmaxTurns(提升为顶层字段,废弃 runConfig.max_turns)、skillsinitialPromptmemoryisolation
packages/core/src/subagents/subagent-manager.ts修改扩展 parseSubagentContent / serializeSubagent 以支持新字段的双向转换(round-trip);扩展 SubagentValidator 调用
packages/core/src/subagents/subagent-validator.ts(假设路径)修改添加与 DL7 错误信息匹配的逐字段验证:Agent file <path> has invalid permissionMode '<x>'. Valid options: … 等。
packages/core/src/subagents/agent-frontmatter-schema.ts新增枚举常量的唯一真实来源(Single source of truth):EFFORT_VALUESPERMISSION_MODE_VALUESMEMORY_VALUESISOLATION_VALUESCOLOR_VALUES。完全镜像 Claude Code 2.1.168。
packages/core/src/subagents/builtin-agents.ts修改新字段默认为 undefined;无行为变更
packages/core/src/tools/agent/agent.ts修改在构建 subagent 选项时从解析后的 SubagentConfig 中读取新字段(modelmaxTurnspermissionModeeffort);为 #4721 引入 isolation 的每次调用覆盖语义
packages/cli/src/config/config.ts修改添加 --agent <name> 标志;在启动时通过 SubagentManager 进行解析;如果名称无法解析则报错
packages/cli/src/config/config.test.ts修改--agent 标志解析 + 错误路径的测试
packages/core/src/extension/claude-converter.ts修改在导入 Claude .md 文件时添加新字段的映射(mcpServershooksmaxTurns 提升为顶层、memoryisolation 等)
packages/core/src/subagents/agent-frontmatter-schema.test.ts新增枚举列表的快照测试;解析/序列化的双向转换测试
packages/core/src/subagents/subagent-manager.test.ts修改新字段验证、优先级、错误信息的测试
packages/core/src/tools/agent/agent.test.ts修改新字段接入 subagent 运行时的测试
docs/cli/agents.md(如果存在)或 docs/declarative-agents.md新增面向用户的参考文档:16 个字段的 schema + 示例

D7. permissionMode 与 approvalMode —— 桥接而非替换

决策: 在 frontmatter 中同时接受 permissionMode(兼容 Claude)和现有的 approvalMode(兼容 qwen)。在解析时,如果设置了 permissionMode,则使用 claude-converter.ts:195-208 中的现有表格将其映射为 approvalModedefault → defaultplan → planacceptEdits → auto-editdontAsk → defaultbypassPermissions → yolo)。如果两者同时存在,则 approvalMode 优先(对 qwen-code 更具体),并发出一个 tengu_frontmatter_shadow_* 风格的遥测事件,注明两者均已设置。理由: 保持与现有使用 approvalMode.qwen/agents/*.md 的向后兼容性,同时原样接受 Claude Code 的 permissionMode,以便用户可以直接放入 Claude Code 的 agent 文件而无需修改。

Schema 映射表

Claude Code 2.1.168 字段qwen-code 字段适配备注
namename相同,必填
descriptiondescription相同,必填
modelmodel接受 inheritfasthaikusonnetopusauthType:model-idqwen-code 已支持更广泛的词汇;inherit 为新增
toolstools接受 string|array;* → undefined (继承全部)已作为 array 支持;新增 string + * 处理
disallowedToolsdisallowedTools接受 string|array;始终与 tools 分开携带优先级规则(#4821 “如果设置了 tools 则忽略”)由调用方强制执行,而非解析器
efforteffort(新增)枚举 low/medium/high/xhigh/max + 整数;别名 med → medium运行时效果是 qwen 特有的(如果存在则映射到现有的 thinking-effort 旋钮,否则存储并忽略)
permissionModepermissionMode(新增)+ 桥接至 approvalMode枚举 acceptEdits/auto/bypassPermissions/default/dontAsk/plan;映射表见 D7原样接受 Claude 格式
mcpServersmcpServers(新增)(string | {name: spec}) 的数组;逐项验证,丢弃无效条目并警告在 P4 中接入 MCP 运行时
hookshooks(新增)匹配 settings.json hooks 结构的对象在 P4 中接入 hook 运行时
maxTurnsmaxTurns(新增顶层字段)正整数;接受数字字符串以保持对等runConfig.max_turns 提升;保留嵌套形式作为废弃别名
skillsskills(新增)skill 名称的数组;也接受逗号分隔的字符串运行时:在 agent 启动时通过 skillManager 预加载
initialPromptinitialPrompt(新增)字符串;仅包含空白 → undefined;仅在 agent 为主会话时触发通过 --agent 标志路径接入
memorymemory(新增)枚举 user/project/local;从 .qwen/agent-memory/<name>/ 等路径加载运行时在 P4 中实现
backgroundbackground接受 bool 或字符串 "true"/"false";仅 truthy → true已支持;放宽解析规则
isolationisolation(新增)枚举仅限 ["worktree"]运行时由 workflow PR (#4732 P3+) 负责;registry 仅携带该字段
color(未文档化 #16)color枚举 _Y = ["red","blue","green","yellow","purple","orange","pink","cyan"];超出范围的值被静默丢弃已在 qwen SubagentConfig 中;收紧验证以匹配 Claude Code 允许列表

TDD 测试计划

模块测试文件断言内容
Schema 枚举常量agent-frontmatter-schema.test.ts (new)EFFORT_VALUESPERMISSION_MODE_VALUESMEMORY_VALUESISOLATION_VALUESCOLOR_VALUES 与 Claude Code 2.1.168 逐字节匹配(快照)
解析器 — 正常路径subagent-manager.test.ts往返解析包含所有 16 个字段的 .qwen/agents/test.md → 输出的记录具有预期的结构
解析器 — 必填字段subagent-manager.test.ts缺少 name 返回 null 并记录 warn 日志;缺少 description 返回 null 并记录 warn 日志
解析器 — 枚举校验subagent-manager.test.ts错误的 permissionMode / memory / isolation / effort / color 会分别触发特定的 warn 日志(符合 DL7 措辞),并丢弃该字段
解析器 — 宽松字段类型subagent-manager.test.tsbackground: "true"truemaxTurns: "5"5effort: "med""medium"tools: "Read,Edit"["Read","Edit"]tools: "*" → undefined
解析器 — 颜色白名单subagent-manager.test.tscolor: "magenta" 会被静默丢弃(无报错),color: "blue" 会被保留
Skills 字段特殊行为subagent-manager.test.ts省略 skills 会导致 skills: [](符合 Claude Code DL7 的输出行为)
解析优先级subagent-manager.test.ts项目与用户中存在相同 name → 项目优先;用户与内置中存在相同 name → 用户优先;扩展与内置中存在相同 name → 扩展优先
Inode 去重subagent-manager.test.ts两个路径指向同一个 inode(符号链接)→ 仅保留一条记录,并输出日志
permissionMode 桥接subagent-manager.test.tspermissionMode: bypassPermissions → 解析为 approvalMode: yolo;两者均设置 → approvalMode 优先 + 记录遥测数据
--agent CLI flagpackages/cli/src/config/config.test.ts该 flag 设置主线程 agent;未解析的名称会抛出 Agent type '<x>' not found. Available agents: … 异常
Agent 工具模糊回退agent.test.tssubagent_type: "Test_Engineer" 通过 NFKC 小写规范化解析为已注册的 test-engineer
Agent 工具未找到错误agent.test.ts未解析的 subagent_type → 错误信息匹配 Agent type '<x>' not found. Available agents: <list>
Workflow 契约agent-frontmatter-schema.test.ts导出的 getAgentByName(name) 接口返回完整的 SubagentConfig,包括 isolationdisallowedToolsmodeleffortpermissionModemaxTurns(可供 workflow PR #4732 使用)

分阶段 PR 计划

阶段标题范围阻塞项
P1feat(core): declarative agent schema fields (effort, permissionMode, maxTurns top-level, memory, isolation, color allowlist)SubagentConfig 添加字段;扩展解析器 + 校验器 + 序列化器;废弃 runConfig.max_turns;添加枚举常量模块;测试
P2feat(core): wire new agent fields into Agent tool runtimemodeleffortmaxTurnspermissionMode/approvalMode 桥接传入 AgentTool.execute()AgentHeadless.create() 调用点;测试P1
P3feat(cli): --agent flag for main-thread agent selectionCliArgs 添加 --agent <name>;在启动时解析;错误路径;测试P1
P4(optional, scope-creep) feat(core): mcpServers + hooks + skills + initialPrompt + memory runtime将四个“在 v1 中仅作元数据”的字段接入实际的运行时效果P1,以及 skill/MCP/hook 子系统

每个 PR 目标 ≤ 800 行代码变更(不含测试);P1 最大,包含约 600 行校验器代码 + 测试。


阶段 3 — 与 workflow 移植的协调矩阵 (#4721 / PR #4732)

声明式 agents 特性Workflow 交互负责方阻塞于
name 字段作为注册表键Workflow 的 opts.agentType 查找字符串(#4721  明确指定)本 PR 定义注册表契约;workflow PR 消费无 — 注册表结构可先稳定下来
agent 上的 disallowedTools 字段Workflow 与硬编码底线 [SEND_MESSAGE, EXIT_PLAN_MODE] 取并集(根据 #4721  §2 — 已对照 PR #4732 diff 验证:ToolNames.SEND_MESSAGEToolNames.EXIT_PLAN_MODE本 PR 携带该字段;workflow PR 在分发时取并集workflow PR #4732 P3 合入
agent 上的 tools 字段Workflow 原样传递给 subagent 的 ToolConfig.tools本 PR 携带该字段;workflow PR 接入workflow PR #4732 P3
agent 上的 model 字段Workflow 的 opts.model 在每次调用时覆盖;agent 的 model 作为默认值本 PR 携带该字段;workflow PR 解析优先级workflow PR #4732 P3
agent 上的 effort 字段Workflow 调用点覆盖优先;回退到 agent 默认值本 PR 携带该字段;workflow PR 解析workflow PR #4732 P3
agent 上的 permissionMode 字段在分发时映射为 subagent 的 approvalMode;Workflow 调用点覆盖优先本 PR 通过 D7 桥接携带该字段;workflow PR 接入workflow PR #4732 P3
agent 上的 maxTurns 字段当 agent 设置该字段时,替换 workflow 硬编码的 WORKFLOW_SUBAGENT_MAX_TURNS = 50本 PR 携带该字段;workflow PR 解析优先级workflow PR #4732 P3
agent 上的 isolation: 'worktree' 字段默认值;每次调用的 opts.isolation 可覆盖(#4721  §3)本 PR 携带该字段;workflow PR 负责运行时workflow PR #4732 P3+(目前在 P1 中会抛出异常)
agent 上的 initialPrompt 字段Workflow 不使用它(仅当 agent 通过 --agent 作为主会话时触发)本 PR + CLI无(独立)
memorymcpServershooksskills除了传递给 subagent 运行时外,Workflow 无特殊处理本 PR 携带这些字段;运行时接入在 P4 / 未来未来 PR
EXCLUDED_TOOLS_FOR_SUBAGENTS 更新Workflow PR #4732 将 WORKFLOW 添加到该集合中(根据 issue/PR 上下文发现 — 尽管对抗性反驳指出这尚未在 main 分支的 agent-core.ts 中,仅在 worktree 中)workflow PR 负责;本 PR 未修改
workflow 底线的工具名称规范形式 (ToolNames.SEND_MESSAGE)本 PR 不导入底线常量;它仅按原样携带 disallowedTools 字符串。规范化由 workflow PR 负责。workflow PRworkflow PR #4732
发布顺序本 PR (P1+P2+P3) 独立于 workflow 发布。Workflow PR #4732 P3 取决于本 PR 的类似 getAgentByName() 的解析器可被导入。在 workflow 的 P3 之前并行workflow P3 读取本 PR 的导出
无循环阻塞: 本 PR 和 workflow PR 可以在其 P1/P2 阶段并行合入。它们在 workflow-P3 阶段同步,该阶段需要本 PR 提供的 registry resolver。如果本 PR 先合入,workflow-P3 将直接读取它。如果 workflow PR 先合入,它将使用现有的 subagent_type 查找(未命中时返回 workflow 默认值)发布,并在本 PR 合入后切换到更丰富的 resolver。

阶段 4 — 风险与未决问题

风险

#风险缓解措施
R1Claude Code 小版本发布之间的 Schema 漂移 (2.1.168 → 2.1.x)通过文档注释将 enum 常量模块固定为“已针对 2.1.168 验证”;作为 feature-reverse skill 的一部分,针对新版本重新运行 strings-grep
R2runConfig.max_turns → 顶层 maxTurns 是对现有 .qwen/agents/*.md 文件的破坏性 schema 变更保留嵌套形式作为废弃别名,进行一个周期的废弃处理;在解析时发出 warn,并在 CHANGELOG 中记录
R3permissionModeapprovalMode 往返转换有损(Claude 有 6 种模式,qwen 有 4 种左右)根据 D7 显式映射双向转换;在双重设置时发出 telemetry;在保存时不要静默重写
R4新字段(hooksmcpServersskillsmemory)在 registry 中携带但在 v1 中无运行时支持 → 用户可能会设置它们但静默无效明确记录 v1 范围;当“已携带但尚无运行时支持”的字段非空时,为每个 agent 发出一次性的 info 日志
R5对抗性验证指出 EXCLUDED_TOOLS_FOR_SUBAGENTSmain 分支上未包含 WORKFLOW —— 这可能意味着 workflow 移植尚未合入,或者缺少 recursive-fanout 守卫与 workflow PR 作者(LaZzyMan = 本人)确认该守卫随 PR #4732 合入,而不是在本次移植中
R6如果照搬 outer-tree-beats-inner-tree 的 projectSettings 行为(Q5),将是一个容易引发误操作的隐患qwen-code 明确选择 innermost-wins;通过 R5 fixture 进行测试
R7字段 color 在二进制的 describe 文本中被记录为 @internal —— 我们可能正在移植 Anthropic 明确不支持的功能移植它,但在 qwen-code 文档中也标记为 @internal;视为仅限 UI 使用;不要在面向用户的参考文档中暴露

未决问题 — 建议的解决方案

#问题解决方案
Q1#4821 中遗漏 color 是故意的吗?视为故意遗漏。移植该字段;在面向用户的文档中不要提及,除非作为“可用,内部使用”。
Q2宽松的 DL7 行为:记录还是 hack?镜像该行为。为了保持一致性,接受 background: "true"maxTurns: "5"effort: "med",即使未记录在案。添加测试。
Q3为什么 agent schema 和 background-session schema 之间的 isolation enum 不同?在代码注释中记录这种差异;“无隔离” = 字段被省略,而不是一个 enum 值。
Q4--agents <json>(复数,flagSettings)应该在 v1 中合入吗?推迟到 P4。这是面向高级用户的 CLI 接口;v1 仅发布 --agent <name>(单数),这也是 #4821 所关注的。
Q5嵌套的 .qwen/agents/ 中 inner-tree 与 outer-tree 的优先级?Innermost-wins。覆盖 Claude Code 意外的 outer-wins 行为。在 P1 中添加测试 fixture。
Q6toolsdisallowedTools 的优先级:#4821 称“如果设置了 tools 则忽略”;#4721 称“与 workflow 底线取并集”Registry 是纯数据。解析器独立保留这两个字段。优先级规则存在于调度点(Agent tool / workflow)。解决这一矛盾。
Q7workflow disallowedTools 底线的工具名称规范形式 —— 已针对 PR #4732 验证为 ToolNames.SEND_MESSAGEToolNames.EXIT_PLAN_MODE不属于本 PR 的范围 —— 由 workflow PR 负责。仅在协调矩阵中记录。
Q8#2409 的 close-resolution 会影响什么吗?继承 #2409 的“将 model + maxTurns 提升到顶层”的指导。已融入本计划中。
Q9qwen-code 现有的 SubagentLevel 优先级中,extension 级别的 agent 应该保持在 builtin 之上(当前状态)还是之下(Claude Code 没有等效项)?保持 extension > builtin。Extensions 是用户安装的;built-ins 是供应商默认的。用户安装的优先。
Q10issues #4821、#4721、#4732 是否已针对本文档提出的契约进行了完整规范?在 #4821 上发布协调评论,链接本文档,总结逐字段的决策,并要求维护者确认:(a) 与 Claude Code 2.1.168 的 16 个字段保持 schema 一致,(b) D7 permissionMode/approvalMode 桥接,(c) D2 优先级顺序,(d) 将 registry 作为纯数据以解决 tools/disallowedTools 矛盾。

协调行动项

#行动位置
A1在 #4821 上发布逐字段总结 + 5 项决策以供维护者确认在 #4821 上发表评论
A2从 #4721 交叉链接本文档,并注明阶段 3 矩阵在 #4721 上发表评论
A3一旦本次移植的 P1 合入,通知 #4732 切换到更丰富的 resolver在 PR #4732 上发表评论(准备就绪时)
A4针对下一个 Claude Code 小版本重新运行 strings-grep 以检测 schema 漂移feature-reverse skill 定时任务(在此之前手动执行)
Last updated on