跳转至

04 · 产品面全解读

Web、headless、SDK/ACP/hooks 等入口背后,用户看到的是 preset 模式、Trajectory、审批弹窗、plan/goal 与长会话压缩——这些都由同一条事件日志驱动,而不是第二份产品状态。本页按十个议题说明上游产品面如何设计,以及 mini 在哪一层复现。

定位说明

本页内容全部可定位到上游源码或 docs;产品体验的归纳(如四种模式中文名)来自 apps/cli/config/agent-presets/*/preset.yml 的 description 字段直译,属上游真实存在。上游存在多处命名换代(如审批词汇、包路径),相关澄清见各议题。

议题 1:模式设计——四种 agent preset

1.1 产品体验

dsh --profile web 启动后,用户在入口处可切换四种"模式":标准模式、PTC 模式、极简模式、创造模式。四个模式是四个独立组成的 agent,各是一组工具目录 + 一套系统提示词 + 若干能力开关,会话内生效、互不影响。其中 PTC = programmatic tool calls(程序化工具调用):模型写一段程序,在程序内通过宿主机提供的异步绑定(如 await tools.add({...}))组合多步操作(上游更名记录:.agents/notes/archived/architecture/2026-08-25-rename-code-mode-to-ptc.md,旧称 "Code Mode")。

模式 preset 目录(roster) 一句话差别
标准模式 agent-presets/standard/ 完整编码 agent:bash/pwsh、文件系统、skills、goals、plan、上下文压缩、子代理/工作流、web 搜索
PTC 模式 agent-presets/code/ 标准全部能力 + 经 PTC 运行时(ctx.ptcRuntime)呈现的编程工具(run_code):模型写一段程序,组合多步工具调用
极简模式 agent-presets/minimal/ 仅两个工具(持久 bash + str_replace_editor)+ 固定完整提示词,无运行时上下文快照、无压缩
创造模式 agent-presets/cordis/ 标准能力 + 运行时检查与插件实验(inspect/define/run)、preset 创作指导

1.2 实现:preset = 会话级 agent 组合

每个 preset 目录含 preset.yml(名字、描述、排序)与 agent.cordis.yml(组合本体)。关键设计(packages/preset/README.md,14):

  • 挂载点:组合挂在 agent 作用域(scope)下,ctx.agentPresets 提供词汇表与文件系统发现(可信根 + 用户自建根);
  • roster 即目录列表:预置名单就是 apps/cli/config/agent-presets/ 的目录列举,不维护第二份清单(preset/README.md);
  • 进程级设施留在 host plane:注册表、跨会话服务是进程单例,属于 host 组合;preset 只携带"这一个 agent 贡献给它们的东西"。命名了进程级全局服务的行会在挂载时被拒绝,而不是与下一个会话冲突;
  • realm 隔离:service 行必须挂在带 isolate realm 的 group 内(standard/agent.cordis.yml:11-18),否则与根 realm 冲突——这就是"一个进程同时跑多个不同 preset 的 agent"的底层保障。

标准模式组合要点(standard/agent.cordis.yml)

  • agent-plane 组成:service agent-mt(多回合 agent)+ service agent(普通)、goal、plan、compaction、subagent、skills 等;
  • 每进程只挂一次(service 行在 scope 树上的位置决定),会话经 scope parentage 加入 agent 作用域;
  • plan 状态只写日志(不设服务),exit_plan_mode 是唯一退出路径;工具目录跨模式切换保持不变,为了请求缓存稳定(system prompt 不变,KV 缓存复用),这也是"plan 工具也要给"的深层原因(standard/agent.cordis.yml:118 注释)。

极简模式组合要点(minimal/agent.cordis.yml:1-6)

  • persona 块 complete: true:系统提示即全部上下文(不注入运行时快照);
  • includeRuntimeContext: false;无 compaction;
  • 工具目录:持久 bash + str_replace_editor 两个工具——演示"preset 可以只换工具集就得到全新产品形态"。

1.3 源码证据

  • apps/cli/config/agent-presets/{standard,code,minimal,cordis}/preset.yml —— 四个模式的姓名/描述/排序(order 1-4)
  • apps/cli/config/agent-presets/{standard,minimal}/agent.cordis.yml —— 组合差异(agent-plane 组合 vs fixed-prompt 双工具)
  • packages/preset/README.md —— per-session agent composition 设计说明
  • .agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md —— 设计笔记

1.4 mini 对照

上游 mini 现状 规划
preset roster + per-agent mount 出厂内置根 miniharness/preset/{standard,minimal}/ + 用户 $MINIHARNESS_HOME/.agent-presets;preset/presets.py 目录发现即名单、per-agent 挂载视图、host 缺工具 fail loud、进程级冲突拒绝挂载;agent.cordis.yml 可翻译为 mini Preset。上游四模式里的 code/cordis 目录可拷入用户根接入(手册 08 章) 声明式 agent-preset-registry(不扫目录)为上游 rc.2+ 形态,mini 仍以目录 roster 为主
PTC 模式(ctx.ptcRuntime + run_code) miniharness/ptc_runtime/ + 工具层 PTC 派发(tool/ptc-dispatch* 日志、Python worker 子进程、可选 sandbox_mode confine);与 preset 解耦——code preset 目录未随包出厂,但运行时与 wire 已接线(architecture.md 映射表) —

Mini trace(议题 1)

preset/presets.py roster 挂载 → tests/test_presets.py;CLI miniharness presets list(教学扩展)。章 08。

议题 2:外部入口全景

2.1 产品体验

用户以四种方式接触系统:终端交互(web surface)、一次性任务(headless)、自动化和 IDE(三个协议入口)、以及通过官方 SDK 的完整会话操控。所有入口最终都是同一个事件溯源会话内核的不同宿主。

2.2 实现

每个入口共用同一条启动链:解析 profile 与 patch → 层叠 bundle 条目 → boot 经 Loader 激活插件树 → 各 surface 的 startup 插件挂 web 端口或 headless 单任务。内置 bundle 根提供出厂组合,home 用户根提供覆盖;composeEntries 只层叠配置,真正加载在 boot。

flowchart TD
 Bin["apps/cli bin<br/>--profile · --patch · task"]
 Prof["loadProfile<br/>profile.yml 双锚点"]
 Comp["composeEntries<br/>bundle + patch 层叠"]
 Boot["boot + Loader<br/>插件树激活"]
 Surf["surface startup<br/>web · headless · acp · sdk"]
 Bin --> Prof --> Comp --> Boot --> Surf
入口 形态 协议/载体 说明
web surface 浏览器 GUI HTTP + WebSocket(packages/api/gateway:POST 一元 RPC /api/<endpoint> + WS /api/remote.mux 流) 完整产品体验:Trajectory、审批、命令、配置等。演进:早期为 host/apiproxy 的 HTTP+SSE 载体(apiproxy 已在 alpha.1 删除,重组为 gateway WS mux + typert 一元 RPC)
headless CLI 一次性任务 packages/bundle/headless stdout 最后一条非空 assistant 文本;退出码按 turn/end reason;不支持会话恢复(见议题 7)
ACP 协议 自动化客户端协议 packages/acp/acp(stdio JSON-RPC) 机器到 agent 的规范接口(会话/审批/工具调用)
SDK 协议 官方 SDK packages/sdk/protocol Python/TS 官方客户端,全会话操控
hooks 桥接 Claude Code / Codex hooks packages/hooks 把 harness 作为这些 IDE agent 的后端工具执行器

注:三协议入口的会话协议信封与事件序列一致(envelope/编号/消息模型),这是 mini 复现"JSON-RPC 最小子集"(见手册 [07 §7.6](../chapters/07-sdk.md#sec-7-6))的约定依据。

2.3 源码证据

  • apps/cli/src/{bin.ts,args.ts,profile-boot/*,boot.ts} —— 启动与 profile 加载链路
  • packages/bundle/headless/src/{index,startup}.ts —— headless 语义(mini 对照见 §2.4);startup.ts 只解析 task 位置参数与 --help,无 --session
  • docs/architecture.md 与 packages/sdk/protocol —— 协议约定

2.4 mini 对照

mini 用同一套「组合 boot → surface 分裂」:cli/ 承载 headless 与启动器选项;web/ 发布与上游 gateway 对齐的 unary + /api/remote.mux + $events/$events/result;protocol/ 承载 SDK/ACP/hooks 最小子集;loader/ + boot/ 承载组合树。字段级端点与帧语义以 interface-wire.md 为准(与 WebApi.ROUTES 同步),模块归属见 architecture.md 映射表。

面 mini 深读
headless / 启动器 cli/headless.py、cli/main.py 手册 07 §7.2
web 传输 miniharness/web/ interface-wire、手册 07 §7.5
三协议 protocol/{sdk,acp,hooks}.py §7.6–§7.8
组合 / Loader boot/、loader/ 手册 08、报告 03 §5.5
插件 Remote web/api.py、boot/plugin_* interface-wire §4.15

浏览器端:webui/ 只依赖 wire;web/static/ 为旧 SSE 教学参照。cli/session_cmds.py 为教学扩展(上游会话列表在 web 面)。并行工具、重试、YAML 组合、CI 等与入口正交,不在此重复,见 architecture 映射表与手册 04/05/12。

Mini trace(议题 2)

T1 headless · T3 web unary · T4 审批 · tests/test_web_server.py · session/create 等在 WebApi.ROUTES(101 条)。

议题 3:Trajectory 轨迹台账

3.1 产品体验

Trajectory 是 web 专属的"Agent 的 DevTools":一个按 turn 组织的可检查事件台账(packages/client/ui-trajectory/README.md)。它把原始事件流折叠成 User/Assistant/Tool/嵌套 Subtool 的记录,用户可:按时间线扫读(Overview 区域,四种投影模式、TTFT 两色标注)、展开任意记录看局部检查器(Summary/Payload/Timing/Input/Output)、全文搜索(浏览器内)、折叠展开。长会话呈虚拟化滚动:尾部打开、向上翻页加载更早记录。

澄清(重要)

Trajectory 不是独立数据系统,也不是某种"session-timeline"包——全仓 grep 无该包。它就是同一份事件溯源日志在浏览器端的投影视图;session-query(议题 7)与它无数据关系。

3.2 实现

Trajectory 从同一份会话日志出发:session/page 按消息边界向前分页,浏览器端用纯函数折叠定义物化节点,再虚拟化渲染。TTFT(time to first token,首 token 延迟)与 Overview 都从折叠结果重算,不另建采集系统。

flowchart LR
 Log["Session 日志&lt;br/&gt;唯一数据源"]
 Page["session/page RPC&lt;br/&gt;消息边界分页"]
 Fold["折叠 definition&lt;br/&gt;match · update · finalNode"]
 Snap["TrajectorySnapshot"]
 UI["webui Trajectory&lt;br/&gt;虚拟化 · 搜索"]

 Log --> Page --> Fold --> Snap --> UI

分页边界取 append-origin 消息边界,折叠窗口内不会出现半截消息。虚拟化只挂载可见行窗口(阈值 100 行、overscan 12),长会话向上翻页加载更早记录。

  • 折叠引擎是纯函数:每个折叠定义由 match/update/finalNode 构成(trajectory-*-definition.ts),无副作用、可重入——这是"日志投影"而非"状态机"的本质;
  • 保留边界:设计笔记明确拒绝"把日志拍平成裸记录流"——Turn/Step/Request 边界保留因果结构(.agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.md);
  • Overview:从同一折叠数据投影真实开始时间与耗时(TTFT 等),不是另一套采集;
  • 搜索:浏览器内增量索引(trajectory-search-index.ts),随事件到达增量更新,不走 session-query;
  • 虚拟化:只挂载可见行(阈值 100 行、overscan 12、DOM 上限 160,trajectory-virtualization.e2e.ts 固定该约定);
  • 分页:依赖 session/page 的消息边界切页语义(议题 7),保证折叠窗口内消息完整。

3.3 源码证据

  • packages/client/ui-trajectory/README.md —— Trajectory 定义(turn 组织的可检查回放)
  • packages/client/ui-conversation/src/client/conversation/assembler.ts —— 折叠窗口与 per-target 物化
  • packages/client/ui-trajectory/src/client/trajectory-contract.ts —— TrajectorySnapshot 结构
  • packages/client/ui-trajectory/src/client/trajectory-*-definition.ts —— 纯函数折叠定义(match/update/finalNode)
  • packages/client/ui-trajectory/src/client/trajectory-search-index.ts —— 浏览器内增量搜索索引
  • apps/web/tests/trajectory-virtualization.e2e.ts —— 虚拟化行窗口约定
  • .agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.md —— 不扁平化、保留边界的决策

3.4 mini 对照

上游 mini 现状 规划
折叠引擎(纯函数) client/trajectory.py:turn/step 聚合 message 内嵌流(V2 stream)→timing、callId 树、tool-call 节点只来自 tool/call 事件、崩溃尾部 partial 标记、纯函数(手册 10 章) turn 摘要为教学扩展(上游 trajectory-contract 无 turns 摘要)
虚拟化 / Overview / 搜索 client/trajectory.py 为终端纯函数引擎(虚拟化与搜索在浏览器前端实现) 浏览器前端 webui/ 提供:Trajectory 虚拟化窗口 + Overview 折叠跳转 + 全文 search(webui/src/trajectory/{model,search}.ts,纯前端零约定改动);client/trajectory.py 保持终端渲染不变

Mini trace(议题 3)

client/trajectory.py → tests/test_trajectory.py;Web 消费 session/follow + session/page(wire §4.1、§3)。章 10。

议题 4:Agent 干预面

4.1 产品体验

宿主(web UI、hooks 桥、ACP 客户端、编排程序)通过 Agent handle 干预运行中的 agent:唤醒(下一条消息)、转向(打断当前思路改道)、注入(静默加料)、取消(清空排队 + 中止)、等待静默(整机 quiescence)、维护任务(非回合的后台活)。干预全部经会话日志可查——inbox 每次变更先落 agent/inbox/spliced,取消留下 turn/end {kind:'aborted'}。

4.2 实现:Agent 接口与干预通道

所有输入先进双列 inbox:followup 进 next-turn(下一回合唤醒),steer/inject 进 next-step(下一 step 边界消费)。cancel 清排队并协作式中止;pre-step / request / pre-execute 瀑布在 driver 路径上叠加策略。

flowchart TD
 Host["宿主 followup · steer · inject · cancel"]
 Inbox["inbox&lt;br/&gt;next-turn | next-step"]
 Pre["agent/pre-step waterfall"]
 Loop["driver turn/step"]
 Log["Session 日志&lt;br/&gt;inbox/spliced · turn/*"]

 Host --> Inbox --> Loop
 Pre --> Loop
 Loop --> Log

Agent 接口定义于 packages/core/agent/src/runtime-types.ts:

方法 语义 日志体现
followup(messages) 下一 turn 唤醒:消息入 inbox,agent idle 时开 turn inbox 事件 + turn/start
steer(steer) 下一 step 唤醒:idle 时同步开 turn;running 时下个 step 边界消费(可转向) inbox 事件 + 下个 step
inject(messages) 非唤醒注入:running 中入 inbox,不触发新 turn inbox 事件
send(msg, target, wakeup) 统一原语:wakeup 决定 followup 还是 inject inbox 事件
cancel(cause, opts) 清 inbox(除非 keepInbox)+ abort 活跃活动;idle no-op inbox/spliced + turn/end aborted
whenIdle 整机 quiescence 信号:无活跃 driver 或 maintenance 才 resolve —
runMaintenance(task) true idle 下执行非回合维护(如 compactNow、goal 驱动的维护路径) —

干预通道(waterfall 系列)

  • pre-step:agent/pre-step 瀑布,决策 {kind:'reject'}|{kind:'enter', messages}(调用点 core/agent-loop/src/agent.ts);reject → turn/end {kind:'blocked'}(agent.ts)。真实拒绝者:hooks-codex、hooks-claude-code(把 pre-tool/step 决策映射进来)、goal-round-driver(校验 goal 消息 reservation,见议题 8);
  • request:agent/request 瀑布可在请求派生前改配置(含 compaction 的 request-error 恢复,见议题 9);
  • approval:tools/pre-execute 瀑布返回 {kind:'ask'} 走审批(见议题 5);
  • inbox 日志语义:被拒绝的 claimed 消息"既不丢弃也不重发";取消时未派发的 tool call 补 ABORTED_BEFORE_DISPATCH 错误结果对。

生命周期

对外暴露 idle | running 两态(内部另有 maintenance);running 是"驱动级排干区间"而非 turn 是否开着;quiescence = 无活跃 driver 且无 maintenance。ctx.agents 提供注册面:register/get/isOwnedBy/list/roots/create/resume/setFactory。

4.3 源码证据

  • packages/core/agent/src/runtime-types.ts —— Agent 接口全表
  • packages/core/agent-loop/src/agent.ts, 267-269 —— pre-step 瀑布与 blocked 落日志
  • packages/hooks/hooks-codex/src/index.ts 与 packages/hooks/hooks-claude-code/src/index.ts —— 真实 pre-step 拒绝者
  • packages/goal/goal-round-driver/src/index.ts —— goal reservation 校验(enter 非 reject)

4.4 mini 对照

mini loop(core/agent_loop/agent.py)覆盖完整干预面:followup + inbox + pre-step blocked + finally 闭合,以及 steer(下一 step 唤醒)/ inject(非唤醒入 inbox)/ cancel(清 inbox + aborted 闭合边界生效)/ when_idle(quiescence)/ run_maintenance(仅 true idle)(手册 09 章)。

Mini trace(议题 4)

tests/test_loop_intervention.py · session/control Remote(wire §4.2)· 章 09。

议题 5:一次性审批体验

5.1 产品体验

当模型调用需要人类点头的操作(危险命令、写文件、删除等)时,web 面板弹出 Reject / Allow once 两个按钮(packages/client/ui-approval/src/client/ApprovalPanel.tsx)。没有"永远允许":每次授权只作用于被询问的那一个动作,下一次调用必须重新 ask。无人值守时可用 never 策略确定性拒绝,或由 ACP 机器应答(只给一次性 allow-once/reject-once 两个选项)。

词汇澄清

现仓库词汇是 ApprovalOutcome / ApprovalPolicy / PreToolDecision,不存在 PermissionStatus、approvalContext 等旧符号;且"allowed-once 豁免下一次"不存在——packages/interaction/user-approval/README.md 已知限制明示无 allow-always、无记忆规则、无撤销、无授权存储(README.md)。

5.2 实现

工具管线在 tools/pre-execute 返回 {kind:'ask'} 时进入审批:ctx.approval.request 先写 log-only 的 approval/asked,再经 approval/request waterfall 收集 answerer(web / ACP / hooks),写 approval/decided,最后映射为 allow 或 deny。唯一放行是 allowed-once;never 策略在进 waterfall 前即拒绝。

flowchart LR
 Pre["tools/pre-execute&lt;br/&gt;kind ask"]
 Svc["ctx.approval.request"]
 Asked["approval/asked"]
 WF["approval/request waterfall"]
 Dec["approval/decided"]
 Pipe["工具管线 allow/deny"]

 Pre --> Svc --> Asked --> WF --> Dec --> Pipe
  • 闭合四值:'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'(packages/interaction/user-approval/src/types.ts),唯一放行是 allowed-once;
  • 两档策略:ApprovalPolicy = 'ask' | 'never'(index.ts);never 在进入 waterfall 分发之前确定性返回 rejected(index.ts)——因为 prepend:true 的监听器无法绕过 service 自身先裁决;有效策略 = 日志中最后一个 approval/policy 事件的纯 fold(index.ts),恢复无需 catch-up,setApprovalPolicy 是唯一写路径;
  • 审计不进模型转录:三个事件(asked/decided/policy)都 log-only、无 surfaceOp、无 role;模型只看到提问方消费者最终的工具结果。策略另经两条模型可见路径:system-prompt runtime-context 快照(approval:policy,order 115,index.ts)+ 策略切换时注入带 source:{kind:'plugin'} 的 user/message(index.ts);
  • 四个 answerer 通道:web 人工(web/approvals.py 挂 async tools/ask 闸门 → approval/request 经 $events 远程事件瀑布投递 → 浏览器 $events/result 回投结算,wire 约定同 packages/api/remotes,api-proxy.ts 旧路径已随 apiproxy 删除);ACP 机器(只给 allow-once/reject-once,acp/src/index.ts);hooks 桥(把 PreToolUse 的 ask 透传,hooks-claude-code/src/index.ts);无内置 answerer(headless 组合 resolve unavailable 并 fail-closed);
  • 权限旋钮在部署层:packages/bundle/base/cordis.patch.yml:188-205 声明 approval 行(policy 由 DSH_PERMISSION_MODE 推导)+ permission-presets 三档表(read-only / workspace-write / danger-full-access,各自捆绑 sandbox 模式 + 审批策略,schema 在 packages/interaction/permission-presets/src/index.ts)。

5.3 源码证据

  • packages/interaction/user-approval/README.md,59-62 —— 一次性 seam 总述;仅 open turn 有效;无内置 answerer
  • packages/interaction/user-approval/src/index.ts,94,112-118,257-344 —— 事件声明、策略、fold、request/decide 全流程
  • packages/core/tools/src/index.ts,1475-1481,1689-1729 —— PreToolDecision 与 serviceAsk 映射
  • packages/api/remotes/src/{remote-events,types}.ts —— web 通道 waterfall 与应答 payload 约束(早期 apiproxy 时代的 api-proxy.ts / api/approvals.ts 已随 apiproxy 删除,职责移入 api/gateway + api/remotes)
  • packages/acp/acp/src/index.ts —— ACP 一次性机器选项
  • packages/bundle/base/cordis.patch.yml:188-205 —— 部署级 permission 配置
  • docs/subsystems/approval.md,33,86-88 —— fail-closed 语义与审计 log-only

5.4 mini 对照

mini 的审批层:ask/never 两档、'never' 在派发前由服务自身确定性拒绝、审计事件 approval/asked|decided turn-enclosed 且 log-only 非 surface、approval/policy 最后一条胜出纯 fold、无 answerer/抛错/非词汇表返回值归一化 'unavailable' fail closed、'allowed-once' 无跨调用豁免(手册 09 章)。web 人工通道(web/approvals.py)接在工具管线闸门 tools/ask 而非上游的 approval/request(教学简化),wire 约定一致——approval/request 经 $events 远程事件瀑布投递、浏览器 $events/result 回投结算(result∈APPROVAL_OUTCOMES,非法值 fail-closed 'unavailable')、网关 dispose 全 pending 'cancelled'(手册 07 章 §7.5.4)。

Mini trace(议题 5)

interaction/approval.py · tests/test_approval.py · T4($events/result)。

议题 6:运行时自我修改

6.1 产品体验

在创造模式下,agent 可以运行时给自己加新插件:先 inspect 运行时(列表/查询/自身),define 一个新包(kind new),run 激活,stop/undefine 回收。示例 demo(scripts/demo-cordis.mjs + examples/web-cordis/cordis.yml,端口 3081)走完全程:define pkg-1 → run → invoke('double') 返回 42。

关键事实(修正预期)

原 packages/self-modification/ 已改名为 packages/extensions/。临时插件只存进程内存,明确不写 cordis.yml、不写入磁盘、重启不恢复(tool-cordis/README.md)——"agent 直接改配置"这条路径是设计上排除的。

6.2 实现

  • 七工具两族(tool-cordis):inspect_list / inspect_query / inspect_self(检查)+ define / run / stop / undefine(修改);
  • 完整流程(cordis-host-runner/tests/runner.spec.ts):define(kind new,mint dyn-1/pkg-1)→ run(立即 status:'running'、run-1、ctx.provide('dynDoubler') 生效)→ invoke 返回 42;
  • 带浏览器半的包:走 cordis/request-run 审批往返(拒绝则不启动,失败回滚);
  • 失败诊断路径:run 失败 → inspect_self 读诊断 → define 新包 → run mode:"update" 热更新;
  • 异步结果回喂:经 agent.steer 送回 agent(cordis-host-runner/src/index.ts)。

6.3 源码证据

  • packages/extensions/tool-cordis/README.md —— 进程内存、不写 cordis.yml 的明示
  • packages/extensions/cordis-host-runner/tests/runner.spec.ts —— 端到端示例全流程
  • packages/extensions/cordis-host-runner/src/index.ts —— inspect_self 诊断与 steer 回喂
  • .agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md —— 改名记录

6.4 mini 对照

mini 的动态插件(extensions/dynamic.py):进程内存 define/run/stop/undefine 生命周期,define 仅登记、run 才生效、运行中 run = retract 旧 run 后 replace、undefine 运行中自动 retract、进程级冲突 fail loud、重启不恢复(手册 11 章)。

Mini trace(议题 6)

python -m unittest tests.test_dynamic -v · 章 11。

议题 7:会话恢复 / resume

7.1 产品体验

web 中打开历史会话有两种不同深度:读历史窗口(滚动回看,不激活 agent)与真正恢复运行(resume,让 agent 继续干活)。侧边栏搜索会话按标题/工作区名 +(可选)全文内容。headless 不支持 --session 恢复——每次都是全新随机会话的一次性任务。

7.2 实现

读历史与 resume 是两条路径:session/page 只拉日志窗口,不激活 agent;ensureSession + agents.resume 才重建运行中的 agent。分页按 append-origin 消息边界切页,不在消息中段截断。

flowchart TB
 subgraph Read ["读历史(不 resume)"]
 Page["session/page&lt;br/&gt;beforeSeq · maxMessages"]
 Follow["session/follow 实时帧"]
 end
 subgraph Run ["恢复运行"]
 Ens["ensureSession"]
 Res["agents.resume&lt;br/&gt;prepare + publish"]
 end
 Log["持久化 Session 日志"]
 Log --> Page
 Log --> Follow
 Log --> Ens --> Res
会话检索与恢复:
sidebar 搜索 → ctx.sessions.search(runtime manager.ts)→ RPC session/search
 · listVisibleSessionSummaries 划定授权可见集
 · sessionQuery.searchSessions:事件过滤 [user/message, assistant/message] × surface current
 · 分页游标消费(≤ SESSION_SEARCH_PROVIDER_CALL_LIMIT 次),逐 hit 校验可见性
 · snippet 240 codepoints,≤20 条 + hasMore(api/session-search.ts,5)

打开窗口(读历史,不激活):
 ui-workspace open → ctx.sessions.open → RPC session/page(尾页 PAGE_MESSAGES=50)
 → host:historySourceFor(attached 优先,否则持久化 inspect,session-controller/src/index.ts,aliased host/history-source)
 → paginate:消息边界切页 → session/page(throughSeq/beforeSeq/maxMessages,gateway 映射);客户端与 mux 实时帧按 seq 缝合;subscribedLastSeq>tailSeq 补拉(gap repair)
 → loadOlder(beforeSeq=窗口首 seq)

真正恢复(resume,激活运行):
 ensureSession:持久化记录存在 → ctx.agents.resume({resumeSessionId})(session-controller/src/index.ts ensureSession 等效)
 → agent-loop:persistence.prepare → setupAndPublish('resume')(agent-loop/src/index.ts)
 声明式路径:config agents[].resumeSessionId(与 sessionId 互斥,index.ts)
  • 分页语义(session-controller/src/index.ts session/page,原 api-proxy.ts 已随 apiproxy 删除):beforeSeq 缺省=尾页;从尾部倒着数 maxMessages 个 append-origin 消息(user/assistant message 且 isAppendSurfaceEvent);replacement 拷贝不占配额;派生事件经 sourceEventSeqs 归并到所属消息(如 tool/result 引其 tool/call;V2 起 assistant 消息内嵌流自带边界、禁带 sourceEventSeqs),绝不在消息中段切页;compaction/summary 与其 replacement 同页;
  • 全文检索是 opt-in:session-query 家族(服务定义 + SQLite FTS5 实现,packages/session-query/)索引六类事件(user/message、assistant/message、tool/call name+arguments、tool/result content+error、todo/write、turn/end reason);结构性事件不产生文档(extraction.ts);出厂 base bundle 默认 openAt: never(SQLite 永不打开,搜索抛 SESSION_QUERY_SEARCH_DISABLED),此时 sidebar 退化为本地子串匹配(bundle/base/cordis.patch.yml:109-121);
  • 读历史从不 resume 或发布 Agent(packages/api/session-controller/src/index.ts)——两条路径刻意分离;
  • headless 无 --session:startup.ts 只解析 task 位置参数;headless.spec.ts 的 resume 桩直接 reject 证明从不调用。

7.3 源码证据

  • packages/api/session-controller/src/index.ts(session/page / session/search / session/fork / resume 约定;原 api-proxy.ts/1533-1538/1617-1662/2036-2165 已随 apiproxy 删除)—— 分页、historySourceFor、ensureSession、session/search
  • packages/api/session-controller/src/types.ts —— page 游标(throughSeq/beforeSeq/maxMessages)+ SessionErrorDetailsMap 错误闭集
  • packages/session-query/session-query/src/extraction.ts —— 索引文档投影六类事件
  • packages/session-query/session-query-sqlite/README.md,40-42,55 —— FTS5、openAt 三态、进程内同步执行
  • packages/core/agent-loop/src/index.ts —— resume 的 prepare/setupAndPublish
  • packages/bundle/headless/src/startup.ts 与 tests/headless.spec.ts —— headless 不支持恢复

7.4 mini 对照

mini 持久化层采用 JSONL + fail-closed + interrupted 修复,且 sessions 子命令(cli/session_cmds.py)提供 CLI 版会话管理(list / resume / delete,教学扩展)。web 表层:webui/ + session/list|create|page|search 等 unary 与 session/follow 流(分页边界同议题 7.2);ensureSession + agents.resume 走同一 Remote 约定。登记简化:跨会话 FTS 检索(上游 session-query + openAt 三态)在 mini 为可选域(session_query/,默认组合未必打开 SQLite FTS);sidebar 全文搜索行为与上游出厂 base bundle 默认可能不一致。headless 保持不支持恢复(同上游 startup.ts 只解析 task 位置参数)。

Mini trace(议题 7)

T2 持久化 · session/page / session/follow · cli/session_cmds.py · tests/test_session_store.py。

议题 8:plan mode 与 goal

8.1 产品体验

plan mode:/plan 进入"先规划后动手"的软引导模式——模型先产出 markdown 计划,经人确认(Approve / Keep planning)后 exit_plan_mode 退出。它不锁任何工具,只是把一段指导文本加进每轮请求的 system prompt。goal:给 agent 立一个带验收标准的多轮目标,系统自动把目标变成一轮一轮的"goal round"消息喂回 agent,直到完成/暂停/超轮次。

8.2 实现

plan 与 goal 都只靠会话日志恢复:plan 折叠 plan/mode 决定是否注入 plan:policy;goal 折叠 goal/change 得到快照,round 驱动器在 agent idle 且目标 active+armed 时经 followup 排入下一轮 goal 消息,pre-step 再做 reservation 校验(失败 → turn/end {kind:'blocked'})。

plan mode(packages/plan/plan-mode/,ctx.planMode)

  • 状态只写日志:唯一持久事实是会话事件 plan/mode {active:boolean}(log-only、非 surface、整值替换,index.ts);生效状态 = foldPlanMode 对日志前缀的纯折叠(index.ts),resume/fork/compaction 都能恢复,无 live mirror;
  • 软指导:激活时 plan:policy prompt section(order 50,index.ts)把部署配置的指导文本加入每个 model request;sandbox 模式与审批策略是独立强制限制,不读写 plan 状态;
  • 工具目录跨模式不变:exit_plan_mode 在非激活时也保持注册(index.ts),进出 plan 只改 prompt 不改工具目录——请求缓存稳定(standard/agent.cordis.yml:118 注释原文);
  • 写入路径:set 时若 agent 空闲(无 open turn)立即 append;turn 打开中只记录 pending intent,等下一次被接受的 in-turn pre-step 追加(index.ts)——与 step 边界严格一致;
  • 退出审查:exit_plan_mode 要求完整 markdown 计划,经 user-questions 弹"Plan review"(Approve / Keep planning);批准 → pending 退出在下一 accepted pre-step 写入日志;Keep planning 是带用户反馈的失败调用(index.ts)。

goal(packages/goal/:goal + goal-round-driver + tool-goal + command-goal)

  • 事件:durable 事件只有 goal/change(全快照或 clear 墓碑,version 1,domain.ts)+ 进程内通知 goal/changed;不存在 goal/claim / goal/round 事件——round 只是消息 source 字段与快照计数(澄清);
  • goal round:驱动器把"active 且 armed 且未超限"的 goal 变成顺序轮次——每条是 goal 来源的 user 消息(source:{kind:'goal', goalId, revision, round}),经 agent.followup 进 inbox,成为普通 FIFO turn(goal-round-driver/src/index.ts);
  • pre-step 消费:goal 消息走 enter 而非 reject;驱动器在 proposed messages 中认出 goal 来源,下游之前/之后各校验一次完整 reservation(active/armed、revision、roundroundsStarted+1、phaseclaimed),校验失败才 reject(或下游 reject 时标记 blocked,code prompt-rejected)(index.ts);
  • 容量与并发:轮次上限 maxGoalRounds 超限自动 block(code round-limit,index.ts);按 agent 串行化——同一时刻最多一个在飞 round(index.ts);resume/fork 后需人授权重新 armed;预留前先 ctx.sessions.flush 检查点保证 goal/changed 持久义务(index.ts)。

图 8:goal round 驱动(快照 → followup → pre-step reservation)

flowchart TD
 Mut["goal/change 写入日志&lt;br/&gt;goal/changed 通知"]
 Fold["fold 快照&lt;br/&gt;active · armed · round 预算"]
 Idle["agent idle"]
 Drv["goal-round-driver&lt;br/&gt;至多一个在飞 round"]
 Fu["followup&lt;br/&gt;source: goal"]
 Pre["agent/pre-step&lt;br/&gt;reservation 校验"]
 Turn["普通 FIFO turn"]
 Block["turn/end blocked&lt;br/&gt;round-limit · prompt-rejected"]

 Mut --> Fold
 Fold --> Idle
 Idle --> Drv --> Fu --> Pre
 Pre -->|enter| Turn
 Pre -->|reject| Block
 Turn --> Idle

8.3 源码证据

  • packages/plan/plan-mode/src/index.ts,63-67,129-138,205-233,305-393,425-460 —— plan 全链路
  • apps/cli/config/agent-presets/standard/agent.cordis.yml:104-124 —— entry-local realm 与部署指导文本(含 118 行缓存稳定注释)
  • docs/subsystems/plan.md,9-11,33 —— 软指导与 log-only 语义
  • packages/goal/goal-round-driver/src/index.ts,208-241,334-414 —— round 驱动与瀑布消费
  • packages/goal/goal/src/domain.ts,66 与 fold.ts —— goal/change 事件与严格重放校验
  • packages/goal/tool-goal/README.md,49 —— 模型侧工具与权威要求
  • docs/subsystems/goal.md,100-111 —— 快照字段与 GoalMessageSource

8.4 mini 对照

wire 与折叠语义与上游一致:plan/mode、goal/change、goal 消息 source.kind==='goal'、pre-step reservation 与图 8 主链。实现细节与登记简化不在此展开,见映射表与手册。

域 mini 深读
plan mode + 审查 miniharness/plan/、interaction/user_questions.py architecture.md plan/* 行;演示 examples/plan_goal_demo.py
goal 服务 + 驱动 + 工具 miniharness/goal/ 手册 09 §9.2(干预 + goal round 消费);tests/test_goal*.py
命令 miniharness/commands/、/plan、/goal 手册 11(动态扩展口旁及 plan 装配序)

登记简化(须在映射表核对):plan scope 层叠未复现;goal 无 Typert remote、无 competingQueued、completionAuthority 近似为「当前 step 在 goal 轮次中」;工具 canonical 值与 Tool.render 分离(plan/goal 三工具同上游 JSON 契约)。

Mini trace(议题 8)

examples/plan_goal_demo.py --approve · tests/test_goal*.py · plan/mode / goal/change 事件 · wire goals/*(interface-wire §4.12)。

议题 9:上下文压缩与后台作业

9.1 产品体验

长会话自动压缩:上下文压力超过阈值时自动把旧消息折叠成摘要并替换(surface 上留一个检查点消息),前缀重放复用 provider 的 KV cache;provider 报上下文超限时强制减容后重试。后台作业:bash/subagent 可 run_in_background: true,返回 job id,模型用 job_output/job_list/job_kill 收集/管理。可继续子代理:子代理以 durable 子会话运行,父 agent 可随时继续喂消息,进程重启后冷恢复。

9.2 实现

压缩在 agent/pre-step 看 token 压力,在 agent/request-error 处理 overflow;一次成功压缩是 start → 摘要 LLM 调用 → 带 surfaceOp replace 的检查点 user/message → end 的事务,失败也要补带 error 的 end。

flowchart LR
 Meter["ctx.tokenMeter fold"]
 Pre["agent/pre-step 压力检查"]
 Sum["compaction 摘要 LLM"]
 CP["user/message 检查点&lt;br/&gt;surfaceOp replace"]
 End["compaction/end"]

 Meter --> Pre --> Sum --> CP --> End

compaction(packages/compaction/:seam + compaction-basic + pruner + command-compact)

  • 可选能力:不在 agent-loop 脊柱上;ctx.tokenMeter 独立服务(docs/subsystems/compaction.md,69);
  • 触发:thresholdRatio 默认 0.8(floor(contextWindow × ratio) → thresholdTokens,config.ts,144-154),tokenMeter 请求+响应压力 ≥ 阈值触发;retainRatio 默认 0.16 为保留尾部预算;context-overflow(provider 报 CONTEXT_WINDOW_EXCEEDED)绕过阈值强制减容(index.ts);
  • 事件:compaction/start(先写入作为锁)、compaction/summary、compaction/end(最后释放,崩溃留下可检测孤儿锁)+ 可选 compaction/prune;不存在 compaction-request / compaction/compress 名称(澄清);三个事件 log-only;surface 实际变更是带 surfaceOp:{op:'replace',start,end} 的 user/message 检查点(事务 id 关联);
  • KV 复用:summarization 是一次直接 ctx.llm.stream(purpose:'compaction'),逐字重放会话自己的 system prompt、工具、被遮蔽区消息,最后追加固定压缩指令——复用 provider warm prefix cache(summarizer.ts,161-176);
  • 衔接:压力检查挂在串行 agent/pre-step(request 派生前);overflow 恢复挂在 agent/request-error,仅当 surface.replaceGeneration 前进才返回 retry(index.ts);手动路径 compactNow 闭合后 ctx.sessions.flush 放行后续 prompt(region.ts)。

tokenMeter(packages/llm/token-meter/)

  • 无配置、自身不产生任何会话事件:对既有事件流做增量 fold(logRevision = consumedEvents),提供请求压力与表面定价两个快照(index.ts,116-147);
  • 用量来源:assistant/message.usage + assistant/message/assistant/attempt 内嵌流展开出的 usage 样本(同一步早样本+最终样本,后者替换前者不重复计数,usage-projection.ts,116——每次 v2 Assistant 结算贡献其流内嵌的最后一个 usage 样本);
  • 锚定/估算双路径:最新成功请求 usage 在其 canonical 信封与当前 request/header 一致且总数 ≥ 启发式锚价时复用(baseline.kind='usage'),否则估算(4 字符/token 等固定启发式,estimate.ts)。

后台作业(packages/jobs/:seam + jobs-local + tool-jobs)

  • 工具:job_output(job_id, wait?, timeout_ms?)(默认非阻塞读流式增量,wait 有界)、job_list、job_kill(job_id, reason?);canonical 值 {text,job} / PublicJobSnapshot[] / {outcome:'cancellation-requested'|'already-finished', job};
  • 生命周期事件:注册表在 owner 会话追加 log-only job/registered/job/settled(JobRegisteredData/JobSettledData:jobId/owner/kind/label/startedAt/toolCall? + finishedAt/status/cause/producerReported/detail?,jobs/jobs/src/types.ts;发射失败只 warn,index.ts);完成 notice 仍走进程内 events.subscribe 监听器(busy 注入 / idle 唤醒,maxConsecutiveWakes 默认 3,tool-jobs/README.md);
  • 发号:<kind>-<16 位 UUID hex>(randomUUID.replaceAll('-','').slice(0,16),同注册表持有旧 id 时重试,index.ts);JobSpec.toolCall? 记录创建它的工具调用(callId/rootCallId),由各 producer(tool-bash/tool-pwsh/tool-terminal/tool-workflow)从 ToolExecution 透传;
  • 并发:maxConcurrentJobsPerOwner 默认 10(按精确 Agent 实例计 running+stopping,jobs-local/src/index.ts,144-146);触发入口是 bash / pwsh / terminal(pty-send) / workflow 的 run_in_background: true(subagent 已不再是 job kind,JobKindMap 仅 bash,view.ts);
  • 注册表在 host plane,standard preset 只挂模型侧 tool-jobs(standard/agent.cordis.yml:64-74)。

可继续子代理(packages/subagent/)

  • durable 子 Session + 至多一个进程内 Activation;Activation 不是 request/result:可执行多个 FIFO turn,后代仍运行期间保持驻留(subagent/continuation.ts,154-159);
  • durable 事件 subagent/descriptor(log-only、跨压缩保留)+ 进程内 observe-only subagent/start/subagent/end(每个 Activation 驻留期一对,cold resume 是新 epoch 新 runId);inbox 生命周期沿用 agent/inbox 事件;
  • followup 路由:running → 同 Activation 入队;waiting → 唤醒;无 Activation → coldResume(persistence.inspect → authorizeLineage → 折叠 descriptor → ctx.agents.resume,不经过 provider,continuation.ts);
  • standard preset 的 spawn/fork 工具 backgroundMode: continuable;结束经 subagent-settled settlement notice 送达(与子代理自发 subagent-report 区分)。

9.3 源码证据

  • packages/compaction/compaction-basic/src/config.ts,144-154 —— 0.8/0.16 阈值
  • packages/compaction/compaction-basic/src/region.ts —— start→summary→replace→end 事务与 flush
  • packages/compaction/compaction-basic/src/summarizer.ts,161-176 —— 前缀重放与 KV 复用
  • packages/compaction/compaction-basic/src/index.ts,368-420 —— pre-step 压力检查 / request-error 恢复 / compactNow
  • packages/llm/token-meter/src/index.ts,116-147,221-261 —— 增量 fold 与 usage 折入锚
  • packages/jobs/tool-jobs/README.md,19-27 与 docs/subsystems/jobs.md,180-285 —— 工具语义与 ctx.jobs 约定
  • packages/jobs/jobs-local/src/index.ts,144-146 —— 并发上限 10
  • packages/subagent/subagent/src/continuation.ts,476-505,883-932,966-1076 —— startContinuable / followup 路由 / coldResume / 物化
  • packages/core/agent/src/consumed-work.ts + packages/subagent/subagent/src/lifecycle.ts —— foldConsumedWork 记账与 epochStopReason 终局折叠
  • apps/cli/config/agent-presets/standard/agent.cordis.yml:64-74,128-155,186-198 —— jobs/compaction/subagent 组合位点

9.4 mini 对照

mini 的 token 计量与压缩实现如下(llm/token_meter.py + compaction/,装配在 demo/headless/ACP/SDK 入口):

  • TokenMeter:增量 fold + usage 折入锚(estimateHeader 按 system/tools 启发式定价且 config 不计价);
  • BasicCompactionEngine:pre-step 压力检查(阈值取 adapter.contextWindow,缺省返回 None 而非抛 TargetPressureConfigError),request-error overflow 减容(仅 surface.replaceGeneration 前进才 retry,上限 maxOverflowRetries,成功/回合结束边界惰性复位);
  • 事务:compaction/start → 前缀重放摘要 → user/message 检查点(surfaceOp replace + sourceEventSeqs)→ compaction/end,任何失败恰好补一次带 error 的 compaction/end;
  • 简化标注:摘要前缀重放无 KV cache 语义;崩溃孤儿锁检出即 busy 拒绝(同上游 assertCompactionInactive,上游同样无自动恢复);
  • toolResultPruner 为可选阶段(compaction/tool_result_pruner.py);_log_result 经 ctx.logger 路由,同上游。

mini 的后台作业(jobs/,进程内注册表 + 三工具 + 完成 notice,装配在 demo/headless/session_cmds/ACP/SDK 入口):

  • ctx.jobs 服务(LocalJobRegistry):start/list/get/read/kill/wait、onJobDone/onJobsChanged、attachController;id 为 <kind>-<16 位 UUID hex>(碰撞重试);owned 作业按会话 id 栅栏、unowned 对任何调用方开放;结算 first-wins,waiters/kill/终态 read 置 reported 抑制 notice;maxConcurrentJobsPerOwner 默认 10(按精确 owner / unowned 桶计 running+stopping);owner 销毁时 cancel 在飞作业 + 限时排干 + 删除(teardown cancel 抛错 force-fail 只改记录);注册/结算在 owner 会话落 job/registered/job/settled(含创建它的 toolCall 与 producerReported);
  • 模型侧三工具:job_output(默认非阻塞读流式增量 / wait 有界、响应以 [status: ...] 结尾)、job_list(<id> [<kind>] <status> — <label>)、job_kill(requested / already-finished);
  • 完成 notice:按 completionDelivery 走 wakeup(idle owner 开 turn,预算 maxConsecutiveWakes=3,user 输入认领恢复)或 quiet(一律 inject);
  • 输出封顶:输出与 notice 按 outputLimitBytes 做 UTF-8 字节封顶,模型可见输出再经工具层 finalizeContent 兜底二次封顶(job_output/job_kill 保 [status: ...] 行,[output truncated] / [result truncated] 截断标记同上游 finalizeTaskContent);
  • 与上游一致:注册表在 owner 会话落 log-only job/registered/job/settled(JobRegisteredData/JobSettledData,含可选 toolCall 与 producerReported),JobSpec.toolCall 由各 producer 从 ToolExecution.callId/rootCallId 透传;
  • 简化标注与已定载体:agent registry + assertLive(core/agents.py AgentRegistry + install_agents + assert_live_agent,注册点 AgentLoop.publish,jobs _assert_access/goal _prepare_mutation 接线;controller/监听器另按 scope 分层:registry.py _layers = ScopedLayers(...) + chain_layers(scope_of(owner)),同上游作用域化分层);wake 预算载体(user 输入认领恢复改经安装 ctx 订阅 agent/inbox/claimed,仅 source.kind=='user' 恢复,同 tool-jobs spendWakes.delete(agent),+ agent/disposed 防泄漏,AgentLoop 删 on_inbox_claimed 零参钩子);teardown 排干为事件驱动(逐任务等待 settled 事件,registry.py,同上游 await Promise.all(settled),非限时轮询);canonical value + native renderer 分离(jobs/tools.py execute 返回结构化结果、render= 承担模型可见文本,同上游 output.render);finalizeContent 载体(上游经 outputLimits WeakMap 即 pre-execute prepend 捕获取上限,mini 每次现查,等价回退路径、无 policy 时行为一致);run_in_background 触发入口已经模型侧 subagent 工具复现(见下)。

mini 的可继续子代理(seams/subagent/:descriptor + continuation + 委托工具 subagent + 控制工具三件套 + report 工具,装配在 demo/headless/session_cmds 入口):

  • durable 子会话:header meta + 描述符事件先写入磁盘;冷恢复走 inspect → authorizeLineage → fold descriptor → 重建组合;
  • 双路径执行:父有 driver → 投递即返回 message id、Activation 跨回合驻留、watchSettlement 结算;无 driver → 同步 pump 门面;
  • 生命周期事件 subagent/start/subagent/end:runId 配对、SubagentRunInfo/RunEndInfo payload、逐监听器收容派发、经委托父 scope 载体的 scoped dispatch(打标监听器按载波键或其祖先接纳、未打标全局接纳,无标号父退化祖先链派发);
  • 命名 provider 注册表:register_provider 登记按模型重建适配器的入口,disposer 注销即发布 subagent/provider-removed 边且幂等;解析时注册表优先、回落缺省工厂;
  • DRAINING 准入截止:manager 级 drain 与 scoped drain_descendants(parents),精确在世根过滤、closingScopes 成员 = 根 + 后代世系、child-first 强制结算、聚合错误;assert_admitting 接入 start/send 准入边界,拒绝措辞逐字一致;
  • 终局折叠 foldConsumedWork + epochStopReason(stepped/claimed 记账、droppedUnrun→aborted);
  • 所有权记账:sendWaking/admitWaking(accepted 窗口、waiting/settled 判定顺序);
  • interrupt 授权矩阵:user/ancestor authority、stale 调用方防探针、缺席目标接受性 no-op;
  • 嵌套续跑:exec.agent 为授权与所有权主体,孙代结算通知投 durable 直属父,list_descendants 沿 parentSession 链 BFS;
  • finishDisposal 顺序:cancel top-down → flushFinalState best-effort → capture → 拆除 → notifySettlement → releaseOwnership → end 边;
  • 模型侧 subagent 工具:同 tool-subagent 约定(文案/canonical+render 逐字,canonical 恒 {kind:'activation',subagentId}、render started subagent <id>、cwd 参数、config 逐字拒绝 persona/toolFilter);
  • 简化标注:invariant 运行时校验不适用(上游 per-provider invariant 分包架构);同步模式结算投递走非唤醒 next-step。

Mini trace(议题 9)

tests/test_compaction.py · tests/test_jobs.py · command/run / command/done 轨迹 notice · schedule/* Remote(wire §4.9)· job/list 流。

议题 10:skills 能力家族——按需加载的指令

10.1 产品体验

模型侧:会话打开时,若存在模型可调用的 skill 且 skill 工具可见,agent 会先收到一条 durable 的目录消息——<system-reminder> 里的 <available_skills> 块,只列出每个 skill 的名字与一句简介(description 最长 500 字符,超长截断)。模型按名调用 skill 工具,拿到该 skill 的全文 <skill_content>(正文 + 资源指引),再照做。

用户侧:用户可以直接在输入里写 /名字(词边界匹配)。命中可用户调用的 skill 时,系统把它的全文渲染成一条 user 消息注入会话,模型直接跟随,无需再走工具。

渐进披露:目录只放摘要(省钱),正文按需加载(省事),资源(脚本/素材/URL)由指令内的相对引用按需解析。模型不会在一开始就看到所有 skill 的正文。这与 Anthropic 生态的 Claude Code Agent Skills 同构——~/.claude/skills/<name>/SKILL.md + frontmatter + 渐进披露;dsh 的差异在于:多了一层分层注册表(host + per-scope)、目录消息是事件溯源出的 durable 会话消息(resume/fork/compaction 后仍在),以及双调用表面(模型工具 vs 用户手势,可分别开关)。

10.2 实现:四包四角色

skills 家族由四个包组成,职责分得很清楚:

  • dsh-skill(Service Definition):只提供 ctx.skills 注册表与渲染原语,不实现任何具体 skill;
  • dsh-skill-filesystem(Provider):把本地目录变成 skill 源,注册进 ctx.skills;
  • dsh-tool-skill(Consumer):目录消息 + skill 工具 + /名字 手势,模型/用户面对的消费端;
  • dsh-skill-badge(Provider):一个内置的"dsh 徽章"skill,示范 source 'bundled'(rank 600)。

分层注册表(packages/skill/skill/src/index.ts)

ctx.skills 与 ctx.tools 同构:一个 global 层 + 每条 scope 链一层,按"离调用会话最近优先"合并,同名冲突由最近层胜出(index.ts)。provider 注册进调用方所在的层(host 组合挂的插件进 global,preset 挂的进 preset 层)。SkillSource 七桶:project-dsh / project-agents / runtime / user-dsh / user-agents / custom / bundled(+ 字符串扩张),skill 名字必须是 kebab-case 小写(^[a-z0-9]+(?:-[a-z0-9]+)*$,index.ts)。

三个观察接口(这是 skills 能力扩展口的核心约定):

  1. list:每个 provider 的候选摘要,期望是一个"完整发现"的数组,而不是增量补丁;
  2. snapshot:{skills, complete},complete 为 true 才缓存复用;collect 期间 revision 变动且重试一次(MAX_COLLECT_ATTEMPTS=2)仍不稳定 → 返回 incomplete 且不缓存(index.ts);
  3. get(name):全文,不缓存正文(每次重新从 provider 取),所以正文变更无需版本号/hash/失效通知。

provider 返回的候选必须通过严格校验(name 合法、description 是字符串等),非法候选直接抛错 fail fast(index.ts),而不是静默过滤。get 若返回的定义名与候选名不符,会精确失效该 provider 并拒绝该条(invalidateEntry,index.ts)。

invocation 策略(双布尔)

每个 skill 携带 invocation = {modelInvocable, userInvocable}(index.ts),四态齐全:两 false = 禁用;只 model = 模型工具可调、用户手势不可;只 user = 手势可调、目录与工具隐去(disable-model-invocation 的 skill 唯一入口);两 true = 双表面。get 是策略中立的加载原语,由消费方自行判谓词——所以工具与手势对同一个 skill 的可见性可以不同。

渲染(renderSkillContent)

<skill_content name="<escaped-name>">
<skill_resources>
<resource-guidance>
</skill_resources>

<skill_instructions>
<provider-owned-instruction-body>
</skill_instructions>
</skill_content>

XML 转义(escapeText/escapeAttr,index.ts),资源指引按 provider 类型四种形态:managed("Resources ... managed by provider")、directory("Base directory for this skill: ")、URL、opaque。

文件系统 provider(packages/skill/skill-filesystem/src/index.ts)

  • 六个 rank 源(README 表格):project-dsh .dsh/skills(rank 100)、project-agents .agents/skills(200)、custom 配置目录(300)、user-dsh $DSH_HOME/skills(400)、user-agents $DSH_AGENTS_HOME/skills(500)、bundled $DSH_BUNDLED_SKILL_DIR(600);
  • 两种载体:目录 bundle <name>/SKILL.md,或扁平 <name>.md;刻意排除嵌套 **/SKILL.md(只挖一层);
  • frontmatter(YAML):必填 name / description,可选 whenToUse / metadata / disable-model-invocation / user-invocable;布尔字段接受 true/false/yes/no/on/off/1/0;camelCase 旧键或非法布尔 → 整条 skill 丢弃(fail-closed)+ warn(README:57),而不是丢弃该字段退回宽松默认;
  • 主体与目录分离生命周期:目录由发现时解析 frontmatter 得到;get 每次重读文件取正文,正文改动无需 hash/缓存失效;
  • watch:chokidar 观察直接成员增减与 SKILL.md 变化,references/scripts/assets 等资源目录变化不触发失效;缺失根目录用 fs.watchFile 逐段探测;首访工具 write/edit 会通过 fs/observed 同步失效 provider(index.ts);
  • ctx.fs 优先:有 fs 服务时 listDir/readText/.git 探测全走 ctx.fs(沙箱 / 远程实现),无则回退 Node fs(README:43)。

消费端(packages/skill/tool-skill/src/index.ts)

  • pre-step 目录:每个 eligible pre-step 调 ctx.skills.snapshot(index.ts);首次非空且 skill 工具可见 → 向 enter 决策注入 durable user 消息(<system-reminder> 目录模板,index.ts);
  • digest 判变:目录消息带 skill-catalog source(catalog form,{entries, update?}),entries 的 sha256 作为 digest 基准(index.ts);成员/描述/可见性变化 → append 一条完整替换目录;删光 → 空目录显式退役旧名字(tombstone);
  • incomplete 不发:snapshot 不完整 → 本 pre-step 不发,保留 last-good 目录(index.ts);
  • 工具可见性参与 digest:skill 工具被限制/被同名 scoped 影子覆盖时目录整体省略——身份比较用"本插件注册的定义"而不是按名查找(README:15),保证全局挂载与 per-agent 挂载行为一致;
  • /名字 手势:SKILL_GESTURE 正则只扫 claimed user 消息(index.ts, 177-204),命中可 user 调用的 skill 则 get 全文渲染为 user instructions 注入(skill-invocation source),同一步内重复手势只注入一次;未知或 user 禁用的名字保持普通文本;
  • skill 工具:name 参数 kebab 校验;错误三态严格区分:Error: invalid skill name "<name>" / Error: skill "<name>" is unknown or no longer available / Error: skill "<name>" is not available for model invocation(index.ts)。

装配点

  • base 组合(host plane):skill(dsh-skill)+ skill-filesystem + skill-badge(disabled:true,packages/bundle/base/cordis.patch.yml:243-245)+ tool-skill(237-248);
  • standard preset 层:skill-filesystem + tool-skill(standard/agent.cordis.yml:83-87)——provider 与消费端成对进 preset 层;
  • web-app 组合:host 的 skill-filesystem 禁用,由 preset 自己拥有本地发现(packages/bundle/web-app/cordis.patch.yml:323-333)。

10.3 源码证据

  • packages/skill/skill/src/index.ts,39,48,147-234,232-271,279-330,391-454,471-546,622-646 —— 注册表、三接口、渲染、校验、失效
  • packages/skill/skill-filesystem/src/index.ts,49-89,130,182-234,241 与 README.md,33-59,69-75 —— rank 表、frontmatter、watch、已知局限
  • packages/skill/tool-skill/src/index.ts,61-69,81-160,177-251,254-277,328-337,409 与 README.md,39-63,148-168 —— 目录生命周期、工具错误三态、手势、模板
  • packages/skill/skill-badge/src/index.ts,25,58 与 assets/dsh-badge.md —— bundled 示例 provider
  • packages/bundle/base/cordis.patch.yml:237-248、packages/bundle/web-app/cordis.patch.yml:323-333、apps/cli/config/agent-presets/standard/agent.cordis.yml:83-87 —— 装配
  • docs/subsystems/skills.md 与 packages/skill/skill/README.md —— 设计意图(社区实现按 progressive disclosure,同 Anthropic Agent Skills 理念,见 deepseekdocs.com/en/docs/features/skills)

10.4 mini 对照

mini 的 skills 实现(miniharness/skills/,L2 编排层;装配在 demo/headless/ACP/SDK 入口)对应上四包如下:

  • registry.py —— SkillRegistry(ctx.skills 服务):global + scope 链分层注册、list/snapshot/get 三接口、候选校验 fail fast、incomplete 不缓存 + revision 抖动重试一次、collect 缓存按 (cwd, scope 链, revision) 键控 LRU、invalidate_cache(revision++ + 清缓存 + skills/change 事件,监听器异常容错)、get 定义名不符 → 精确失效该 provider 条目;renderSkillContent/escapeText/escapeAttr、digest 均逐字一致。register_provider 同时接受"create 工厂返回 dict 或对象"两种形态。
  • filesystem.py —— FileSystemSkillProvider:六类根(project-dsh 100 / project-agents 200 / custom 300 / user-dsh 400 / user-agents 500 / bundled 600,同根 rank 一致靠 providerOrder+localOrder 决胜)、目录 bundle <name>/SKILL.md + 扁平 <name>.md、find_project_root(向上找 .git)、user-dsh 根跳过 .system;frontmatter 经 pyyaml(硬依赖 safe_load);camelCase 旧键或非法布尔 → fail-closed 丢弃 + warn;get 每次重读文件取正文。
  • tool_skill.py —— 消费端:pre-step 目录注入(首次非空且 skill 工具可见 → durable user 消息)、digest 判变(成员/描述/可见性变化 → 完整替换目录)、空目录退役 tombstone、incomplete 不发、工具可见性参与 digest(身份比较用本插件注册的定义)、skill 工具(错误三态逐字)、/名字 手势(skill-invocation source、去重保序、未知/user 禁用名保持普通文本);手势 listener 先注册、catalog 后注册(waterfall 顺序保证目录在前、手势离答案最近)。
  • 装配:install_skills(ctx)(幂等;创建注册表 + 挂 filesystem provider + 两个 pre-step listener),register_skill_tools(reg, registry) 把 skill 工具注册进现有 ToolRegistry;demo/headless/ACP/SDK 均已接线,standard preset 的 tools 列表保留 "skills" 与实现一致。

mini 与上游的差异:

  • watch:FileSystemSkillProvider 默认开启 watchdog 文件监听(skills/watcher.py 的 SkillWatchManager,事件过滤 / 去抖 → 失效回调 + 失效条目精确失效,filesystem.py,watch=False 可关);
  • badge 为 opt-in bundled provider:skills/badge.py 实现内置 dsh-badge,需经 install_badge_skill(ctx) 显式装配,install_skills(ctx) 不自动装(skills/__init__.py);
  • 无 ctx.fs 服务适配(直接 os 读取);lookup cwd 读 session.meta['cwd'](同上游 session.header.cwd);
  • execute 直接返回渲染文本(无 canonical value + native renderer 分离,skills/tool_skill.py);工具错误前缀统一 Error:(同上游 toolErrorResult);
  • 目录注入为每个会话增加 durable 消息,与 skill 工具的可见性联动(digest 含工具可见性)一致,不存在"目录与工具不一致"的偏差。

Mini trace(议题 10)

miniharness/skills/ · tests/test_skills.py · pre-step 目录 + skill 工具 · 章 03/04 工具注册惯例。