PTC mode — the model writes TypeScript against the tool registry
PTC mode——模型针对工具注册表编写 TypeScript
In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. `ToolRuntime` contributes its schemas to the system-prompt assembly, the assembly's `tools` land on the wire (and in the logged request header), the model invokes one `tool-call` block per step, and at the time of this note the loop dispatched each call through `ctx.tools.execute()` **sequen
English
Problem
In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. ToolRuntime contributes its schemas to the system-prompt assembly, the assembly's tools land on the wire (and in the logged request header), the model invokes one tool-call block per step, and at the time of this note the loop dispatched each call through ctx.tools.execute() sequentially (parallel tool execution was an open TODO then; bounded parallel dispatch has since shipped — the parallel tool-call note, the rolling pool in docs/architecture.md) — with every intermediate tool-result re-entering the model's context on the next request.
For multi-step tool work this is token-heavy and serial. The model cannot compose tools — loop over a result set, branch on an intermediate value, fan out, post-process — without a full model round-trip per call, and each round-trip drags the entire intermediate result back into context whether the model needs it or not.
Cloudflare's Code Mode proposes an alternative grounded in a simple observation: LLMs are better at writing code than at emitting tool calls, because they have seen millions of lines of real code and comparatively few contrived tool-calling traces. Instead of one tool call per step, the model writes a TypeScript program against a generated API over the tools, the program executes in a sandboxed runtime, and the model curates what comes back — only what it prints or returns — instead of every intermediate result.
Tool presentation belongs to the registry that owns tool visibility: implementing another presentation as an after-the-fact waterfall transform would depend on listener order and fight reconstructable requests. Execution must preserve fresh program state, host-owned bindings and cancellable settlement independently of the presentation mode.
Decision
Three decisions, each elaborated in its own section below:
- PTC mode is a first-class presentation mode of
ToolRuntime(dsh-tools), selected by a validatedmodeconfig:'native'(the default, contributing the visible capability schemas) or'ptc'(the registry contributes only its reservedrun_codetransport plus a generated SDK.d.tsin the system prompt). The registry constructs its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation. - Code execution is a capability seam —
packages/ptc-runtime/contains the Service Definition package@deepseek-ai/dsh-ptc-runtime, which ownsctx.ptcRuntime(capability seams; Consumer =dsh-tools, with core-consumes-a-seam precedent inagent-loop→dsh-llm). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports{ value, logs, error? }. Language and substrate are backend properties, so a future Python or container backend is another Service Provider package, not a redesign. - The shipped implementation is
@deepseek-ai/dsh-ptc-runtime-node: each program runs in a fresh managed Node process under the calling Session's resolved sandbox policy. Direct Node APIs remain available within that policy; named host bindings cross a separately validated control channel.
This note owns PTC presentation, composition and settlement. The typed tool-return decision owns generated outputs, canonical binding values, ToolCallError and lossless outer output. The sandboxed Node decision supersedes the worker execution, trust posture and budget realization while preserving this note's consumer rules.
Per-program execution controls
The consumer advertises timeoutMs from the runtime's readonly timeout descriptor and passes explicit values through its resolver. Unsupported providers expose no timeout field and reject an explicit request. Sandbox escalation uses the shared strict-widening approval helper before runtime resolution and launch; replacing only the resolved policy's mode keeps the grant local to one program. Nested dispatches retain the original Agent and Session, so their approvals and file policy remain independent. No failed program is replayed implicitly because earlier effects may already have committed.
The Node execution decision owns elapsed-budget accounting and deferred lifetime questions.
The registry owns the mode
ToolRuntime gains a schemastery-validated config (static Config), its first: mode: 'native' | 'ptc', default 'native'. A deployment flips it from cordis.yml (tools: { mode: ptc }) — no code edit, per the no-hardcoded-tunables convention.
Wire tool list. The registry contributes visible capabilities in 'native' and only run_code in 'ptc'. The presentation simplification owns the removal of mixed presentation. The final PromptAssembly.tools list is logged in the request header. run_code is a reserved presentation transport outside registration and restriction layers; direct prompt providers and the assembly waterfall remain responsible for their own contributions.
Interaction with toolOrder: a configured systemPrompt.toolOrder naming native capabilities rejects every assembly under mode: 'ptc', because those names are outside that mode's wire-validation universe. This is correct behavior, not a bug: a deployment using PTC mode updates its order config or drops it.
SDK prompt section. In 'ptc', the lazy tools:sdk section in the tool-guidance order band renders the loaded runtime's language declarations plus fixed usage instructions for the scope's visible capabilities (TypeScript by default; the language-dispatch note added Python and the ctx.ptcRuntime.language renderer table). It shares lookup and execution visibility, excludes run_code, and sorts tools lexicographically for byte-stable output.
Assembly ownership. run_code and tools:sdk enter the trusted system-prompt/assemble waterfall as normal assembly inputs. A scoped tools:sdk section may shadow the global default before dispatch, and a listener may remove or replace either contribution. The waterfall's returned assembly is final, so whoever changes these inputs owns preserving a viable PTC mode protocol when the deployment expects PTC mode to remain usable; no restoration pass overrides deliberate composition.
Codegen. jsonSchemaToTs() maps the defineTool JSON-Schema subset to TypeScript, carries schema descriptions into JSDoc, and degrades unsupported constructs to unknown. The SDK exposes tools as quoted object keys, supporting arbitrary names without aliases or collisions. Typing is advisory because the runtime strips types before execution.
The run_code tool and the dispatch bridge
Under 'ptc' the registry owns run_code as a reserved presentation transport with two required parameters, { code: string; description: string } (the description labels the call in UIs, the bash precedent). It is represented by a normal ToolDefinition for dispatch but stays outside the filterable capability layers, so restrictions cannot accidentally remove PTC mode's only entry point. Calls traverse the complete tool pipeline — tools/pre-execute → monotonic guards → tools/execute around dispatch → tools/post-execute → optional definition-owned finalizeContent → immutable tools/result notification — exactly like native calls; a permission plugin can inspect the program text before it runs, and final-result observers see the normalized outer outcome. Its execute(args, exec):
- Build bindings. One run-scoped signal follows outer cancellation and is aborted whenever the run settles. Each visible tool binding snapshots lossless-JSON arguments, enters the native-contract dispatch pool (the live-parallel reference owns the scheduling design), executes with a deterministic call id and the outer token as
parent, defers returned contexts through the outer execution, and logs thetool/ptc-dispatch-start/tool/ptc-dispatchpair, the settle side carrying the full rendered result content. Success returns the tool's final canonical JSON value; failure becomes the program-visibleToolCallError. Every sub-call retains its own immutable execution identity and traverses the full tool pipeline. - Runs resolved inputs:
ctx.ptcRuntime.run(ctx.ptcRuntime.resolve(request)). The request carries the program, bindings, run-scoped signal, calling Session cwd and applicable resolved file policy. The run-scoped signal connects outer settlement to execution cancellation. - Settle after quiescence. When the runtime settles, the bridge aborts outstanding work and drains the dispatch queue before returning. Success returns captured logs and the completion value as canonical output; the registry renders that value into durable
tool/result.content, which the result card reads directly. A runtime failure becomesCodeRunFailedError; backend rejection uses the registry's normal error boundary. Both produce structured error results, and no sub-call can append afterrun_codesettles.
Sub-call contexts are deferred through the parent. Injecting inside run_code would break parent call/result adjacency, so ToolRunContext.deferContext() collects every sub-result additionalContexts entry in dispatch order. The registry carries that array even when the program later throws, and the loop appends each entry only after the outer result and every sibling result in the step. An outer post-execute block discards tool-deferred entries and exposes only contexts explicitly attached by the blocking decision.
Concurrency is bounded, not serialized. Each run owns a dispatch queue that starts calls strictly in submission order and classifies each one through registry.executionMode, the same fail-closed isConcurrencySafe contract the native loop uses. Consecutive parallel-classified calls overlap up to maxParallelSubCalls (default 10; 1 restores serial dispatch); an exclusive call drains the pool and runs alone. Settlement abandons queued calls that have not started. This note shipped the serialized placeholder; the live-parallel reference owns the scheduler that replaced it.
Presentation. run_code's render intent is decided here per the render-intent Agent Note: presentCall creates a generic card with kind: 'execute', the program text as its title, and the same program text as rawInput; run_code intentionally declares no presentResult, so the TUI and host/client runtime (Web) complete that card through their generic raw-content fallback using the final durable tool/result.content, including captured logs plus the returned value, failure, or post-policy spill preview. This is not a terminal card: that card's semantics are "a shell command in a working directory", which a program is not. See the result-card completeness note.
Observability: tool/ptc-dispatch
Each sub-dispatch appends a log-only tool/ptc-dispatch-start event at pool entry and a tool/ptc-dispatch settle event containing parent and child call ids, tool identity, normalized arguments, and the complete rendered content/isError outcome. It remains outside model history but available to persistence and UIs. Appends occur inside the open run_code turn. Direct executions without an agent still run but cannot log the event.
New sub-calls use <parent>:ptc:<n> ids, numbered in submission order. All call ids are opaque to consumers: migration preserves every historical id byte-for-byte, including :code: substrings, so dispatch pairs, spill references, and other correlations remain intact. The bridge attributes forwarded image context to { kind: 'plugin', plugin: 'tools-ptc' }.
The V2-to-V3 PTC specification owns exact historical tag and attribution conversion; native V3 admission owns predecessor-tag refusal. These are not runtime aliases: an opaque extension must not acquire PTC lifecycle meaning merely through a version change.
The ptc-runtime seam
dsh-ptc-runtime owns PtcRuntime and the request, resolved-spec, binding and result types. resolve(request) validates supported cwd, timeout and policy choices and supplies deployment defaults; run(spec) executes complete inputs. Named async bindings and optional rejection constructors keep registry-specific tool names outside the runtime.
language selects the SDK renderer and run_code flavor; an unsupported language fails assembly. isolation identifies the substrate, not its authority. sandboxMode indicates whether the consumer can pass a resolved Session file policy. Missing runtimes fail PTC assembly, while native presentation does not require one.
Program outcomes resolve as result fields; caller misuse may reject. Lossless binding values cross under provider-owned transport limits. The sandbox mode, denial and enforcement facts are independent from success or failure.
The Node process runtime
The Node provider strips erasable TypeScript, resolves its executable/bootstrap in the configured filesystem/subprocess world, and launches through the shared sandbox and managed process owners. Each child has an empty model environment and fresh program globals. Typed rejection constructors and the capturing console remain program-visible.
Binding traffic uses a dedicated control channel separate from stdout/stderr. The host validates frames, call identities and allowed bindings before dispatch. Output, pending calls and channel buffers have configured limits; completion, deadline, cancellation and protocol failure terminate and await the managed range. The provider README and sandboxed Node decision own exact bounds and platform limits.
Trust posture
The selected OS backend constrains direct Node effects under the same policy used by Bash. Nested tools retain their registry policy and approval path. Full or partial enforcement is reported separately, and an unavailable required backend fails closed. Cleanup inherits the subprocess provider's documented range; a process descriptor does not imply a stronger multi-tenant guarantee.
What the model sees
The SDK instructs the model to write an async body in the loaded runtime's language (an erasable-TypeScript body by default; a Python async body under a Python runtime — see the language-dispatch note), call tools through await tools.name(args), catch rejected tool calls when needed, and return or log only the output that should re-enter context. Both flavors state the same contract in their own primitive: independent read-only calls MAY overlap under Promise.all (TypeScript) or asyncio.gather (Python), mutating calls run alone in submission order, and dependent work sequences with await. The declaration prefix can be as large as native schemas, but remains stable for provider caching.
The transport's own description and both SDK instruction flavors open by naming code and description as the call's two required arguments. Prose that describes the call as passing a program leaves the second argument discoverable only through the parameter schema, and a model that emits {code} alone loses the whole written program to an INVALID_ARGS rejection.
Consequences
Deployments switching to 'ptc' must update any native-only toolOrder. Assembly listeners own the integrity of any rewritten protocol messages. Sub-dispatch starts in submission order under a bounded overlap pool, while per-call contexts retain their source, envelope, and metadata through the outer result.
Testing
- Node runtime: Provider tests cover direct Node execution, resolved cwd/policy, empty environment, typed bindings, hostile control frames, output limits, deadlines, cancellation and managed cleanup. Bootstrap tests cover source and built execution.
- Registry integration: Tests cover code generation, all presentation modes, reserved-name and restriction rules, scoped visibility, authoritative assembly rewrites,
toolOrder, runtime compatibility failures, full-pipeline sub-dispatch, parent-token correlation, serialization, cancellation and queue drain, JSON normalization, error propagation, log events, ordered context deferral across successful and failed programs, outer-block suppression, and HMR cleanup. - With-key e2e: A real model composes two bash calls in one program; another discovers nested workspace instructions through a PTC mode fs dispatch. The tests verify collapsed request headers, correlated dispatch events, resulting files, deferred context, and model behavior.
- Snapshot: The
ptc-turn,both-mode-turn, andptc-workspace-contextfixtures pin SDK text, header tool lists, dispatch events, deferred context, and result cards. The TypeScript SDK PTC scenario mounts its Node runtime through an explicit test-owned profile patch and pins Session events and JSON-RPC notifications; its expected response and completed-turn checks run before refresh writes.
The optional Node in-memory execution mode permits disposable cross-call memory without restoring objects or replaying programs after restart; the default execution mode stays stateless.
Alternatives considered
An add-on consumer plugin with zero core changes. Rejected because agent/request is call-config-only under reconstructable requests, while transforming an assembled tool list would have to undo toolOrder canonicalization without owning its config and would depend on listener order. Which tools the model is offered, and in which representation, is the registry's single concern: native schemas and the SDK are two projections of one visible store.
Use node:vm as the runtime security mechanism. A language realm does not enforce OS file/process policy on direct Node APIs. The provider therefore uses the shared sandbox and managed process owners; a separate realm is not a substitute for those protections.
Result elision / summarization over native tool-calling. Addresses only the context-bloat half of the problem: trimming old tool-results is cheap to add as a logged surface replacement under reconstructable requests, but still pays one model round-trip per call and cannot express loops, branches, or joins. Complementary, not competing; it can layer under PTC mode for residual native calls.
Parallel native dispatch in the loop. The other answer to round-trip cost at decision time; it was blocked on concurrency-safety metadata and offers no composition either way — it parallelizes calls the model already decided on in one step. PTC mode's queue decision kept the two compatible, and both later shipped: the metadata as isConcurrencySafe (the parallel tool-call note), and native rolling-pool dispatch plus per-tool binding parallelism on the same classifier.
Always-exclusive (Cloudflare-faithful, no mode). Rejected for this SDK's primary consumer: a coding agent's bread-and-butter single calls (bash, read, edit) are already ideal as native calls, and forcing every edit through a program taxes the common case. The mode config keeps the faithful form ('ptc') one line away without imposing it.
Per-tool visibility tiers (this tool native, that tool ptc-only). Deferred: it needs per-tool metadata and separate admission rules for native and program-only operations. Whole-agent native or PTC presentation keeps one calling convention per agent.
Sanitized identifier aliases in the SDK (my-tool → my_tool, Cloudflare's approach). Rejected: quoted keys on a declare const make every name reachable with zero alias-collision logic; models handle tools["my-tool"](…) fine.
A REPL-style persistent kernel (state survives across run_code calls). Rejected for the MVP: cross-call state would be invisible to the session log, breaking the reconstructability guarantee that every request is a pure function of the log; fresh-per-run keeps it. A kernel-style backend remains expressible behind the seam later, with its own logging story.
Risks
Platform enforcement and cleanup vary. The runtime reports the selected sandbox backend's completeness and inherits the managed process owner's range. Unsupported or partial platform guarantees remain explicit rather than being hidden behind the isolation descriptor.
stripTypeScriptTypes is marked experimental. It is the same engine (amaro/swc) behind Node's own native .ts execution, exposed as an API across this repo's whole engines range. Mitigations: the runtime's unit suite checks position preservation and the required parts of the erasable-only rejection message, the call sits behind one private function, and amaro/sucrase are direct replacements if the API shifts. The erasable-only subset is a model-facing input restriction, and the error tells the model how to correct the program.
Prompt cost of the SDK. The .d.ts can rival the native schemas it replaces. Prefix stability + provider caching amortize per-session cost; the mode is per-deployment; the Agent Note makes no unconditional-savings claim. Measured guidance (when to prefer which mode) is explicitly post-ship learning.
Registry scope growth. dsh-tools absorbs codegen, a tool, a bridge, and an event. Package modules separate these responsibilities (ts-types.ts and ptc.ts beside schema.ts, json-schema.ts, and presentation.ts), while ctx.ptcRuntime owns all ptc-runtime-specific implementation.
Large binding values can still exhaust memory. The Node provider bounds control frames, outstanding arguments and queued writes, while a host binding can allocate its result before those checks. Lossless JSON transfer and the outer output ledger do not create a process-wide allocation quota.
Sub-dispatch overlap is bounded by tool safety claims, not by the caller. A program's Promise.all or asyncio.gather buys wall-clock parallelism only across calls the tool itself classifies concurrency-safe; a run of exclusive calls still costs its round-trips in sequence, and models may over-expect. Both flavors' SDK instructions state the real contract. This note shipped the serialized placeholder that made the risk absolute; the live-parallel reference owns the scheduler and its overlap cap.
Elapsed deadlines include awaited work. Nested tools and approval waits consume the program's elapsed budget. The host stops execution on expiry and awaits managed cleanup; it does not meter process-tree CPU consumption. The sandboxed Node decision records current defaults and deferred timeout questions.
中文
问题
在注册表的原生呈现方式下,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 写入前执行。
可选的Node 内存保留执行模式允许跨调用保留可丢失的内存,不在重启后恢复对象或回放程序;默认执行模式保持无状态。
曾考虑的替代方案
一个零核心改动的附加消费方插件。 否决,因为 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 问题。