Rename the tool-presentation transport from code-mode to ptc everywhere that is not written into session logs: the mode config value becomes 'ptc', the preset directory/id becomes ptc, the demo becomes demo:ptc, the dispatch waterfall becomes tools/ptc-dispatch-log (types PtcDispatch*), the prompt rule becomes tools:ptc-only, source/test files become ptc.ts etc., and prose says PTC mode / PTC 模式. The session-persistent vocabulary (durable events tool/code-dispatch*, logged plugin name tools-code-mode, sub-call id segment :code:) intentionally stays and moves in the stacked persistence PR, which is blocked until the SESSION_FORMAT_VERSION v0→v1 migration lands with it. run_code, its code parameter, CodeSdkLanguage, CodeRunFailedError, the dsh-code-runtime family, third-party codex names, and frozen archived notes keep their names.
5.6 KiB
Agent Note: 按 agent 的工具呈现方式,以及 ptc 预设
Status: implemented
English | 中文
问题
agent preset 已经能按会话组装一个 agent 的工具,却管不了这些工具以何种形态抵达模型。PTC mode——一个 run_code 工具加一份生成的 TypeScript SDK,用一段程序替代一串调用——此前是宿主 dsh-tools 那一行上的部署级 mode 字段。一个部署要么所有会话都跑 PTC mode,要么一个都不跑,于是那个显而易见的产品形态(预设选择器里「代码模式」与标准/极简/创造并列)无处安放。
「把 tools 下沉到 agent 平面」这个字面读法行不通。ctx.tools 有一批跟不下来的宿主平面消费者:dsh-agent-loop 读它私有的调度器 seam,dsh-apiproxy 读它的 presenter 来渲染工具卡,每个工具插件都往里注册。按本 stack 自己的规则——只有所有消费者一起下沉,服务才能下沉——注册表必须留在原地。
决策
把注册表和它的投影拆开。注册表留在宿主平面;呈现方式变成它内部按 scope 的状态,与已经住在那里的作用域限制和守卫并列。
ToolRuntime.presentAs(mode) 只接受 scoped 上下文,形状照抄 restrict():它通过 ScopedLayers.effect 在调用方 scope 的 ToolLayer 上写一个单元,因此会随声明它的那个 scope 一起卸载。在随附的 Web 界面里那个 scope 是某个 agent preset 的常驻挂载——ptc preset 携带 tool-presentation 行——因此一份声明覆盖加入该 preset 的每个 agent,而 modeFor(scope) 取作用域链上最近的那份声明。它与 config 的 mode 一并解析,后者于是成为「未作声明的 scope」的默认值,而不再是进程级事实。原先决定呈现方式的三处读取——wire schema、可见性视图里的 run_code 条目、以及生成的 SDK 段——改为读取该 scope 的模式,而非服务的。
有两个随之而来的结果,且都是承重的:
run_code按 scope 追加。 此前只要传输存在,它就进入每一个视图。按 agent 之后,一个 native agent 不能因为进程里别的 agent 呈现了它、就在自己的分发表里看到run_code——因此这次追加以该 scope 自身的模式为条件,传输也改为首次需要时才构建。- 保留名现在无条件生效。
run_code此前只在配置了 PTC 模式时才被拒绝注册。如今任何 agent 都可能选择 PTC 模式,因此一个在 native 部署下可以随便占用的名字,会在某个 preset 挂载的那一刻变成冲突。
SDK 提示词段由 PTC 模式的部署全局注册(不变),并由 presentAs 额外按 scope 注册一份,后者按名字遮蔽前者。它的正文对 native scope 渲染为空,而提示词渲染器会丢弃空段——正是这一点让「在 PTC 模式部署下选择退出」的 agent 不带 SDK 段。
preset 用一行来表达这个选择:@deepseek-ai/dsh-agent-tool-presentation,其全部内容就是一次 presentAs 调用。code 类模式通过 ctx.inject 等待 ctx.codeRuntime 而非假定它存在:运行时在宿主平面,而一个 pending 的行正是 dsh-agent-presets 已经会报告的「不可用挂载」并会指名该行——于是在无运行时的部署上选择 PTC mode 的 preset,会在操作者能够动手的地方失败。
考虑过的替代方案
在 preset 的 isolate realm 里再起一个 ToolRuntime。 否决:dsh-agent-loop 通过一个私有 symbol 从宿主上下文一次性解析注册表,因此按 agent 的注册表对调度器不可见。把 loop 改成按 agent 解析注册表,远比把一个字段变成 scope 感知的改动大。
在 preset 自己的 YAML 里加一个顶层键。 否决,理由与 preset 展示元数据落到独立 preset.yml 相同:组装是一个顶层的插件行列表,装不下并列的键。
把包命名为 dsh-tool-mode。 被一道 gate 否决,而且它是对的。gen-tool-catalog 以 packages/*/tool-* 通配,并要求每个命中项发布一个面向模型的工具 schema——因为在本仓库里这个前缀就意味着「带工具」。而这一行不带任何工具。
在构造函数里无条件注册 SDK 段。 试过之后否决:renderPrompt 会丢弃空段,但 PromptAssembly.sections 会保留它们,于是每个 native 部署都将携带一个什么也不渲染的 tools:sdk 条目,而两处既有断言不得不为此放宽。
用 include 共享 standard 的组装。 按本 stack 自己的惯例否决:cordis 已经复制了一份 standard,而 preset 的价值恰在于整份组装能在一个文件里读完。代价——第三份约 240 行、且必须同步演进的副本——是真实的,也正是未来引入 include 机制最有力的论据。
后果
同一进程内的两个会话现在可以有不同的呈现方式,因此「模型看到哪些工具」不再能只凭部署配置回答,必须给出 agent。凡是引用模式的诊断信息,现在引用的都是该 scope 的,而不是服务的。
ctx.tools.schemas(agent) 仍然是该 agent 的能力清单,不受呈现方式影响——坍缩的只是 assembly 里的工具。断言「模型收到什么」的测试必须读 assembly;web-agent-presets.spec.ts 对随附的 ptc 预设同时断言了这个区分的两侧。
随附的名单变成四个预设(标准/代码/极简/创造),因此任何列出它们的 golden 都会变动。未组装 code 运行时的部署无法组装任何 PTC 模式的 preset;随附的 Web overlay 带了一个,base 组装没有。