qwen-code 声明式 Agent 移植:Claude Code Agent 文件 Schema 兼容方案与源码实现详解
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
本文基于 qwen-code 仓库的设计文档 declarative-agents-port.md,完整解析如何把 Claude Code 2.1.168 的声明式 Agent 定义(Markdown + YAML frontmatter)移植到 qwen-code 子代理体系中:16 个 frontmatter 字段的反向工程结论、字段级解析与桥接规则、permissionMode → approvalMode映射、per-agentmcpServers/hooks的运行时接线,以及五级存储层级的优先级解析。读完你可以直接理解 qwen-code 中.qwen/agents/*.md文件的完整字段语义、宽松解析(lenient parse)行为,以及为什么某些字段被推迟实现。
一、移植目标与实现状态
该移植工作的目标(对应上游 issue #4821 与 #4721)是让 Claude Code 的.claude/agents/*.md声明式 Agent 文件能够原样放入 qwen-code 的.qwen/agents/并被正确解析。整个移植采用"垂直切片"(vertical-sliced)方式交付:
- 第一个 PR(#4842)带来了核心字段并打通端到端运行时路径(
permissionMode、maxTurns、color白名单); - 第二个 PR(#4870)把 YAML 解析器替换为支持块标量的实现(参见 yaml-parser-replacement.md);
- 后续 PR 把
mcpServers与hooks表面化到SubagentConfig上,并接线到运行时,使 per-agent 的 MCP 服务器与 hooks 在子代理运行时真正生效。
当前各字段的落地状态如下(引自设计文档的 Implementation status 表):
| 字段 | 状态 | 说明 |
|---|---|---|
permissionMode | 已交付 | 解析期桥接到 qwen 现有的approvalMode |
maxTurns | 已交付 | 接入现有runConfig.max_turns运行时路径 |
color白名单 | 已交付 | 收紧为 Claude Code 的_Y颜色集,保留auto旧哨兵 |
mcpServers | 已交付(后续 PR) | 通过 eemeli/yaml序列化保证嵌套 YAML 往返安全;运行时通过子代理 Config 包装器合并会话级 + Agent 级服务器,并强制工具注册表重建 |
hooks | 已交付(后续 PR) | 临时 HookRegistry 条目在子代理派生时注册、onStop时移除;v1 全局触发(尚无 Agent 级作用域过滤) |
effort | 推迟 | qwen 各 provider 尚不存在模型层effort参数 |
memory | 推迟 | qwen 的 auto-memory 尚无user/project/local作用域区分 |
isolation | 推迟 | 运行时归 workflow 移植(PR #4732)所有,per-agent 默认值随其落地 |
initialPrompt | 推迟 | 需要--agentCLI 标志(qwen 尚无主会话 Agent 基础设施) |
skills | 推迟 | 需要 SkillManager 消费config.skills |
二、反向工程:Claude Code 2.1.168 的 Agent Schema
设计文档的 Phase 1 通过对 Claude Code 2.1.168 原生二进制的字符串提取(约 342k 行)做了对抗性证伪式的反向工程。关键结论是:Agent frontmatter 存在两套 schema——影子 schemaIg5(仅用于遥测)与生产加载器DL7(parseAgentFromMarkdown),后者是逐字段手工校验、带自定义错误信息的宽松实现;另有一套更紧的 JSON 形式 schemaJL7(供--agents <json>与settings.agents使用)。
2.1 15 + 1 个 frontmatter 字段
| # | 字段 | 类型 | 必填 | 默认 | 枚举 / 约束 |
|---|---|---|---|---|---|
| 1 | name | string,非空 | 是 | — | 缺失直接返回 null |
| 2 | description | string,非空 | 是 | — | Description cannot be empty |
| 3 | model | string | 否 | undefined | inherit(大小写不敏感)归一化为字面量"inherit",否则 trim 后透传 |
| 4 | tools | string | array | 否 | undefined | 单 token*→ undefined(表示"继承全部") |
| 5 | disallowedTools | string | array | 否 | undefined | "若设置了tools则被忽略"(由调用方强制) |
| 6 | effort | string | integer | 否 | undefined | 枚举["low","medium","high","xhigh","max"]或整数;别名med → medium |
| 7 | permissionMode | string | 否 | undefined | 6 值枚举:acceptEdits/auto/bypassPermissions/default/dontAsk/plan |
| 8 | mcpServers | record | 否 | undefined | 每项为 string 或record(string, MCPServerSpec),逐条safeParse |
| 9 | hooks | record | 否 | undefined | 运行时按 settings.json hooks 形状惰性校验 |
| 10 | maxTurns | number | string | null | 否 | undefined | 正整数,数字或数字字符串均可 |
| 11 | skills | string | array | 否 | [](会输出) | 逗号串归一化;无*通配 |
| 12 | initialPrompt | string | 否 | undefined | 纯空白 → undefined;仅当 Agent 为主会话时自动提交 |
| 13 | memory | string | 否 | undefined | 枚举["user","project","local"] |
| 14 | background | string | bool | 否 | undefined | 接受true/false/"true"/"false";仅真值归一化为true |
| 15 | isolation | string | 否 | undefined | 枚举仅["worktree"](不含none——"无隔离"即省略字段) |
| 16 | color | string | 否 | undefined | 8 色枚举;超出值在解析期被静默丢弃;标注为@internal(仅 UI 展示色) |
一个经对抗性验证的细节:skills虽为可选,但DL7的输出子句会在 frontmatter 省略该字段时仍然输出skills: []——这会影响下游的相等性检查,是移植时必须留意的行为。
2.2 解析器与热加载生命周期
Claude Code 的 frontmatter 解析不依赖gray-matter/js-yaml,而是手写分片器 +Bun.YAML.parse:
- 分割正则为
/^---\s*\n([\s\S]*?)---\s*\n?/; - YAML 解析失败时先做 tab→2 空格归一化重试,仍失败则记 warn 日志并返回
{frontmatter: {}, content: body},从不抛异常; - 该分片器被 agents / skills / commands / output-styles 四类加载器共享;
- schema 校验是"影子"模式:Zod v4 的
strict().safeParse()结果只用于遥测(tengu_frontmatter_shadow_unknown_key/_mismatch),生产路径完全以DL7的逐字段宽松校验为准。
热加载方面,Claude Code 使用 chokidar watcher 监听.claude/agents(用户 + 项目两级),300ms 去抖后调用clearAgentDefinitionsCache失效缓存;活跃期轮询间隔 2000ms,空闲(60s 无交互)降为 30000ms,切换时重建 watcher 实例。插件来源的 Agent 不被 watcher 覆盖,需靠/reload-plugins重新装配。
2.3 优先级决议
Claude Code 的DS()(getActiveAgentsFromList)按固定顺序把 6 个来源桶迭代进一个以agentType为键的Map,Map.set覆盖语义使最后写入的桶获胜:
[built-in, plugin, userSettings, projectSettings, flagSettings, policySettings] ^ 最高优先级即 policySettings(系统托管目录)最高,built-in 最低;同名冲突被静默解决,只触发tengu_plugin_name_collision遥测。一个反直觉的行为:项目级目录树内层目录先 push、但 last-wins,导致外层.claude/agents/压过内层——设计文档明确标记这是"陷阱",qwen-code 移植决定不镜像该行为。
三、qwen-code 现状与架构决策
3.1 移植前的基础设施
qwen-code 在移植前已具备可观的子代理基础设施:
- SubagentManager 对
.qwen/agents/(项目级)与~/.qwen/agents/(用户级)中的 markdown + YAML frontmatter 文件提供 CRUD; - SubagentConfig 已有
name、description、tools、disallowedTools、approvalMode、systemPrompt、model、runConfig、color、background; - SubagentLevel 已是五级作用域
session > project > user > extension > builtin; - Agent 工具(packages/core/src/tools/agent/agent.ts)声明
subagent_type参数并动态刷新 schema 枚举。
3.2 关键架构决策(D1–D7)
| 决策 | 内容 |
|---|---|
| D1 | 复用现有 yaml-parser.ts 处理 frontmatter——与 Claude Code 共享解析器的架构模式一致,不引入gray-matter/js-yaml新依赖 |
| D2 | 优先级沿用 qwen-code 现有五级session > project > user > extension > builtin,v1 不镜像 Claude Code 的flagSettings/policySettings桶(企业管理目录是 qwen 没有的场景) |
| D3 | 扩展现有SubagentValidator手工校验,不引入 zod——与 Claude Code 影子 schema + 手工校验的形态对齐,保持错误信息可读 |
| D4 | v1 不交付 chokidar 热加载,依赖冷加载 + 显式失效(changeListener) |
| D5 | 交付--agent <name>CLI 标志;不采用 Claude Code 的CLAUDE_CODE_AGENT环境变量间接层,Config对象直接承载 |
| D6 | 对 workflow 移植暴露稳定的 resolver 接口契约:frontmattername即 workflow 的agentType字符串(键相等、大小写敏感);workflow 硬编码的disallowedTools地板[SEND_MESSAGE, EXIT_PLAN_MODE]与 agent 级disallowedTools取并集,地板恒生效 |
| D7 | permissionMode与approvalMode双收桥接:解析期把permissionMode映射到approvalMode;两者同时存在时approvalMode获胜(对 qwen 更具体),并发出"双写"遥测 |
D6 同时解决了 #4821("设置了tools则忽略disallowedTools")与 #4721("与 workflow 地板取并集")的表面矛盾:注册表是"笨数据载体",两个字段始终独立携带,优先级规则放在派发点(Agent 工具 / workflow)而非解析期。
3.3 字段映射表(Claude Code 2.1.168 → qwen-code)
| Claude Code 字段 | qwen-code 字段 | 适配 |
|---|---|---|
name/description | 同名 | 完全一致,必填 |
model | model | 接受inherit、fast、具体 model-id、authType:model-id |
tools | tools | string | array;逗号串切分;*→ undefined(继承全部) |
disallowedTools | disallowedTools | string | array;与tools独立携带 |
permissionMode | permissionMode+ 桥接approvalMode | 6 值枚举,映射表见下节 |
maxTurns | maxTurns(新顶层字段) | 正整数,兼容数字字符串;由runConfig.max_turns提升而来,旧嵌套形式保留为弃用别名 |
mcpServers | mcpServers | record-of-records 浅校验;逐 spec 深校验推迟到运行时 MCP loader |
hooks | hooks | record-of-arrays 浅校验;逐 matcher 校验由SessionHooksManager负责 |
background | background | 接受 bool 或"true"/"false"字符串,仅真值 →true |
color(未文档化 #16) | color | 8 色白名单 + qwen 旧auto哨兵;超界值静默丢弃 |
isolation | 推迟 | 枚举仅["worktree"],运行时归 workflow PR |
effort/memory/skills/initialPrompt | 推迟 | 见第一节状态表 |
四、源码实现:字段解析与桥接
4.1 枚举常量单一事实源
设计文档要求的新模块 agent-frontmatter-schema.ts 是枚举常量的单一事实源,逐字镜像 Claude Code 2.1.168:
- PERMISSION_MODE_VALUES:
acceptEdits、auto、bypassPermissions、default、dontAsk、plan; - COLOR_VALUES:
red、blue、green、yellow、purple、orange、pink、cyan; - parseMaxTurns:接受正整数 number 或数字字符串,其余一律返回 undefined(对应 CC 的
W46)。
permissionMode → approvalMode映射表(claudePermissionModeToApprovalMode)值得细看:
| permissionMode | approvalMode | 语义说明 |
|---|---|---|
default | default | 工具调用需确认 |
plan | plan | 计划模式 |
acceptEdits | auto-edit | 自动批准编辑 |
auto | auto-edit | 同上 |
bypassPermissions | yolo | 全部自动批准 |
dontAsk | default | Claude 侧dontAsk语义偏限制(会弹窗的调用一律拒绝),因此映射到同样需要确认的default而非自动批准的auto-edit,保留限制意图 |
实现上特意用Map而非普通Record,防止调用方传入'__proto__'/'constructor'沿原型链取到非字符串值——这是从源码结构中可见的防御性细节。
4.2 宽松解析(lenient parse)的逐字段行为
核心解析逻辑在 parseSubagentContent。frontmatter 分割用正则/^---\n([\s\S]*?)\n---\n([\s\S]*)$/,随后逐字段处理:
permissionMode:仅当值命中 6 值枚举才保留,否则 warn 并丢弃(Agent file <path> has invalid permissionMode '<x>'. Dropping field.);仅当approvalMode未设置时才桥接,即effectiveApprovalMode = approvalMode ?? bridgedApprovalMode——与 D7 "双写时 qwen 字段赢"的决策完全一致;maxTurns:走parseMaxTurns,非法值 warn 并丢弃;color:白名单内或遗留auto哨兵保留,其余静默丢弃并记 warn;background:"true"/true→true,"false"/false→false,其余 warn 并置 undefined;mcpServers:parseAgentMcpServers 做 record-of-records 浅校验——非对象整体丢弃、值为标量/数组/null 的条目逐条丢弃,并返回空原型对象(Object.create(null))避免 YAML 键字面量__proto__触发原型链污染;hooks:parseAgentHooks 仅保留值为数组的条目,逐 matcher 的深校验推迟到SessionHooksManager。
这套"丢弃而非抛错"(drop-the-whole-field)姿态与 Claude CodeDL7的宽松语义一致:一个写坏的mcpServers块不会杀死整个 Agent 定义。源码注释明确说明这与 qwen 早期字段(如approvalMode,非法值直接抛错拒绝加载)的严格姿态有意区分,以保护存量.qwen/agents/*.md文件。
值得注意的是,qwen-code 在此镜像 schema 之外增加了一个executor扩展字段(SubagentExecutorSpec):声明kind: 'acp' | 'codex'、command、args?,让子代理的 turn 交给外部 Agent 进程执行。它与mcpServers/hooks的宽松姿态刻意相反——解析失败时拒绝加载整个定义(named executor refusal),因为丢弃该字段会让任务静默地以 Qwen 进程内模型代跑,形成"静默替换"。这一 fail-closed 设计在 subagent-manager.ts 中有大量防御注释,包括用 YAML 真实 AST(parseDocument)而非宽松行式解析探测executor:声称,避免块标量中的散文误判。
4.3 校验器
SubagentValidator 保留了手工校验路线(D3 决策的落实):
- 名字校验(validateName):2–50 字符、仅字母数字连字符下划线、不能以
-/_开头结尾,且拒绝保留名self/system/user/model/tool/config/default/main(main是/stats归因管线的哨兵,子代理占用会静默并入主会话桶); description必填非空,超过 1000 字符仅告警;systemPrompt至少 10 字符,超过 10000 字符告警;model经resolveModelId解析校验,显式写inherit会收到"省略即可"的提示;runConfig.max_turns必须是正整数,超过 100 告警;max_time_minutes超过 60 告警。
五、运行时接线:per-agent MCP 服务器与 hooks
设计文档声明mcpServers与hooks已从"仅携带元数据"升级为真正生效。源码中的落地路径:
5.1mcpServers:Config 包装器 + 强制注册表重建
在 buildSubagentContextOverride 中:
- 用
deriveConfig派生一个薄的 Config 包装器作为子代理运行时上下文(顺带让子代理获得独立的FileReadCache,避免继承父进程的已读记录而削弱"先读后写"约束); - 若 frontmatter 声明了
mcpServers,则sessionServers与config.mcpServers按键合并——同名键时 Agent 级覆盖会话级(more-specific-wins,对齐 Claude Code 的scope: 'agent'语义); - 只要存在 per-agent 服务器就绕过
hasRebuiltToolRegistry跳过优化,强制rebuildToolRegistryOnOverride——否则父级注册表缓存的McpClientManager永远看不到合并后的服务器集合,per-agent 发现会静默空转; - 新注册表以
skipDiscovery: true构造后从父级回填工具,因此 per-agent 服务器需要显式discoverToolsForServer,且用Promise.allSettled并行发现——单个挂死的 stdio 服务器不会串行拖慢整个子代理派生,单个失败仅记 warn 不阻塞其他服务器; - 返回的
cleanup回调持有新注册表(其 stdio 子进程/ socket 是父级Config.shutdown够不到的),由createAgentHeadless的dispose闭包在子代理终止时执行。
5.2hooks:临时注册 + 作用域标注 + 信任门禁
在 createAgentHeadless 中:
- 子代理派生时以
agent:<name>:<uuid>作为作用域,调用hookRegistry.addAgentHooks注册 frontmatter hooks,返回的 unregister 回调挂到dispose上(构造失败路径也会执行,防泄漏); - 项目级 Agent 的 hooks 受信任门禁:非受信目录(untrusted folder)中的项目 Agent 可以只读加载,但其 hooks 属于仓库提供的代码执行,与
Config.getProjectHooks()对 settings 文件 hooks 的门禁一致,直接忽略并 warn; - 宿主没有 HookSystem 时也仅 warn 忽略,不抛错;
- v1 局限(与设计文档一致):条目驻留注册表期间对同类型的所有事件全局触发,尚无 per-agent 作用域过滤。
5.3 模型路由
model选择器经resolveModelId/buildRuntimeContentGeneratorView解析:当选择器指向不同 provider 或调用方要求 per-agent 推理强度时,构建专属 ContentGenerator + 视图,经 AsyncLocalStorage 在运行期生效,不影响父进程——这与 SubagentConfig.model 文档的inherit/fast/model-id/authType:model-id四种选择器语义对应。
六、存储层级与优先级解析
qwen-code 沿用了 D2 决策的五级结构,loadSubagent的解析顺序(subagent-manager.ts)为:session → project → user → extension → builtin,逐级回退;每级之间还插入throwRecordedExecutorRefusal检查——若某级存在一个声明了executor但加载失败的同名文件,按名派生直接抛错拒绝,而不是回退到低优先级的进程内定义。这正是 4.2 节所述"静默替换"防护的落点。
项目级目录内嵌套目录的冲突,qwen-code 明确选择innermost-wins(最内层获胜),不镜像 Claude Code 意外形成的 outer-wins 行为(设计文档 Q5/R6 的显式决策)。
Agent 工具侧,subagent_type参数(agent.ts)省略时回退到 general-purpose Agent;builtin-agents.ts 提供内建 Agent(如Explore,且其模型可被agents.builtin.exploreModel设置覆盖,见 applyBuiltinSettings)。
七、测试与验证路径
设计文档给出了 TDD 计划,仓库中对应落地的测试文件:
| 测试文件 | 覆盖 |
|---|---|
| agent-frontmatter-schema.test.ts | 枚举常量快照(与 Claude Code 2.1.168 逐字节对齐)、parseMaxTurns/parseAgentMcpServers/parseAgentHooks/parseAgentExecutor、getAgentByName风格的 resolver 导出(供 workflow 消费) |
| subagent-manager.test.ts | 全字段往返解析、必填字段缺失、非法枚举值的 warn + 丢弃、宽松字段类型(background: "true"、maxTurns: "5")、color 白名单、优先级决议 |
| validation.test.ts | 校验器错误与告警路径 |
| subagent-manager-override.test.ts | per-agent MCP 覆盖与注册表重建 |
| packages/core/src/tools/agent/agent.test.ts | subagent_type解析与运行时字段管道 |
八、风险与推迟字段的取舍
设计文档 Phase 4 列出的风险中,与实现直接相关的取舍:
- R2(
maxTurns提升是破坏性变更):旧嵌套形式runConfig.max_turns保留为弃用别名,两者同设时顶层maxTurns获胜(见 SubagentConfig.maxTurns 注释),解析时发 warn,记入 CHANGELOG; - R4(携带但无运行时效果的字段):
effort、memory、skills、initialPrompt在 v1 只做"携带",设计上要求文档明确 v1 边界,避免用户设置后静默无效; - R7(
color是 Claude 的@internal字段):照搬但同样标记 internal,不作为用户面向文档内容,仅 UI 展示用。
推迟理由均可从源码结构印证:effort需要 provider 层的推理强度参数(qwen 已有reasoningEffort概念,见 buildRuntimeContentGeneratorView 中的modelConfig.reasoningEffort,但 frontmatter 尚未接通该旋钮);isolation的 worktree 运行时归 workflow 移植所有;initialPrompt依赖尚不存在的--agent主会话选择器。
九、小结
qwen-code 的声明式 Agent 移植是一次"schema 逐字镜像 + 运行时按需接线"的工程:frontmatter 的 16 字段语义、枚举常量、宽松解析姿态与 Claude Code 2.1.168 保持逐字节对齐,使.claude/agents/*.md可以原样放入.qwen/agents/;permissionMode → approvalMode桥接、mcpServers的合并 + 强制注册表重建、hooks的临时注册 + 信任门禁,则把"仅携带"的字段推进为真正生效的运行时能力;而 qwen-code 独有的executor扩展用 fail-closed 拒绝加载替代宽松丢弃,划定了镜像与扩展之间的边界。对维护者而言,枚举事实源在 agent-frontmatter-schema.ts、解析行为在 parseSubagentContent、运行时接线在 buildSubagentContextOverride,三处即可覆盖该特性的全部实现面。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考