22 KiB
Agent Note: PTC mode——模型针对工具注册表编写 TypeScript
Status: implemented
English | 中文
问题
在注册表的原生呈现方式下,agent loop(智能体循环)将每个可见能力以 JSON Schema 函数定义的形式通告给模型。ToolRuntime 将其 schema 贡献给系统提示词组装,组装结果中的 tools 落到协议格式(wire format)上(也记录在请求头日志中),模型每步调用一个 tool-call 块,而在本 note 写作时,循环通过 ctx.tools.execute() 逐个分发每次调用(并行工具执行当时还是 open TODO;此后有界的并行分发已经交付——见并行工具调用 note,以及 docs/architecture.md 中的 rolling pool)——且每一个中间 tool-result 都会在下一次请求时重新进入模型上下文。
对于多步工具操作,这种方式 token 开销大且串行。模型无法组合工具——遍历结果集、根据中间值分支、扇出、后处理——每次调用都需要一次完整的模型往返,而每次往返都会把完整的中间结果拖回上下文,不管模型是否需要。
Cloudflare 的 Code Mode 提出了一种替代方案,基于一个简单的观察:LLM(大语言模型)编写代码的能力优于发出工具调用,因为它们见过数百万行真实代码,而人为构造的工具调用 trace 相对很少。模型不再每步发出一次工具调用,而是针对工具生成的 API 编写一段 TypeScript 程序,程序在沙箱运行时中执行,模型只筛选返回的内容——仅限它 print 或 return 的部分——而非所有中间结果。
工具呈现属于掌管工具可见性的注册表:将另一种呈现实现为事后的 waterfall(瀑布式事件)变换会依赖监听器顺序,并与可重建请求冲突。执行必须独立于呈现模式,保留全新程序状态、Host 拥有的绑定和可取消结算。
决策
三项决策,各自在下方独立小节中展开:
- PTC mode 是
ToolRuntime(dsh-tools)的一等呈现模式,通过经校验的mode配置选择:'native'(默认,贡献可见能力 schema)或'ptc'(注册表仅贡献其保留的run_code传输通道加一份生成的 SDK.d.ts到系统提示词中)。注册表在源头构建其规范贡献;协作式提示词组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。 - 代码执行是一个能力 seam——
packages/ptc-runtime/包含 Service Definition 包@deepseek-ai/dsh-ptc-runtime,拥有ctx.ptcRuntime(能力 seam;消费方 =dsh-tools,core 消费 seam 的先例见agent-loop→dsh-llm)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告{ value, logs, error? }。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个 Service Provider 包,而非重新设计。 - 交付的实现是
@deepseek-ai/dsh-ptc-runtime-node:每个程序在全新的受管 Node 进程中,按调用 Session 的已解析沙箱策略运行。直接 Node API 在该策略内仍可使用;具名 Host 绑定通过单独验证的控制通道调用。
本说明负责 PTC 呈现、组合与结算。类型化工具返回决策负责生成输出、规范绑定值、ToolCallError 与无损外层输出。沙箱 Node 决策取代 worker 执行、信任姿态与预算实现,同时保留本说明的消费方规则。
每段程序的执行控制
消费方根据运行时只读 timeout 描述符通告 timeoutMs,并把显式值交给其解析器。不支持的提供方不公开超时字段,并拒绝显式请求。沙箱提升在运行时解析和启动前使用共享的严格扩大权限审批辅助函数;仅替换已解析策略的模式,使授权只作用于一段程序。嵌套分发保留原始 Agent 和 Session,因此其审批与文件策略保持独立。失败程序不会隐式重放,因为先前效果可能已提交。
Node 执行决策 负责经过时间预算核算与延期的生命周期问题。
注册表拥有模式
ToolRuntime 获得一个经 schemastery 校验的配置(static Config),这是它的第一个配置:mode: 'native' | 'ptc',默认 'native'。部署通过 cordis.yml 翻转模式(tools: { mode: ptc }),无需改代码,遵循 no-hardcoded-tunables 约定。
协议工具列表。 注册表在 'native' 下贡献可见能力,在 'ptc' 下仅贡献 run_code。呈现方式简化决策 记录混合呈现的移除。最终的 PromptAssembly.tools 列表记录在请求头中。run_code 是一个保留的呈现传输通道,位于注册和限制层之外;直接提示词提供方和组装 waterfall 仍各自负责自己的贡献。
与 toolOrder 的交互: 如果配置的 systemPrompt.toolOrder 引用了原生能力名称,在 mode: 'ptc' 下会拒绝所有组装,因为那些名称不在该模式的协议校验范围内。这是正确行为而非 bug:使用 PTC mode 的部署需要更新其 order 配置或移除它。
SDK 提示词段。 在 'ptc' 下,tool-guidance order band 中的惰性 tools:sdk 段为当前 scope 的可见能力渲染所加载运行时语言的声明加固定的使用说明(默认 TypeScript;语言分发 note 加入了 Python 与按 ctx.ptcRuntime.language 选择的渲染器表)。它共享查找和执行可见性,排除 run_code,并按字典序排列工具以获得字节稳定的输出。
组装所有权。 run_code 和 tools:sdk 作为正常的组装输入进入受信任的 system-prompt/assemble waterfall。一个 scoped 的 tools:sdk 段可以在分发前遮蔽全局默认值,监听器也可以移除或替换任一贡献。waterfall 返回的组装结果是最终的,因此修改这些输入的人有责任在部署期望 PTC mode 可用时保持协议面的完整性;没有恢复 pass 会覆盖有意的组合。
代码生成。 jsonSchemaToTs() 将 defineTool 的 JSON Schema 子集映射为 TypeScript,将 schema 描述带入 JSDoc,不支持的构造降级为 unknown。SDK 将工具暴露为带引号的对象键,支持任意名称而无需别名或冲突处理。类型是建议性的,因为运行时在执行前会剥离类型。
run_code 工具与分发桥
在 'ptc' 下,注册表拥有 run_code 作为保留的呈现传输通道,带两个必需参数 { code: string; description: string }(description 为 UI 标注该调用,沿用 bash 的先例)。它由一个正常的 ToolDefinition 表示以供分发,但位于可过滤的能力层之外,因此限制规则不会意外移除 PTC mode 的唯一入口。调用遍历完整的工具流水线——tools/pre-execute → 单调性守卫 → tools/execute 包裹分发 → tools/post-execute → 由定义拥有的可选 finalizeContent → 不可变的 tools/result 通知——与原生调用完全一致;权限插件可以在程序运行前检查程序文本,最终结果观察者看到的是规范化的外层结果。其 execute(args, exec):
- 构建绑定。 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定都会对无损 JSON 参数创建快照,进入原生约定的分发池(调度设计由实时并行 参考 负责),以确定性的 call id 和外层 token 作为
parent执行,通过外层 execution 延后返回的上下文,并记录tool/ptc-dispatch-start/tool/ptc-dispatch事件对,其中结算侧携带完整渲染后的结果内容。成功时返回工具最终的规范 JSON 值;失败则变为程序可见的ToolCallError。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。 - 运行已解析输入:
ctx.ptcRuntime.run(ctx.ptcRuntime.resolve(request))。请求携带程序、绑定、运行级信号、调用 Session 的 cwd 与适用的已解析文件策略。运行级信号将外层结算与执行取消关联。 - 完全停稳后结算。 运行时结算后,桥 abort 未完成的工作并排空分发队列后再返回。成功时返回捕获的日志和完成值,将其作为规范输出;注册表再把该值渲染为持久化的
tool/result.content,供结果卡片直接读取。运行时失败变为CodeRunFailedError;后端拒绝使用注册表的正常错误边界。两者都产生结构化的错误结果,且run_code结算后不允许子调用追加。
子调用上下文通过父调用延后。 在 run_code 内部注入会破坏父调用/结果的相邻性,因此 ToolRunContext.deferContext() 按分发顺序收集每个子结果的 additionalContexts 条目。即使程序后来抛出异常,注册表仍携带该数组;循环只在外层结果与步骤中所有兄弟结果之后追加每个条目。外层 post-execute 阻止会丢弃工具延后的条目,只暴露阻止 decision 显式附加的上下文。
并发是有界的,而非被序列化。 每次 run 拥有一个分发队列,严格按提交顺序启动调用,并通过 registry.executionMode 对每个调用分类——与原生循环所用的 fail-closed isConcurrencySafe 约定相同。连续的 parallel 类调用最多重叠 maxParallelSubCalls 个(默认 10;设为 1 恢复串行分发);exclusive 类调用会排空池并单独运行。结算时放弃尚未开始的排队调用。本 note 交付的是被序列化的占位实现;取代它的调度器由实时并行 参考 负责。
呈现。 run_code 的 render intent 按呈现意图 Agent Note在此决定:presentCall 创建一个 generic 卡片,kind: 'execute',以程序文本作为标题,并将同一程序文本作为 rawInput;run_code 有意不声明 presentResult,因此 TUI 和宿主/客户端运行时(Web)会通过通用原始内容回退机制,使用最终持久化的 tool/result.content 补全该卡片,其中包括捕获的日志,以及返回值、失败信息或 post-policy spill 预览。这不是 terminal 卡片:该卡片的语义是「工作目录中的 shell 命令」,程序不是。参见结果卡片完整性说明。
可观测性:tool/ptc-dispatch
每次子分发在进入分发池时追加一个仅日志的 tool/ptc-dispatch-start 事件,并以一个 tool/ptc-dispatch 结算事件收尾,后者包含父子 call id、工具标识、规范化参数以及完整渲染后的 content/isError 结果。它不进入模型历史,但可供持久化和 UI 使用。追加发生在开放的 run_code 轮次内。没有 agent 的直接执行仍然运行,但无法记录该事件。
新子调用使用 <parent>:ptc:<n> 标识,按提交顺序编号。消费者将所有 call id 视为不透明值:迁移逐字节保留每个历史标识,包括 :code: 子串,因此分发事件对、spill 引用及其他关联保持完整。桥接层将转发图片上下文的来源标记为 { kind: 'plugin', plugin: 'tools-ptc' }。
V2 到 V3 PTC 规范负责精确的历史标签与归属转换;原生 V3 准入负责前代标签拒绝。这些不是运行时别名:不透明扩展不能仅因版本变化就获得 PTC 生命周期含义。
ptc-runtime seam
dsh-ptc-runtime 负责 PtcRuntime 与请求、已解析 spec、绑定及结果类型。resolve(request) 验证支持的 cwd、timeout 与策略选择并补全部署默认值;run(spec) 执行完整输入。具名异步绑定和可选拒绝构造器使运行时不必了解注册表特定的工具名称。
language 选择 SDK 渲染器与 run_code flavor;不支持的语言使组装失败。isolation 标识执行基底,不标识权限。sandboxMode 表示消费方能否传入已解析 Session 文件策略。缺少运行时使 PTC 组装失败,native 呈现则不需要运行时。
程序结果以字段返回;调用方误用可以拒绝。无损绑定值在提供方自己的传输限制下跨越。沙箱模式、拒绝与强制事实独立于成功或失败。
Node 进程运行时
Node 提供方擦除可擦除 TypeScript,在配置的文件系统/子进程世界中解析可执行文件与 bootstrap,并通过共享沙箱与受管进程所有者启动。每个子进程都有空模型环境和全新程序全局对象。类型化拒绝构造器与捕获 console 仍对程序可见。
绑定通信使用与 stdout/stderr 分开的专用控制通道。Host 在分派前验证帧、调用身份与允许的绑定。输出、待处理调用与通道缓冲有配置上限;完成、截止、取消和协议失败会终止并等待受管范围。提供方 README 与沙箱 Node 决策负责确切上限和平台限制。
信任姿态
所选 OS 后端按照与 Bash 相同的策略约束直接 Node 副作用。嵌套工具保留注册表策略与审批路径。完整或部分强制能力独立报告;所需后端不可用时以失败关闭。清理继承子进程提供方声明的范围;进程描述符不意味着更强的多租户保证。
模型看到的内容
SDK 指示模型编写一个所加载运行时语言的异步函数体(默认可擦除 TypeScript;Python 运行时下为 Python async 函数体——见语言分发 note),通过 await tools.name(args) 调用工具,在需要时捕获被拒绝的工具调用,并仅 return 或 log 应重新进入上下文的输出。两种 flavor 用各自的原语陈述同一约定:相互独立的只读调用可以(MAY)在 Promise.all(TypeScript)或 asyncio.gather(Python)下重叠,有副作用的调用按提交顺序单独运行,有依赖的工作用 await 排序。声明前缀可能与原生 schema 一样大,但对提供方缓存保持稳定。
传输自身的 description 与两种 flavor 的 SDK 说明都以点名 code 和 description 这两个必填参数开头。把该调用描述成「传入一个程序」的散文会让第二个参数只能从参数 schema 中发现,而只发出 {code} 的模型会因 INVALID_ARGS 被拒,连同已写好的整个程序一起丢失。
后果
切换到 'ptc' 的部署必须更新任何仅限 native 的 toolOrder。组装监听器有责任维护任何被重写的协议消息的完整性。子分发在有界的重叠池下按提交顺序启动,而每次调用的上下文会通过外层结果保留其 source、信封与元数据。
测试
- Node 运行时: 提供方测试覆盖直接 Node 执行、已解析 cwd/策略、空环境、类型化绑定、恶意控制帧、输出上限、截止、取消与受管清理。Bootstrap 测试覆盖源代码与构建后执行。
- 注册表集成: 测试覆盖代码生成、所有呈现模式、保留名称和限制规则、scoped 可见性、权威组装重写、
toolOrder、运行时兼容性失败、完整流水线子分发、parent-token 关联、序列化、取消和队列排空、JSON 规范化、错误传播、日志事件、成功与失败程序中的有序上下文延后、外层阻止抑制以及 HMR(热模块替换)清理。 - 带密钥 e2e: 真实模型在一个程序中组合两次 bash 调用;另一个模型通过 PTC mode fs 分发发现嵌套的工作区指令。测试验证折叠的请求头、关联的分发事件、结果文件、延后上下文和模型行为。
- 快照:
ptc-turn、both-mode-turn和ptc-workspace-contextfixture(测试前置数据)固定 SDK 文本、请求头工具列表、分发事件、延后上下文和结果卡片。TypeScript SDK PTC 场景通过测试拥有的显式 profile patch 挂载 Node 运行时,并固定 Session 事件与 JSON-RPC 通知;预期回复和完成轮次检查在 refresh 写入前执行。
曾考虑的替代方案
一个零核心改动的附加消费方插件。 否决,因为 agent/request 在可重建请求下仅限 call-config,而变换已组装的工具列表需要在不拥有其配置的情况下撤销 toolOrder 规范化,并依赖监听器顺序。向模型提供哪些工具、以何种表示形式提供,是注册表的单一关注点:原生 schema 和 SDK 是同一个可见存储的两种投影。
将 node:vm 作为运行时安全机制。 语言 realm 不会对直接 Node API 强制 OS 文件/进程策略。因此提供方使用共享沙箱与受管进程所有者;独立 realm 不能代替这些保护。
在原生工具调用上做结果省略/摘要。 仅解决问题中上下文膨胀这一半:裁剪旧 tool-result 作为可重建请求下的日志化表面替换成本低,但仍需每次调用一次模型往返,且无法表达循环、分支或汇合。互补而非竞争;它可以在 PTC mode 下为残余的原生调用分层。
循环中的并行原生分发。 决策当时对往返成本的另一个答案;它被并发安全元数据阻塞,且无论如何都不提供组合能力——它并行化的是模型在一步中已经决定的调用。PTC mode 的队列决策保持了两者兼容,两者后来都已交付:元数据即 isConcurrencySafe(见并行工具调用 note),原生 rolling-pool 分发加每工具绑定并行化则基于同一个分类器。
始终排他(忠于 Cloudflare,无模式)。 否决,因为本 SDK 的主要消费方是编码 agent:其日常的单次调用(bash、read、edit)作为原生调用已经是最优的,强制每次编辑都通过程序会给常见场景增加负担。mode 配置让忠实形式('ptc')只需一行配置即可启用,而不强加于人。
每工具可见性分层(此工具 native,彼工具 ptc-only)。 推迟:它需要每工具元数据,以及针对原生操作和仅限程序操作的独立准入规则。整个 agent 采用原生或 PTC 呈现,可让每个 agent 保持一种调用约定。
SDK 中的清洁化标识符别名(my-tool → my_tool,Cloudflare 的做法)。否决:declare const 上的带引号键使每个名称可达,零别名碰撞逻辑;模型能正常处理 tools["my-tool"](…)。
REPL 风格的持久内核(状态跨 run_code 调用存活)。在 MVP 中否决:跨调用状态对会话日志不可见,破坏了「每个请求是日志的纯函数」这一可重建性保证;每次 run 均使用全新实例则维持了这一保证。内核风格的后端在未来仍可通过同一 seam 表达,配合自己的日志方案。
风险
平台强制能力与清理能力不同。 运行时报告所选沙箱后端的完整性,并继承受管进程所有者的范围。不支持或部分的平台保证保持明确,不隐藏在 isolation 描述符之后。
stripTypeScriptTypes 标记为 experimental。 它与 Node 自身原生 .ts 执行背后的引擎(amaro/swc)相同,在本仓库的整个引擎范围内作为 API 暴露。缓解措施:运行时的单元测试套件会检查位置保持和可擦除限制拒绝消息中的必需部分;调用位于一个私有函数之后,且 amaro/sucrase 可在 API 变化时直接替换它。仅可擦除子集是面向模型的输入限制,错误消息会告诉模型如何修正程序。
SDK 的提示词成本。 .d.ts 可能与它替代的原生 schema 体量相当。前缀稳定性 + 提供方缓存摊销了每会话成本;mode 按部署配置;本 Agent Note 不做无条件节省的声明。何时优先使用哪种模式的量化指导明确属于上线后学习。
注册表 scope 增长。 dsh-tools 吸收了代码生成、一个工具、一个桥和一个事件。包内模块把这些职责分开(ts-types.ts、ptc.ts 与 schema.ts、json-schema.ts、presentation.ts 并列),所有 ptc-runtime 专用实现都由 ctx.ptcRuntime 提供。
大型绑定值仍可能耗尽内存。 Node 提供方限制控制帧、未完成参数和排队写入,而 Host 绑定可以在这些检查前分配其结果。无损 JSON 传输与外层输出账本不构成进程级分配配额。
子分发的重叠由工具自身的安全声明限定,而非由调用方决定。 程序里的 Promise.all 或 asyncio.gather 只在工具自己分类为并发安全的调用之间换来挂钟并行性;一串 exclusive 调用仍要按顺序付出各自的往返开销,模型可能过度期望。两种 flavor 的 SDK 说明都陈述了真实约定。本 note 交付的是使该风险绝对化的序列化占位实现;调度器及其重叠上限由实时并行 参考 负责。
经过时间截止包括被等待的工作。 嵌套工具和审批等待消耗程序的经过时间预算。Host 在截止到期时停止执行并等待受管清理;它不计量进程树 CPU 消耗。沙箱 Node 决策记录当前默认值与延后评估的 timeout 问题。