diff --git a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml index a6c2c93f3d..cf9b3216dc 100644 --- a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md -2026-07-20-canonical-tool-output-contract.md: f2c17325f77b93675086c39dd5a7693854b8521a -2026-07-20-canonical-tool-output-contract.zh.md: d81c1730aab66df2dcf4eea4515f260df917ffb4 +2026-07-20-canonical-tool-output-contract.md: 4dbcac3da8381e69809b15a653cdb4987af4e0e7 +2026-07-20-canonical-tool-output-contract.zh.md: c2f4ebdfc7266495570766c69b3aa264f91172cc diff --git a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md index f2c17325f7..4dbcac3da8 100644 --- a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md +++ b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md @@ -34,7 +34,7 @@ type ToolExecutionResult = `tools/post-execute` has two mutually exclusive successful projections. Replacing `content` changes only Native/model presentation and preserves the canonical value and metadata. Replacing `value` revalidates the replacement and recomputes both presentation projections. A block removes the value and becomes a failure. Content replacement is therefore not a confidentiality mechanism: policy that must prevent programmatic access blocks the call or replaces the value. -Canonical values are execution-local. The agent loop persists `tool/result` with only `content`, `error`, and optional `meta`; PTC mode's `tool/code-dispatch` persists the sub-call's rendered `content` and `isError`. Neither event stores the canonical intermediate value, so replay reproduces presentation but cannot reconstruct the programmatic result. When a tool declares `presentationMeta`, it is computed only for a direct surface call; a nested Code dispatch gets no metadata. The Client can derive [nested terminal cards](../bug-fix/2026-09-05-nested-terminal-cards.md) from raw arguments and rendered content without that metadata. The outer `run_code` card instead reads final post-policy content and declares no presentation metadata. Generic and tool-owned spill projections similarly skip nested dispatches, whose canonical value never enters model context. +Canonical values are execution-local. The agent loop persists `tool/result` with only `content`, `error`, and optional `meta`; PTC mode's `tool/ptc-dispatch` persists the sub-call's rendered `content` and `isError`. Neither event stores the canonical intermediate value, so replay reproduces presentation but cannot reconstruct the programmatic result. When a tool declares `presentationMeta`, it is computed only for a direct surface call; a nested Code dispatch gets no metadata. The Client can derive [nested terminal cards](../bug-fix/2026-09-05-nested-terminal-cards.md) from raw arguments and rendered content without that metadata. The outer `run_code` card instead reads final post-policy content and declares no presentation metadata. Generic and tool-owned spill projections similarly skip nested dispatches, whose canonical value never enters model context. The first-party tools preserve their existing Native text while returning domain DTOs: diff --git a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md index d81c1730aa..c2f4ebdfc7 100644 --- a/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.zh.md @@ -34,7 +34,7 @@ type ToolExecutionResult = `tools/post-execute` 为成功结果提供两种互斥的投影方式。替换 `content` 只改变 Native/模型展示,并保留规范值和元数据。替换 `value` 会重新校验替代值,并重新计算两份展示投影。阻止操作会移除值并转为失败。因此,替换内容并不是保密机制:必须阻止程序化访问的策略,应当阻止调用或替换值。 -规范值仅存在于执行期间。agent loop(智能体循环)持久化的 `tool/result` 只包含 `content`、`error` 和可选的 `meta`;PTC mode 的 `tool/code-dispatch` 持久化子调用渲染后的 `content` 与 `isError`。两个事件都不存储规范中间值,因此回放可以重现展示,却无法重建程序化结果。当工具声明 `presentationMeta` 时,系统只会为直接的外层调用计算它;嵌套 Code 分发没有元数据。Client 可以从原始参数与渲染后的内容派生[嵌套 terminal 卡片](../bug-fix/2026-09-05-nested-terminal-cards.zh.md),无需这些元数据。外层 `run_code` 卡片则读取最终的 post-policy 内容,并且不声明展示元数据。通用以及工具自有的 spill 投影同样跳过嵌套分发,因为它们的规范值永远不会进入模型上下文。 +规范值仅存在于执行期间。agent loop(智能体循环)持久化的 `tool/result` 只包含 `content`、`error` 和可选的 `meta`;PTC mode 的 `tool/ptc-dispatch` 持久化子调用渲染后的 `content` 与 `isError`。两个事件都不存储规范中间值,因此回放可以重现展示,却无法重建程序化结果。当工具声明 `presentationMeta` 时,系统只会为直接的外层调用计算它;嵌套 Code 分发没有元数据。Client 可以从原始参数与渲染后的内容派生[嵌套 terminal 卡片](../bug-fix/2026-09-05-nested-terminal-cards.zh.md),无需这些元数据。外层 `run_code` 卡片则读取最终的 post-policy 内容,并且不声明展示元数据。通用以及工具自有的 spill 投影同样跳过嵌套分发,因为它们的规范值永远不会进入模型上下文。 第一方工具在保持现有 Native 文本不变的同时返回领域 DTO: diff --git a/.agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml b/.agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml index faa551cd78..88b0c9ae9f 100644 --- a/.agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-15-ptc.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-15-ptc.md -2026-06-15-ptc.md: 96dda525c677af638654cef042b583803d948707 -2026-06-15-ptc.zh.md: 1da45205c62eb054fd534e4f395e570244066c5a +2026-06-15-ptc.md: 94a0fd3492c049ad2ce6fe3634422b1569dc80b8 +2026-06-15-ptc.zh.md: 12e321b111d1b6a63068f4e65fd836ba4918e086 diff --git a/.agents/notes/implemented/feature/2026-06-15-ptc.md b/.agents/notes/implemented/feature/2026-06-15-ptc.md index 96dda525c6..94a0fd3492 100644 --- a/.agents/notes/implemented/feature/2026-06-15-ptc.md +++ b/.agents/notes/implemented/feature/2026-06-15-ptc.md @@ -42,7 +42,7 @@ This note owns PTC mode's presentation, composition, isolation, and settlement f Under `'ptc'` and `'both'` 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)`: -1. **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 note](2026-07-26-ptc-live-parallel-dispatch.md) 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 the `tool/code-dispatch-start`/`tool/code-dispatch` pair, the settle side carrying the full rendered result content. Success returns the tool's final canonical JSON value; failure becomes the program-visible `ToolCallError`. Every sub-call retains its own immutable execution identity and traverses the full tool pipeline. +1. **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 note](2026-07-26-ptc-live-parallel-dispatch.md) 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 the `tool/ptc-dispatch-start`/`tool/ptc-dispatch` pair, the settle side carrying the full rendered result content. Success returns the tool's final canonical JSON value; failure becomes the program-visible `ToolCallError`. Every sub-call retains its own immutable execution identity and traverses the full tool pipeline. 2. **Runs the program**: `ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal })`. The runtime receives the run-scoped signal, not only the caller's outer signal, so any way the outer run settles also aborts work inside the runtime. 3. **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 becomes `CodeRunFailedError`; backend rejection uses the registry's normal error boundary. Both produce structured error results, and no sub-call can append after `run_code` settles. @@ -52,9 +52,13 @@ Under `'ptc'` and `'both'` the registry owns `run_code` as a reserved presentati **Presentation.** `run_code`'s render intent is decided here per the [render-intent Agent Note](../architecture/2026-07-02-tool-render-intent-union.md): `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](../../archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md). -### Observability: `tool/code-dispatch` +### Observability: `tool/ptc-dispatch` -Each sub-dispatch appends a log-only `tool/code-dispatch-start` event at pool entry and a `tool/code-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. +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 `:ptc:` 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 stage](../../../../packages/session/session-format-v2-to-v3/README.md) maps the exact `tool/code-dispatch-start` and `tool/code-dispatch` event tags to their PTC names. It maps `tools-code-mode` to `tools-ptc` only in plugin-kind sources at `user/message.data.source`, `agent/inbox/spliced.data.inserted[].source`, and `session/title-llm-request.data.messages[].source`. Other fields, arbitrary text, opaque payloads, and ids remain unchanged. This is an adjacent historical conversion, not an alias accepted by the native v3 reader: old required event tags are refused; unknown events marked `ignorable` retain the normal admission policy. Source-v2 events already named `tool/ptc-dispatch` or `tool/ptc-dispatch-start` are rejected even when ignorable because migration must not promote an opaque extension into a reserved v3 lifecycle event. Frozen predecessor modules and committed historical generations remain unchanged. ### The code-runtime seam @@ -98,7 +102,7 @@ Deployments switching to `'ptc'` must update any native-only `toolOrder`. Assemb - **Worker runtime:** Real-worker tests cover typed binding values and failures, every lossless JSON completion root, invalid and over-limit output, exact combined ledger boundaries, compute and wall budgets, hostile binding traffic, empty environment, and disposal to quiescence. A built-package test runs the worker entry under plain Node. - **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`, and `ptc-workspace-context` fixtures pin SDK text, header tool lists, dispatch events, deferred context, and result cards. +- **Snapshot:** The `ptc-turn`, `both-mode-turn`, and `ptc-workspace-context` fixtures pin SDK text, header tool lists, dispatch events, deferred context, and result cards. The TypeScript SDK PTC scenario mounts its worker 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. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-06-15-ptc.zh.md b/.agents/notes/implemented/feature/2026-06-15-ptc.zh.md index 1da45205c6..12e321b111 100644 --- a/.agents/notes/implemented/feature/2026-06-15-ptc.zh.md +++ b/.agents/notes/implemented/feature/2026-06-15-ptc.zh.md @@ -42,7 +42,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一 在 `'ptc'` 和 `'both'` 下,注册表拥有 `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)`: -1. **构建绑定。** 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定都会对无损 JSON 参数创建快照,进入原生约定的分发池(调度设计由[实时并行 Agent Note](2026-07-26-ptc-live-parallel-dispatch.zh.md) 负责),以确定性的 call id 和外层 token 作为 `parent` 执行,通过外层 execution 延后返回的上下文,并记录 `tool/code-dispatch-start`/`tool/code-dispatch` 事件对,其中结算侧携带完整渲染后的结果内容。成功时返回工具最终的规范 JSON 值;失败则变为程序可见的 `ToolCallError`。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。 +1. **构建绑定。** 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定都会对无损 JSON 参数创建快照,进入原生约定的分发池(调度设计由[实时并行 Agent Note](2026-07-26-ptc-live-parallel-dispatch.zh.md) 负责),以确定性的 call id 和外层 token 作为 `parent` 执行,通过外层 execution 延后返回的上下文,并记录 `tool/ptc-dispatch-start`/`tool/ptc-dispatch` 事件对,其中结算侧携带完整渲染后的结果内容。成功时返回工具最终的规范 JSON 值;失败则变为程序可见的 `ToolCallError`。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。 2. **运行程序**:`ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal })`。运行时接收的是 run 级别的 signal 而非仅调用方的外层 signal,因此外层 run 以任何方式结算都会同时 abort 运行时内部的工作。 3. **完全停稳后结算。** 运行时结算后,桥 abort 未完成的工作并排空分发队列后再返回。成功时返回捕获的日志和完成值,将其作为规范输出;注册表再把该值渲染为持久化的 `tool/result.content`,供结果卡片直接读取。运行时失败变为 `CodeRunFailedError`;后端拒绝使用注册表的正常错误边界。两者都产生结构化的错误结果,且 `run_code` 结算后不允许子调用追加。 @@ -52,9 +52,13 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一 **呈现。** `run_code` 的 render intent 按[呈现意图 Agent Note](../architecture/2026-07-02-tool-render-intent-union.zh.md)在此决定:`presentCall` 创建一个 `generic` 卡片,`kind: 'execute'`,以程序文本作为标题,并将同一程序文本作为 `rawInput`;`run_code` 有意不声明 `presentResult`,因此 TUI 和宿主/客户端运行时(Web)会通过通用原始内容回退机制,使用最终持久化的 `tool/result.content` 补全该卡片,其中包括捕获的日志,以及返回值、失败信息或 post-policy spill 预览。这不是 `terminal` 卡片:该卡片的语义是「工作目录中的 shell 命令」,程序不是。参见[结果卡片完整性说明](../../archived/bug-fix/2026-07-20-code-mode-result-card-completeness.md)。 -### 可观测性:`tool/code-dispatch` +### 可观测性:`tool/ptc-dispatch` -每次子分发在进入分发池时追加一个仅日志的 `tool/code-dispatch-start` 事件,并以一个 `tool/code-dispatch` 结算事件收尾,后者包含父子 call id、工具标识、规范化参数以及完整渲染后的 `content`/`isError` 结果。它不进入模型历史,但可供持久化和 UI 使用。追加发生在开放的 `run_code` 轮次内。没有 agent 的直接执行仍然运行,但无法记录该事件。 +每次子分发在进入分发池时追加一个仅日志的 `tool/ptc-dispatch-start` 事件,并以一个 `tool/ptc-dispatch` 结算事件收尾,后者包含父子 call id、工具标识、规范化参数以及完整渲染后的 `content`/`isError` 结果。它不进入模型历史,但可供持久化和 UI 使用。追加发生在开放的 `run_code` 轮次内。没有 agent 的直接执行仍然运行,但无法记录该事件。 + +新子调用使用 `:ptc:` 标识,按提交顺序编号。消费者将所有 call id 视为不透明值:迁移逐字节保留每个历史标识,包括 `:code:` 子串,因此分发事件对、spill 引用及其他关联保持完整。桥接层将转发图片上下文的来源标记为 `{ kind: 'plugin', plugin: 'tools-ptc' }`。 + +[v2-to-v3 阶段](../../../../packages/session/session-format-v2-to-v3/README.zh.md) 将精确匹配的 `tool/code-dispatch-start` 和 `tool/code-dispatch` 事件标签映射为对应的 PTC 名称。它仅在 `user/message.data.source`、`agent/inbox/spliced.data.inserted[].source` 和 `session/title-llm-request.data.messages[].source` 中,针对 plugin 类型来源将 `tools-code-mode` 映射为 `tools-ptc`。其他字段、任意文本、不透明载荷和标识保持不变。这是相邻历史版本转换,不是原生 v3 读取器接受的别名:旧的必需事件标签会被拒绝;标记为 `ignorable` 的未知事件保留通常的准入规则。源 v2 中已命名为 `tool/ptc-dispatch` 或 `tool/ptc-dispatch-start` 的事件即使可忽略也会被拒绝,因为迁移不得将不透明扩展提升为 v3 保留的生命周期事件。冻结的前代模块和已提交的历史代保持不变。 ### code-runtime seam @@ -98,7 +102,7 @@ SDK 指示模型编写一个所加载运行时语言的异步函数体(默认 - **Worker 运行时:** 真实 worker 测试覆盖类型化的绑定值与失败、每一种无损 JSON 根类型的完成值、无效和超限输出、精确的组合账本边界、compute 和 wall 预算、恶意绑定流量、空环境以及 dispose 至完全停稳。一个构建后包测试在纯 Node 下运行 worker 入口。 - **注册表集成:** 测试覆盖代码生成、所有呈现模式、保留名称和限制规则、scoped 可见性、权威组装重写、`toolOrder`、运行时兼容性失败、完整流水线子分发、parent-token 关联、序列化、取消和队列排空、JSON 规范化、错误传播、日志事件、成功与失败程序中的有序上下文延后、外层阻止抑制以及 HMR(热模块替换)清理。 - **带密钥 e2e:** 真实模型在一个程序中组合两次 bash 调用;另一个模型通过 PTC mode fs 分发发现嵌套的工作区指令。测试验证折叠的请求头、关联的分发事件、结果文件、延后上下文和模型行为。 -- **快照:** `ptc-turn`、`both-mode-turn` 和 `ptc-workspace-context` fixture(测试前置数据)固定 SDK 文本、请求头工具列表、分发事件、延后上下文和结果卡片。 +- **快照:** `ptc-turn`、`both-mode-turn` 和 `ptc-workspace-context` fixture(测试前置数据)固定 SDK 文本、请求头工具列表、分发事件、延后上下文和结果卡片。TypeScript SDK PTC 场景通过测试拥有的显式 profile patch 挂载 worker 运行时,并固定 Session 事件与 JSON-RPC 通知;预期回复和完成轮次检查在 refresh 写入前执行。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml index 7de92a4dd5..be9a598897 100644 --- a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md -2026-07-20-ptc-typed-tool-returns.md: 04ef9f7da4cd59ba07632684541dd6962008b810 -2026-07-20-ptc-typed-tool-returns.zh.md: c197d3131cc0f7fa326a9a47d945b2b7730f01c0 +2026-07-20-ptc-typed-tool-returns.md: b7d7cc56210b229d7e36f8face888a34c9d1ac3e +2026-07-20-ptc-typed-tool-returns.zh.md: 20e14cdac6151ceead084ff6a78eb5f7a6277af1 diff --git a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md index 04ef9f7da4..b7d7cc5621 100644 --- a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md +++ b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.md @@ -73,7 +73,7 @@ Temporary Cordis Plugins follow the same rule: `cordis_mount` returns `{ id, plu ### Persistence, metadata, and spill -Nested dispatch logs the sub-call's full rendered `content`/`isError` on `tool/code-dispatch` but does not persist canonical values. `tool/result` continues to persist only rendered content, error, and optional metadata. A successful final content sequence containing an image is also wrapped in a source-attributed user message and deferred through the outer result; the normal session event makes that model-visible input reconstructable. This feature did not itself require a structural Session-format change; the released v0-to-v1 identity edge preserves these records, and replay still cannot recreate intermediate canonical program values. +Nested dispatch logs the sub-call's full rendered `content`/`isError` on `tool/ptc-dispatch` but does not persist canonical values. `tool/result` continues to persist only rendered content, error, and optional metadata. A successful final content sequence containing an image is also wrapped in a source-attributed user message and deferred through the outer result; the normal session event makes that model-visible input reconstructable. The [PTC mode note](2026-06-15-ptc.md) owns durable event names and historical identity preservation; replay cannot recreate intermediate canonical program values. The opaque `exec.parent` token marks nested calls. Presentation metadata and generic or tool-owned spill projections skip those calls; their canonical values never enter context. The Client can derive [nested terminal cards](../bug-fix/2026-09-05-nested-terminal-cards.md) from raw dispatch events without metadata. The outer `run_code` call produces the model-facing result and may spill its final post-policy presentation; `run_code` intentionally declares neither a result presenter nor presentation metadata, so UI adapters complete the card through their generic raw-content fallback using durable `tool/result.content`. diff --git a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md index c197d3131c..20e14cdac6 100644 --- a/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md +++ b/.agents/notes/implemented/feature/2026-07-20-ptc-typed-tool-returns.zh.md @@ -73,7 +73,7 @@ PTC mode 通过运行时请求中的 `{ name: "ToolCallError", memberNamePropert ### 持久化、元数据与 spill -嵌套分发在 `tool/code-dispatch` 上记录子调用完整渲染后的 `content`/`isError`,但不会持久化规范值。`tool/result` 继续只持久化渲染后的内容、错误和可选元数据。包含图片的成功最终内容序列还会包装成带来源归属的用户消息,并经外层结果延后;普通会话事件使该模型可见输入可以重建。该功能本身不要求结构性 Session 格式变更;已发布的 v0-to-v1 恒等边会保留这些记录,回放仍无法重建程序的规范中间值。 +嵌套分发在 `tool/ptc-dispatch` 上记录子调用完整渲染后的 `content`/`isError`,但不会持久化规范值。`tool/result` 继续只持久化渲染后的内容、错误和可选元数据。包含图片的成功最终内容序列还会包装成带来源归属的用户消息,并经外层结果延后;普通会话事件使该模型可见输入可以重建。[PTC mode note](2026-06-15-ptc.zh.md) 负责持久事件名称与历史标识保留规则;回放无法重建程序的规范中间值。 不透明的 `exec.parent` token 用于标识嵌套调用。展示元数据以及通用或工具自有的 spill 投影都会跳过这些调用;它们的规范值永远不会进入上下文。Client 可以从原始分发事件派生[嵌套 terminal 卡片](../bug-fix/2026-09-05-nested-terminal-cards.zh.md),无需元数据。外层 `run_code` 调用产生面向模型的结果,并且可能对 post-policy 处理后的最终展示执行 spill;`run_code` 有意既不声明结果展示器,也不声明展示元数据,因此 UI 适配器会通过通用的原始内容回退机制,使用持久化的 `tool/result.content` 补全该卡片。 diff --git a/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.i18n.yaml b/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.i18n.yaml index fe3522ee91..ae720a62ac 100644 --- a/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.md -2026-07-26-ptc-live-parallel-dispatch.md: 53388e4b4cc067527bd1ed04d6c66054e84ecd44 -2026-07-26-ptc-live-parallel-dispatch.zh.md: 8eb8e6d743be65db50a4685eafb2496bc791141c +2026-07-26-ptc-live-parallel-dispatch.md: f064314098af9c074f7cd06dc9ceea3df4c91c76 +2026-07-26-ptc-live-parallel-dispatch.zh.md: 70cbebcdc30c5771a2c29f36b588232822e19269 diff --git a/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.md b/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.md index 53388e4b4c..f064314098 100644 --- a/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.md +++ b/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.md @@ -4,7 +4,7 @@ Status: implemented English | [中文](2026-07-26-ptc-live-parallel-dispatch.zh.md) -> Scope: the `tool/code-dispatch-start` event, per-sub-call running state in the web chat, and the bridge's scheduler reusing the native concurrency contract. Builds on the [host foundation](../../archived/feature/2026-07-26-ptc-dispatch-ui-foundation.md) and [chat sub-call rows](../../archived/feature/2026-07-26-ptc-chat-subcall-rows.md); the native contract itself is owned by the [parallel tool-call note](2026-07-10-parallel-tool-call-execution.md). +> Scope: the `tool/ptc-dispatch-start` event, per-sub-call running state in the web chat, and the bridge's scheduler reusing the native concurrency contract. Builds on the [host foundation](../../archived/feature/2026-07-26-ptc-dispatch-ui-foundation.md) and [chat sub-call rows](../../archived/feature/2026-07-26-ptc-chat-subcall-rows.md); the native contract itself is owned by the [parallel tool-call note](2026-07-10-parallel-tool-call-execution.md). ## Problem @@ -14,7 +14,7 @@ Two gaps remained after the host foundation and chat sub-call rows shipped. Sub- **One lifecycle pair, one scheduling contract, shared with native.** -- **Event pair**: `tool/code-dispatch-start` (parent/sub ids, name, normalized args) is appended when the scheduler actually starts a call — not at submission, so a queued call abandoned by run settlement logs nothing. The existing `tool/code-dispatch` settles the pair (same `subCallId`); every started call settles exactly once (aborts settle as `isError` outcomes through the pipeline). Timing = the two events' `time` fields. Both stay log-only; model context is untouched; format stays v0. +- **Event pair**: `tool/ptc-dispatch-start` (parent/sub ids, name, normalized args) is appended when the scheduler actually starts a call — not at submission, so a queued call abandoned by run settlement logs nothing. The existing `tool/ptc-dispatch` settles the pair (same `subCallId`); every started call settles exactly once (aborts settle as `isError` outcomes through the pipeline). Timing = the two events' `time` fields. Both stay log-only; model context is untouched. - **Bridge scheduler**: submitted calls are classified at start time via `registry.executionMode` (the SAME fail-closed `isConcurrencySafe` contract the loop uses) and start strictly in submission order. One single-lane driver owns every ORDERED stage — the start append, `prepare` (pre-execute/guards), the head-of-line `finalize`/`finish` commit (post-execute + context deferral + settle append) — so ordered policy stages never overlap each other and only the around-dispatch/body stage runs concurrently, exactly the native loop's sequencing (`fillPool` awaits `startCall` then `commitReady`). Consecutive parallel-classified calls overlap up to `maxParallelSubCalls` (a `Config` field validated by the Loader schema AND re-validated at direct construction, default 10 — the loop scheduler's own default; `1` restores serial dispatch); an exclusive call drains the pool, runs alone, and holds its barrier until its COMMIT completes (post-execute included), like a native exclusive group. Run settlement aborts in-flight dispatches and abandons queued-unstarted ones (binding rejection, no events), then drains to quiescence — including a commit already mid-flight when the program returned — before the outer result closes the turn. - **Client**: Runtime's `ToolCallTree` stores a start event as a `RunningToolCall` child and projects it through the parent's recursive `subCalls` (rows derive the running ring from that shape, exactly as for native in-flight calls). Its settle replaces the private-index entry in place, preserving start order under parallel completion and carrying the start's `time` as `callTime` (duration source). A settle with no observed start (window cut mid-pair, or a pre-start-event log) appends directly, so old logs keep rendering. - **SDK prompt**: the model-facing "calls execute sequentially" sentence is replaced with the true contract (independent safe calls may overlap under `Promise.all`; dependent work sequences with `await`) — a model-visible change, re-recorded across every ptc snapshot. diff --git a/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.zh.md b/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.zh.md index 8eb8e6d743..70cbebcdc3 100644 --- a/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.zh.md +++ b/.agents/notes/implemented/feature/2026-07-26-ptc-live-parallel-dispatch.zh.md @@ -4,7 +4,7 @@ Status: implemented [English](2026-07-26-ptc-live-parallel-dispatch.md) | 中文 -> 范围:`tool/code-dispatch-start` 事件、Web chat 中每个子调用的运行状态,以及桥接层调度器对原生并发约定的复用。构建在[宿主侧基础](../../archived/feature/2026-07-26-ptc-dispatch-ui-foundation.md)与 [chat 子调用行](../../archived/feature/2026-07-26-ptc-chat-subcall-rows.md)之上;原生约定本身归[并行工具调用 Agent Note](2026-07-10-parallel-tool-call-execution.zh.md) 所有。 +> 范围:`tool/ptc-dispatch-start` 事件、Web chat 中每个子调用的运行状态,以及桥接层调度器对原生并发约定的复用。构建在[宿主侧基础](../../archived/feature/2026-07-26-ptc-dispatch-ui-foundation.md)与 [chat 子调用行](../../archived/feature/2026-07-26-ptc-chat-subcall-rows.md)之上;原生约定本身归[并行工具调用 Agent Note](2026-07-10-parallel-tool-call-execution.zh.md) 所有。 ## 问题 @@ -14,7 +14,7 @@ Status: implemented **一对生命周期事件,一份调度约定,与原生共用。** -- **事件对**:`tool/code-dispatch-start`(父/子 id、名称、规范化参数)在调度器真正启动某个调用时才追加,而非在提交时,因此因 run 结算而被放弃的排队调用不会留下任何日志。既有的 `tool/code-dispatch` 结算该事件对(`subCallId` 相同);每个已启动的调用恰好结算一次(中止也会作为 `isError` 结果经由流水线结算)。计时即这两个事件的 `time` 字段。两个事件仍仅用于日志;模型上下文不受影响;格式保持 v0。 +- **事件对**:`tool/ptc-dispatch-start`(父/子 id、名称、规范化参数)在调度器真正启动某个调用时才追加,而非在提交时,因此因 run 结算而被放弃的排队调用不会留下任何日志。既有的 `tool/ptc-dispatch` 结算该事件对(`subCallId` 相同);每个已启动的调用恰好结算一次(中止也会作为 `isError` 结果经由流水线结算)。计时即这两个事件的 `time` 字段。两个事件仍仅用于日志;模型上下文不受影响。 - **桥接层调度器**:已提交的调用在启动那一刻经 `registry.executionMode` 分类(与 loop 所用完全相同、故障时默认判为不安全的 `isConcurrencySafe` 约定),并严格按提交顺序启动。所有有序阶段——start 事件追加、`prepare`(pre-execute/守卫)、队首 `finalize`/`finish` 提交(post-execute + 上下文延迟提交 + settle 事件追加)——由单通道驱动器独占执行,因此有序策略阶段彼此绝不重叠,只有 around-dispatch/工具体阶段并发运行,与原生 loop 的时序完全一致(`fillPool` 先 await `startCall` 再 `commitReady`)。连续被分类为可并行的调用可以重叠执行,上限为 `maxParallelSubCalls`(`Config` 字段,Loader schema 校验之外直接构造时也重新校验,默认值 10,即 loop 调度器自身的默认值;设为 `1` 即恢复串行分发);独占调用则先排空池、独自运行,且其屏障保持到自身提交(含 post-execute)完成为止,与原生独占分组一致。run 结算时会中止仍在运行的分发,并放弃已排队未启动的分发(绑定调用被拒绝,不产生事件),随后排空到完全停稳——包括程序返回时已在途的提交——之后外层结果才结束该轮次。 - **客户端侧**:运行时的 `ToolCallTree` 把 start 事件存为 `RunningToolCall` 子级,并通过父级递归的 `subCalls` 投影出来(行组件从该形状推导出运行指示环,与原生运行中的调用处理完全一致)。其结算事件会原位替换私有索引中的条目,即使并行完成也保持启动顺序不变,并把 start 事件的 `time` 作为 `callTime`(时长来源)带入。未观察到对应 start 的结算事件(窗口切在事件对中间,或日志录制于 start 事件引入之前)会直接追加,因此旧日志仍能照常渲染。 - **SDK 提示词**:面向模型的「调用按顺序执行」一句替换为真实约定(相互独立的安全调用可以在 `Promise.all` 下重叠执行;相互依赖的工作以 `await` 顺序衔接);这是模型可见的变更,每一份 PTC mode 快照都已重新录制。 diff --git a/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts b/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts index 8ee3affb8a..aa3c9e7a17 100644 --- a/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/ptc.e2e.ts @@ -30,7 +30,7 @@ import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis' /** * With-key PTC mode proof: a real model receives only `run_code`, composes two * sub-calls, writes a file, and returns curated output while the log records - * each `tool/code-dispatch`. The keyless Loader smoke is in the sibling test. + * each `tool/ptc-dispatch`. The keyless Loader smoke is in the sibling test. */ const PERSONA = 'You are a coding agent. You work by writing TypeScript programs for run_code: ' @@ -380,7 +380,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('PTC mode: real model writes a pr expect(calls.length).toBeGreaterThan(0) expect(calls.every(event => event.data.name === RUN_CODE_NAME)).toBe(true) // …and the program's tool calls landed as dispatch events under it. - const dispatches = events.filter(event => event.type === 'tool/code-dispatch') + const dispatches = events.filter(event => event.type === 'tool/ptc-dispatch') expect(dispatches.length).toBeGreaterThanOrEqual(2) expect(dispatches.every(event => event.data.name === 'bash')).toBe(true) const parents = new Set(calls.map(event => event.data.callId)) @@ -419,7 +419,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('PTC mode: real model writes a pr await waitForIdle(ctx, handle.agent) const events: readonly SessionEvent[] = handle.agent.session.snapshotEvents() - const dispatch = events.find(event => event.type === 'tool/code-dispatch' && event.data.name === 'read') + const dispatch = events.find(event => event.type === 'tool/ptc-dispatch' && event.data.name === 'read') const outerResult = events.find(event => event.type === 'tool/result') const workspaceContext = await vi.waitFor(() => { const splice = handle.agent.session.snapshotEvents().findLast(event => event.type === 'agent/inbox/spliced' diff --git a/apps/web/tests/ptc-round.e2e.ts b/apps/web/tests/ptc-round.e2e.ts index a5e9cf2abc..964f4922ec 100644 --- a/apps/web/tests/ptc-round.e2e.ts +++ b/apps/web/tests/ptc-round.e2e.ts @@ -68,17 +68,22 @@ describe('web e2e: PTC mode round renders nested sub-calls', () => { const calls = sessionEvents.filter(event => event.type === 'tool/call') expect(calls.length).toBeGreaterThanOrEqual(1) expect(new Set(calls.map(call => (call.data as { name: string }).name))).toEqual(new Set(['run_code'])) - const dispatches = sessionEvents.filter(event => (event.type as string) === 'tool/code-dispatch') + const starts = sessionEvents.filter(event => event.type === 'tool/ptc-dispatch-start') + const dispatches = sessionEvents.filter(event => event.type === 'tool/ptc-dispatch') expect(dispatches.length).toBeGreaterThanOrEqual(2) for (const dispatch of dispatches) { - const data = dispatch.data as unknown as { - parentCallId: string - subCallId: string - name: string - isError: boolean - content: { type: string }[] - } - expect(data.subCallId.startsWith(`${data.parentCallId}:code:`)).toBe(true) + const data = dispatch.data + expect(calls.some(call => call.data.callId === data.rootCallId)).toBe(true) + expect(data.parentCallId).toBe(data.rootCallId) + expect(starts.filter(start => start.data.subCallId === data.subCallId)).toMatchObject([{ + data: { + rootCallId: data.rootCallId, + parentCallId: data.parentCallId, + subCallId: data.subCallId, + name: data.name, + arguments: data.arguments, + }, + }]) expect(Array.isArray(data.content)).toBe(true) expect(typeof data.isError).toBe('boolean') } diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index df3203d870..999fc6bf9b 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: b22a5257631c008b3e870097000498f798161f10 -persistence-catalog.zh.md: 917b1c3d8e1a8006b6068d518200118ae4fadb05 +persistence-catalog.md: 15d0c864696d1392c668eb0619ad4d323f950f76 +persistence-catalog.zh.md: dfc04c1bbf30b913e12a981659830865416c47f0 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index b22a525763..15d0c86469 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -914,14 +914,14 @@ Types: [ToolCallId](subsystems/core.md) Source: [`packages/core/session/src/types.ts:334`](../packages/core/session/src/types.ts) - + -#### `tool/code-dispatch` — log-only +#### `tool/ptc-dispatch` — log-only ```ts persistence-catalog /** * One bridged sub-dispatch SETTLING: the pairing ids (matching the - * `tool/code-dispatch-start` with the same `subCallId`), the tool `name` + * `tool/ptc-dispatch-start` with the same `subCallId`), the tool `name` * with the same JSON-normalized `arguments`, and the sub-call's complete * model-facing outcome in `tool/result`'s own vocabulary * (`content` + `isError`), so UIs render a sub-call through the exact @@ -934,30 +934,30 @@ Source: [`packages/core/session/src/types.ts:334`](../packages/core/session/src/ * before returning), so its execution-enclosure relation holds by * construction. */ -'tool/code-dispatch': PtcDispatchEventData +'tool/ptc-dispatch': PtcDispatchEventData ``` Source: [`packages/core/tools/src/types.ts:56`](../packages/core/tools/src/types.ts) - + -#### `tool/code-dispatch-start` — log-only +#### `tool/ptc-dispatch-start` — log-only ```ts persistence-catalog /** * One sub-dispatch STARTING inside a `run_code` program: the parent - * `run_code` call id, the deterministic sub-call id (`:code:`, - * numbered in submission order), and the tool `name` with its + * `run_code` call id, the opaque sub-call id (new calls use + * `:ptc:`, numbered in submission order), and the tool `name` with its * JSON-normalized `arguments` — the exact value dispatched, normalized * BEFORE dispatch, so this append can never fail on payload shape. * Appended when the scheduler actually starts the call (not at * submission), so a start means the tool body pipeline was entered; a * call abandoned in the queue logs nothing. Log-only: `deriveMessages()` * ignores it; UIs use it for live per-sub-call running state and pair it - * with `tool/code-dispatch` by `subCallId` (timing = the two events' + * with `tool/ptc-dispatch` by `subCallId` (timing = the two events' * `time` fields). */ -'tool/code-dispatch-start': PtcDispatchStartEventData +'tool/ptc-dispatch-start': PtcDispatchStartEventData ``` Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types.ts) diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 917b1c3d8e..dfc04c1bbf 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -916,14 +916,14 @@ export type SessionEvent = { 来源:[`packages/core/session/src/types.ts:334`](../packages/core/session/src/types.ts) - + -#### `tool/code-dispatch` — log-only +#### `tool/ptc-dispatch` — log-only ```ts persistence-catalog /** * One bridged sub-dispatch SETTLING: the pairing ids (matching the - * `tool/code-dispatch-start` with the same `subCallId`), the tool `name` + * `tool/ptc-dispatch-start` with the same `subCallId`), the tool `name` * with the same JSON-normalized `arguments`, and the sub-call's complete * model-facing outcome in `tool/result`'s own vocabulary * (`content` + `isError`), so UIs render a sub-call through the exact @@ -936,30 +936,30 @@ export type SessionEvent = { * before returning), so its execution-enclosure relation holds by * construction. */ -'tool/code-dispatch': PtcDispatchEventData +'tool/ptc-dispatch': PtcDispatchEventData ``` 来源:[`packages/core/tools/src/types.ts:56`](../packages/core/tools/src/types.ts) - + -#### `tool/code-dispatch-start` — log-only +#### `tool/ptc-dispatch-start` — log-only ```ts persistence-catalog /** * One sub-dispatch STARTING inside a `run_code` program: the parent - * `run_code` call id, the deterministic sub-call id (`:code:`, - * numbered in submission order), and the tool `name` with its + * `run_code` call id, the opaque sub-call id (new calls use + * `:ptc:`, numbered in submission order), and the tool `name` with its * JSON-normalized `arguments` — the exact value dispatched, normalized * BEFORE dispatch, so this append can never fail on payload shape. * Appended when the scheduler actually starts the call (not at * submission), so a start means the tool body pipeline was entered; a * call abandoned in the queue logs nothing. Log-only: `deriveMessages()` * ignores it; UIs use it for live per-sub-call running state and pair it - * with `tool/code-dispatch` by `subCallId` (timing = the two events' + * with `tool/ptc-dispatch` by `subCallId` (timing = the two events' * `time` fields). */ -'tool/code-dispatch-start': PtcDispatchStartEventData +'tool/ptc-dispatch-start': PtcDispatchStartEventData ``` 来源:[`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types.ts) diff --git a/docs/subsystems/tools.i18n.yaml b/docs/subsystems/tools.i18n.yaml index d448297059..fd980125fb 100644 --- a/docs/subsystems/tools.i18n.yaml +++ b/docs/subsystems/tools.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/tools.md -tools.md: 9939e8ab9fff9fa5bd23fd370e07f6296a608824 -tools.zh.md: 52e812a35d6932a5ed00a86a3d3fe71460b26cd4 +tools.md: 2a57d21d628935ff08bcb6032e5750f031f7c020 +tools.zh.md: 5d29824969b01a04356dfc67832ca20a6790a1dc diff --git a/docs/subsystems/tools.md b/docs/subsystems/tools.md index 9939e8ab9f..2a57d21d62 100644 --- a/docs/subsystems/tools.md +++ b/docs/subsystems/tools.md @@ -262,14 +262,14 @@ PTC mode's bridge additionally exposes each settled sub-dispatch to the `tools/p * copy a listener may reshape. `content` is the RENDERED result projection * (what a native `tool/result` would carry) — the program itself received * the structured `value` (or just the error message on failure); only the - * `tool/code-dispatch` event's copy changes. + * `tool/ptc-dispatch` event's copy changes. */ interface PtcDispatchLog { /** The outer `run_code` execution. */ readonly exec: ToolExecution /** The calling agent (the scope routing key and the spill owner), when the outer call has one. */ readonly agent?: Agent - /** Deterministic sub-call id (`:code:`). */ + /** Opaque sub-call id; new calls use `:ptc:`. */ readonly subCallId: ToolCallId /** The dispatched sub-tool name. */ readonly name: string @@ -367,7 +367,7 @@ interface ToolExecutionFailure { type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure ``` -The result carries only the outcome. Call identity remains on the immutable `ToolExecution` that accompanies it through every hook and on the durable `tool/call` / `tool/result` session events, so wrappers cannot create a second, disagreeing identity. The canonical `value` is execution-local: the loop persists only `content`, `error`, and `meta`, while `tool/code-dispatch` stores the sub-call's rendered `content` and `isError` verbatim. Replay reproduces presentation but cannot reconstruct canonical intermediate values. +The result carries only the outcome. Call identity remains on the immutable `ToolExecution` that accompanies it through every hook and on the durable `tool/call` / `tool/result` session events, so wrappers cannot create a second, disagreeing identity. The canonical `value` is execution-local: the loop persists only `content`, `error`, and `meta`, while `tool/ptc-dispatch` stores the sub-call's rendered `content` and `isError` verbatim. Replay reproduces presentation but cannot reconstruct canonical intermediate values. On success the registry snapshots and validates the body value, freezes it, and invokes the pure renderer plus the optional top-level-call metadata projector. It separately materializes the durable presentation fields immediately before `tools/result`; an invalid value, renderer/projector failure, or non-JSON presentation becomes a JSON-safe `isError`. The final live observer therefore sees the exact execution-local value beside fields safe for the later durable append. @@ -674,13 +674,13 @@ Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index #### `tools/ptc-dispatch-log` — waterfall -Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event. `next()` keeps the content unchanged; a listener may return replacement blocks (e.g. the spill policy's preview + locator for an oversized text result). Only the logged copy is affected — the program already received the complete value, and the model sees neither. A throwing listener is contained: the bridge falls back to logging the original settled content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches. +Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/ptc-dispatch` event. `next()` keeps the content unchanged; a listener may return replacement blocks (e.g. the spill policy's preview + locator for an oversized text result). Only the logged copy is affected — the program already received the complete value, and the model sees neither. A throwing listener is contained: the bridge falls back to logging the original settled content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches. ```ts cordis-catalog /** * Allow a listener to replace content in the DURABLE LOG COPY of one * `run_code` sub-dispatch outcome before the bridge appends its - * `tool/code-dispatch` event. `next()` keeps the + * `tool/ptc-dispatch` event. `next()` keeps the * content unchanged; a listener may return replacement blocks (e.g. the * spill policy's preview + locator for an oversized text result). Only the * logged copy is affected — the program already received the complete diff --git a/docs/subsystems/tools.zh.md b/docs/subsystems/tools.zh.md index 52e812a35d..5d29824969 100644 --- a/docs/subsystems/tools.zh.md +++ b/docs/subsystems/tools.zh.md @@ -262,14 +262,14 @@ PTC mode 的桥接层还会把每个已结算的子分派暴露给 `tools/ptc-di * copy a listener may reshape. `content` is the RENDERED result projection * (what a native `tool/result` would carry) — the program itself received * the structured `value` (or just the error message on failure); only the - * `tool/code-dispatch` event's copy changes. + * `tool/ptc-dispatch` event's copy changes. */ interface PtcDispatchLog { /** The outer `run_code` execution. */ readonly exec: ToolExecution /** The calling agent (the scope routing key and the spill owner), when the outer call has one. */ readonly agent?: Agent - /** Deterministic sub-call id (`:code:`). */ + /** Opaque sub-call id; new calls use `:ptc:`. */ readonly subCallId: ToolCallId /** The dispatched sub-tool name. */ readonly name: string @@ -367,7 +367,7 @@ interface ToolExecutionFailure { type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure ``` -结果仅承载产出。调用身份保留在不可变的 `ToolExecution` 上,后者伴随结果经过每个钩子,并出现在持久化的 `tool/call` / `tool/result` 会话事件上,因此包装层无法创建第二个相互矛盾的身份。规范的 `value` 仅存在于执行期间:循环只持久化 `content`、`error` 和 `meta`,`tool/code-dispatch` 则原样存储子调用渲染后的 `content` 与 `isError`。回放可以重现展示,却无法重建规范的中间值。 +结果仅承载产出。调用身份保留在不可变的 `ToolExecution` 上,后者伴随结果经过每个钩子,并出现在持久化的 `tool/call` / `tool/result` 会话事件上,因此包装层无法创建第二个相互矛盾的身份。规范的 `value` 仅存在于执行期间:循环只持久化 `content`、`error` 和 `meta`,`tool/ptc-dispatch` 则原样存储子调用渲染后的 `content` 与 `isError`。回放可以重现展示,却无法重建规范的中间值。 成功时,注册表会快照并校验函数体返回值,将其冻结,然后调用纯渲染器;对于直接的外层调用,还会调用可选的元数据投影器。注册表会在 `tools/result` 之前另行物化持久展示字段;无效值、渲染器/投影器失败或非 JSON 展示都会转为 JSON 安全的 `isError`。因此,最终实时观察者能看到精确的执行期值,以及可安全用于后续持久追加的字段。 @@ -674,13 +674,13 @@ Source: [`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index #### `tools/ptc-dispatch-log` — waterfall -Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event. `next()` keeps the content unchanged; a listener may return replacement blocks (e.g. the spill policy's preview + locator for an oversized text result). Only the logged copy is affected — the program already received the complete value, and the model sees neither. A throwing listener is contained: the bridge falls back to logging the original settled content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches. +Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/ptc-dispatch` event. `next()` keeps the content unchanged; a listener may return replacement blocks (e.g. the spill policy's preview + locator for an oversized text result). Only the logged copy is affected — the program already received the complete value, and the model sees neither. A throwing listener is contained: the bridge falls back to logging the original settled content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's dispatches. ```ts cordis-catalog /** * Allow a listener to replace content in the DURABLE LOG COPY of one * `run_code` sub-dispatch outcome before the bridge appends its - * `tool/code-dispatch` event. `next()` keeps the + * `tool/ptc-dispatch` event. `next()` keeps the * content unchanged; a listener may return replacement blocks (e.g. the * spill policy's preview + locator for an oversized text result). Only the * logged copy is affected — the program already received the complete diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 4bb43418d9..16319731c8 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/tool-catalog.md -tool-catalog.md: 1a98079dcf3d30696cf2dae2895372a90377204d -tool-catalog.zh.md: 41b5f6d4bc8b0e666e74dde5b4bb43bd0064da8e +tool-catalog.md: c85201523e66408e6aa536de1f141df650055265 +tool-catalog.zh.md: ce5f5b57f6ac6f6a3db6a1503967fc7ceb7c48fd diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 1a98079dcf..c85201523e 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -16,7 +16,7 @@ This table connects model-visible tool names to the plugin package and service s | Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Deployment note | | --- | --- | --- | --- | --- | --- | | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after a UI/provider answers the question` | - | ask_user_question pauses the tool call until the active UI provider returns a human answer. | -| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/code-dispatch-start + tool/code-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` / `mode: both` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. | +| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` / `mode: both` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. | | `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.userQuestions (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. | | `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.jobs` runtime and is collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled. | | `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs at call time for run_in_background` | `tool/call`, `tool/result` | - | The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.shell`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.jobs` runtime and are collected/stopped through the `job_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-shell-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables. | diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 41b5f6d4bc..ce5f5b57f6 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -20,7 +20,7 @@ | 工具包 | 模型可见名称 | 依赖 | 写入/影响 | 随产品发布的别名 | 部署说明 | | --- | --- | --- | --- | --- | --- | | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after a UI/provider answers the question` | - | ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类答案。 | -| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`、`ctx.codeRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/code-dispatch-start + tool/code-dispatch pair per bridged sub-call`、`tool/result` | - | 在 `mode: ptc`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式(wire format)的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 | +| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`、`ctx.codeRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`、`tool/result` | - | 在 `mode: ptc`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式(wire format)的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 | | `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`、`ctx.systemPrompt`、`ctx.userQuestions (execution time, opportunistic)` | `tool/call`、`plan/mode inactive on an approved review`、`tool/result` | - | 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 | | `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | bash 工具是 bash 执行器 seam 面向模型的消费方。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;禁用 `enableRunInBackground` 配置(默认为 true)后,该参数会被完全移除。 | | `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs at call time for run_in_background` | `tool/call`、`tool/result` | - | pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费方(由 `@deepseek-ai/dsh-pwsh-local` 等 PowerShell 执行器为 `ctx.shell` 提供后端);除沙箱接口外,它逐项对应 bash 工具调用。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具收集/停止;托管的 `DSH_*` 环境来自 `@deepseek-ai/dsh-shell-env`。每次调用都在新进程中运行,不使用持久 PTY 会话。路径采用原生 `C:\...` 形式,变量采用 `$env:NAME`。 | diff --git a/docs/tool-execution-pipeline.i18n.yaml b/docs/tool-execution-pipeline.i18n.yaml index 6ff5ef0a15..5db82c4fc1 100644 --- a/docs/tool-execution-pipeline.i18n.yaml +++ b/docs/tool-execution-pipeline.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/tool-execution-pipeline.md -tool-execution-pipeline.md: a799c68a60f6782ef3bb79c52f89cbc78d762ab3 -tool-execution-pipeline.zh.md: 7ffdd34298f630c975b23f23daf875a3c4cdc84d +tool-execution-pipeline.md: f9d3d145bb3d7c271882942b4adc100f53a55290 +tool-execution-pipeline.zh.md: 3bfa6f358ee8482feaa9d26541e495b86a02e515 diff --git a/docs/tool-execution-pipeline.md b/docs/tool-execution-pipeline.md index a799c68a60..f9d3d145bb 100644 --- a/docs/tool-execution-pipeline.md +++ b/docs/tool-execution-pipeline.md @@ -17,7 +17,7 @@ flowchart TD around["tools/execute waterfall
timeout, retry, metrics (around dispatch)"] toolBody["Registered tool execute() body"] fsGate["fs/write-intent or fs/edit-intent
tool-fs mutations only"] - owned["Tool-owned session events
todo/write, fs/observed, hook/invoked, hook/result, tool/code-dispatch"] + owned["Tool-owned session events
todo/write, fs/observed, hook/invoked, hook/result, tool/ptc-dispatch"] post["tools/post-execute waterfall
accept, block, replace, add context"] normalized["Registry outer normalization
pipeline/result snapshot throws become isError"] finalize["ToolDefinition.finalizeContent
last content-only invariant"] @@ -57,6 +57,6 @@ flowchart TD allResults --> context ``` -Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition's snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. PTC mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency. +Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition's snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. PTC mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/ptc-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency. Maintenance mode: curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs. diff --git a/docs/tool-execution-pipeline.zh.md b/docs/tool-execution-pipeline.zh.md index 7ffdd34298..3bfa6f358e 100644 --- a/docs/tool-execution-pipeline.zh.md +++ b/docs/tool-execution-pipeline.zh.md @@ -19,7 +19,7 @@ flowchart TD around["tools/execute waterfall
timeout, retry, metrics (around dispatch)"] toolBody["Registered tool execute() body"] fsGate["fs/write-intent or fs/edit-intent
tool-fs mutations only"] - owned["Tool-owned session events
todo/write, fs/observed, hook/invoked, hook/result, tool/code-dispatch"] + owned["Tool-owned session events
todo/write, fs/observed, hook/invoked, hook/result, tool/ptc-dispatch"] post["tools/post-execute waterfall
accept, block, replace, add context"] normalized["Registry outer normalization
pipeline/result snapshot throws become isError"] finalize["ToolDefinition.finalizeContent
last content-only invariant"] @@ -59,6 +59,6 @@ flowchart TD allResults --> context ``` -文件系统的先读后编辑检查位于 `tool-fs` 之下,通过 `fs/*` 事件实现。通用的前置/后置 waterfall 承载钩子与审批策略;`ctx.approval` 在单调守卫之前处理询问,而不得重新排序的所有者策略仍作为已注册的守卫。超时等环绕分发关注点对 `tools/execute` 进行包装。注册表会对候选结果进行无损快照;如果快照失败,则会先将失败规范化,之后再由可见定义中已随快照固定的 `finalizeContent` 回调强制执行其同步且仅限内容的不变式。随后,`tools/result` 会观察不可变、可由 JSON 无损表示的结果。这样一来,钩子便可跨越不同工具系列,而无需让工具与某个策略服务耦合。PTC mode 会将保留的 `run_code` 传输及其序列化子调用都送入流水线;子调用携带父级 token、记录 `tool/code-dispatch`、将拒绝呈现为具有约束力的驳回,并省略 `additionalContexts`,以保持调用与结果相邻。 +文件系统的先读后编辑检查位于 `tool-fs` 之下,通过 `fs/*` 事件实现。通用的前置/后置 waterfall 承载钩子与审批策略;`ctx.approval` 在单调守卫之前处理询问,而不得重新排序的所有者策略仍作为已注册的守卫。超时等环绕分发关注点对 `tools/execute` 进行包装。注册表会对候选结果进行无损快照;如果快照失败,则会先将失败规范化,之后再由可见定义中已随快照固定的 `finalizeContent` 回调强制执行其同步且仅限内容的不变式。随后,`tools/result` 会观察不可变、可由 JSON 无损表示的结果。这样一来,钩子便可跨越不同工具系列,而无需让工具与某个策略服务耦合。PTC mode 会将保留的 `run_code` 传输及其序列化子调用都送入流水线;子调用携带父级 token、记录 `tool/ptc-dispatch`、将拒绝呈现为具有约束力的驳回,并省略 `additionalContexts`,以保持调用与结果相邻。 维护模式:英文源文件包含人工维护的 Mermaid 流程图,并由生成器写出;本中文文件作为经评审对侧通过双语配对维护。确切的工具 schema 与事件签名位于生成的目录中。 diff --git a/packages/api/session-controller/tests/event-script.client.ts b/packages/api/session-controller/tests/event-script.client.ts index 59caa3de59..3fcafcc9e7 100644 --- a/packages/api/session-controller/tests/event-script.client.ts +++ b/packages/api/session-controller/tests/event-script.client.ts @@ -66,8 +66,8 @@ export const ev = { }), codeDispatchStart: (seq: SessionSeq, parentCallId: string, n: number, name: string, args: unknown): SessionEvent => at(seq, { - type: 'tool/code-dispatch-start', - data: { rootCallId: parentCallId, parentCallId, subCallId: `${parentCallId}:code:${n}`, name, arguments: args }, + type: 'tool/ptc-dispatch-start', + data: { rootCallId: parentCallId, parentCallId, subCallId: `${parentCallId}:ptc:${n}`, name, arguments: args }, }), codeDispatch: ( seq: SessionSeq, @@ -79,8 +79,8 @@ export const ev = { isError = false, ): SessionEvent => at(seq, { - type: 'tool/code-dispatch', - data: { rootCallId: parentCallId, parentCallId, subCallId: `${parentCallId}:code:${n}`, name, arguments: args, isError, content: text(body) }, + type: 'tool/ptc-dispatch', + data: { rootCallId: parentCallId, parentCallId, subCallId: `${parentCallId}:ptc:${n}`, name, arguments: args, isError, content: text(body) }, }), stepEnd: (seq: SessionSeq, turn: number, step = 0): SessionEvent => at(seq, { type: 'step/end', data: { turn, step } }), diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index 66224dd6f4..b3d5796a74 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -866,13 +866,13 @@ function buildAlphaLog(): SessionEvent[] { push({ type: 'tool/call', data: { turn, step: 0, callId, name: 'run_code', arguments: args } }) const dispatchPair = (n: number, name: string, dispatchArgs: Record, resultText: string, isError = false): void => { push({ - type: 'tool/code-dispatch-start', - data: { rootCallId: callId, parentCallId: callId, subCallId: `${callId}:code:${n}`, name, arguments: dispatchArgs }, + type: 'tool/ptc-dispatch-start', + data: { rootCallId: callId, parentCallId: callId, subCallId: `${callId}:ptc:${n}`, name, arguments: dispatchArgs }, }) push({ - type: 'tool/code-dispatch', + type: 'tool/ptc-dispatch', data: { - rootCallId: callId, parentCallId: callId, subCallId: `${callId}:code:${n}`, name, + rootCallId: callId, parentCallId: callId, subCallId: `${callId}:ptc:${n}`, name, arguments: dispatchArgs, isError, content: [{ type: 'text', text: resultText }], }, }) diff --git a/packages/client/ui-chat/src/client/conversation-nodes/tool.ts b/packages/client/ui-chat/src/client/conversation-nodes/tool.ts index acfcb4bca8..15f93b1465 100644 --- a/packages/client/ui-chat/src/client/conversation-nodes/tool.ts +++ b/packages/client/ui-chat/src/client/conversation-nodes/tool.ts @@ -139,13 +139,13 @@ function acceptsEdge(state: ToolState, parent: string, child: string): boolean { function updateDispatch(state: ToolState, match: ConversationMatch): ToolState { const event = match.event - if (event.type !== 'tool/code-dispatch-start' && event.type !== 'tool/code-dispatch') return state + if (event.type !== 'tool/ptc-dispatch-start' && event.type !== 'tool/ptc-dispatch') return state const data = event.data const parentCallId = String(data.parentCallId) const subCallId = String(data.subCallId) const siblings = state.children.get(parentCallId) ?? [] const index = siblings.findIndex(candidate => candidate.callId === subCallId) - if (event.type === 'tool/code-dispatch-start') { + if (event.type === 'tool/ptc-dispatch-start') { if (index >= 0 || !acceptsEdge(state, parentCallId, subCallId)) return state const children = new Map(state.children) children.set(parentCallId, [...siblings, childCall(match, data)]) @@ -227,7 +227,7 @@ function fallbackState(context: ConversationNodeContext): ToolState | return state } -/** Root Tool lifecycle and nested Code Dispatch Definition. */ +/** Root Tool lifecycle and nested PTC dispatch Definition. */ export const toolDefinition: ConversationNodeDefinition = { kind: 'tool-call', target: 'chat', @@ -236,7 +236,7 @@ export const toolDefinition: ConversationNodeDefinition = { if (event.type === 'tool/result' && isAppendSurfaceEvent(event)) { return { id: String(event.data.message.source.callId), role: 'update' } } - if (event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch') { + if (event.type === 'tool/ptc-dispatch-start' || event.type === 'tool/ptc-dispatch') { const rootCallId: unknown = event.data.rootCallId return typeof rootCallId === 'string' && rootCallId !== '' ? { id: rootCallId, role: 'update' } diff --git a/packages/client/ui-chat/src/client/model/tool-call-tree.ts b/packages/client/ui-chat/src/client/model/tool-call-tree.ts index 878a78e63a..8426873941 100644 --- a/packages/client/ui-chat/src/client/model/tool-call-tree.ts +++ b/packages/client/ui-chat/src/client/model/tool-call-tree.ts @@ -22,7 +22,7 @@ function sameReferences( } /** - * Owns Code Dispatch pairing and projects its private parent index into the + * Owns PTC dispatch pairing and projects its private parent index into the * recursive Tool call contract exposed by conversation snapshots. */ export class ToolCallTree { @@ -50,12 +50,12 @@ export class ToolCallTree { } /** - * Fold one event when it belongs to the Code Dispatch lifecycle. + * Fold one event when it belongs to the PTC dispatch lifecycle. * @param event - Session event from the current live or history window. * @returns Whether the event was consumed as a child-call lifecycle event. */ apply(event: SessionEvent): boolean { - if (event.type === 'tool/code-dispatch-start') { + if (event.type === 'tool/ptc-dispatch-start') { const data = event.data const running: RunningToolCall = { callId: data.subCallId, @@ -73,7 +73,7 @@ export class ToolCallTree { this.revision++ return true } - if (event.type !== 'tool/code-dispatch') return false + if (event.type !== 'tool/ptc-dispatch') return false const data = event.data const siblings = this.childrenByParent.get(data.parentCallId) ?? [] const at = siblings.findIndex(sub => sub.callId === data.subCallId) diff --git a/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts b/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts index ee3235f006..a8770b8900 100644 --- a/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts +++ b/packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts @@ -1117,14 +1117,14 @@ describe('built-in conversation node Definitions', () => { }) const history = assembler([ - at(14, 'tool/code-dispatch-start', { + at(14, 'tool/ptc-dispatch-start', { rootCallId: 'history-root', parentCallId: 'history-root', subCallId: 'child', name: 'read', arguments: { path: 'README.md' }, }), - at(15, 'tool/code-dispatch', { + at(15, 'tool/ptc-dispatch', { rootCallId: 'history-root', parentCallId: 'history-root', subCallId: 'child', @@ -1164,7 +1164,7 @@ describe('built-in conversation node Definitions', () => { ]) const firstChild = (after?.data as ToolChatData).root.subCalls[0] - history.append(at(17, 'tool/code-dispatch-start', { + history.append(at(17, 'tool/ptc-dispatch-start', { rootCallId: 'history-root', parentCallId: 'history-root', subCallId: 'second-child', @@ -1176,6 +1176,55 @@ describe('built-in conversation node Definitions', () => { expect((withSecondChild?.data as ToolChatData).root.subCalls[0]).toBe(firstChild) }) + it('joins mixed historical and current subcall IDs by explicit fields through replay', () => { + const historicalId = 'other-root:code:1' + const currentId = 'other-root:ptc:2' + const historical = { + rootCallId: 'root', parentCallId: 'root', subCallId: historicalId, + name: 'run_code', arguments: {}, + } + const current = { + rootCallId: 'root', parentCallId: historicalId, subCallId: currentId, + name: 'read', arguments: { file_path: 'README.md' }, + } + const events = [ + at(1, 'turn/start', { turn: 1 }), + at(2, 'step/start', { turn: 1, step: 1 }), + at(3, 'tool/call', { turn: 1, step: 1, callId: 'root', name: 'run_code', arguments: '{}' }), + at(4, 'tool/call', { turn: 1, step: 1, callId: 'other-root', name: 'run_code', arguments: '{}' }), + at(5, 'tool/ptc-dispatch-start', historical), + at(6, 'tool/ptc-dispatch-start', current), + at(7, 'tool/ptc-dispatch', { ...current, isError: false, content: [{ type: 'text', text: 'contents' }] }), + at(8, 'tool/ptc-dispatch', { ...historical, isError: false, content: [] }), + ] + const expected = { + callId: 'root', + subCalls: [{ + kind: 'tool-result', callId: historicalId, parentCallId: 'root', callTime: events[4]!.event.time, + subCalls: [{ + kind: 'tool-result', callId: currentId, parentCallId: historicalId, callTime: events[5]!.event.time, + content: [{ type: 'text', text: 'contents' }], subCalls: [], + }], + }], + } + const live = assembler(events.slice(0, 4)) + for (const event of events.slice(4)) live.append(event) + live.flush() + const replay = assembler(events.slice(4), true) + replay.prepend(events.slice(0, 4), false) + replay.flush() + for (const value of [live, replay]) { + const view = snapshot(value) + const roots = view.order.flatMap((key) => { + const entry = view.nodes.get(key) + return entry?.kind === 'tool-call' ? [(entry.data as ToolChatData).root] : [] + }) + expect(roots).toHaveLength(2) + expect(roots.find(root => root.callId === 'root')).toMatchObject(expected) + expect(roots.find(root => root.callId === 'other-root')?.subCalls).toEqual([]) + } + }) + it('prepends an older turn without replacing already materialized nodes', () => { const value = assembler([ at(20, 'turn/start', { turn: 2 }), @@ -2034,7 +2083,7 @@ describe('built-in conversation node Definitions', () => { expect(node(snapshot(value), 'compaction')).toBeUndefined() }) - it('ignores legacy retry and code-dispatch events without correlation ids', () => { + it('ignores legacy retry and PTC dispatch events without correlation ids', () => { const value = assembler([ at(10, 'llm/retry', { turn: 1, @@ -2059,13 +2108,13 @@ describe('built-in conversation node Definitions', () => { delayMs: 10, failure: { code: 'TRANSPORT', message: 'second legacy retry' }, }), - at(30, 'tool/code-dispatch-start', { + at(30, 'tool/ptc-dispatch-start', { parentCallId: 'root', subCallId: 'child', name: 'legacy-subcall', arguments: {}, }), - at(31, 'tool/code-dispatch', { + at(31, 'tool/ptc-dispatch', { parentCallId: 'root', subCallId: 'child', name: 'legacy-subcall', @@ -2210,10 +2259,10 @@ describe('built-in conversation node Definitions', () => { it('preserves nested Tools and manual compaction evidence when their start events are outside the window', () => { const value = assembler([ - at(12, 'tool/code-dispatch-start', { + at(12, 'tool/ptc-dispatch-start', { rootCallId: 'root', parentCallId: 'root', subCallId: 'child', name: 'read_file', arguments: { path: 'a' }, }), - at(13, 'tool/code-dispatch', { + at(13, 'tool/ptc-dispatch', { rootCallId: 'root', parentCallId: 'root', subCallId: 'child', name: 'read_file', arguments: { path: 'a' }, isError: false, content: [{ type: 'text', text: 'child result' }], }), diff --git a/packages/client/ui-chat/tests/tool-call-tree.client.spec.ts b/packages/client/ui-chat/tests/tool-call-tree.client.spec.ts index d53c34e69c..fbfbcb1c47 100644 --- a/packages/client/ui-chat/tests/tool-call-tree.client.spec.ts +++ b/packages/client/ui-chat/tests/tool-call-tree.client.spec.ts @@ -9,12 +9,12 @@ const at = (seq: number, type: string, data: Record): SessionEv ({ seq, time: 1_700_000_000_000 + seq, type, data }) as unknown as SessionEvent const start = (seq: number, parentCallId: string, subCallId: string): SessionEvent => - at(seq, 'tool/code-dispatch-start', { + at(seq, 'tool/ptc-dispatch-start', { parentCallId, subCallId, name: 'run_code', arguments: {}, }) const settle = (seq: number, parentCallId: string, subCallId: string): SessionEvent => - at(seq, 'tool/code-dispatch', { + at(seq, 'tool/ptc-dispatch', { parentCallId, subCallId, name: 'run_code', arguments: {}, isError: false, content: [], }) diff --git a/packages/client/ui-conversation/tests/conversation-assembler.client.spec.ts b/packages/client/ui-conversation/tests/conversation-assembler.client.spec.ts index 15ad36aec2..1b12d7f1a0 100644 --- a/packages/client/ui-conversation/tests/conversation-assembler.client.spec.ts +++ b/packages/client/ui-conversation/tests/conversation-assembler.client.spec.ts @@ -1291,7 +1291,7 @@ describe('ConversationNodeAssembler', () => { it('carries explicit coordinates across coordinate-free events in a partial window and live tail', () => { const definition: ConversationNodeDefinition = { kind: 'location-probe', - match: event => (event.type as string) === 'tool/code-dispatch-start' + match: event => (event.type as string) === 'tool/ptc-dispatch-start' ? { id: String(event.seq), role: 'start' } : null, start: () => null, @@ -1310,11 +1310,11 @@ describe('ConversationNodeAssembler', () => { ) assembler.replaceWindow([ input(at(SessionSeq(10), 'tool/call', { turn: 2, step: 3, callId: 'root', name: 'x', arguments: '{}' })), - input(at(SessionSeq(11), 'tool/code-dispatch-start', { rootCallId: 'root', subCallId: 'a' })), + input(at(SessionSeq(11), 'tool/ptc-dispatch-start', { rootCallId: 'root', subCallId: 'a' })), ], true) assembler.flush() - assembler.append(input(at(SessionSeq(12), 'tool/code-dispatch-start', { rootCallId: 'root', subCallId: 'b' }))) + assembler.append(input(at(SessionSeq(12), 'tool/ptc-dispatch-start', { rootCallId: 'root', subCallId: 'b' }))) assembler.flush() expect([...testSnapshot(assembler)?.nodes.values() ?? []].map(value => value.data)) diff --git a/packages/client/ui-tool/tests/spill-policy-terminal.client.spec.ts b/packages/client/ui-tool/tests/spill-policy-terminal.client.spec.ts index 02403944a1..0db57e2139 100644 --- a/packages/client/ui-tool/tests/spill-policy-terminal.client.spec.ts +++ b/packages/client/ui-tool/tests/spill-policy-terminal.client.spec.ts @@ -70,7 +70,7 @@ async function executeShell(text: string, nested: boolean, name = 'bash', maxInl let block: ToolResultNode if (nested) { expect(result.value).toMatchObject({ result: true }) - const dispatches = session.snapshotEvents().filter(event => event.type === 'tool/code-dispatch') + const dispatches = session.snapshotEvents().filter(event => event.type === 'tool/ptc-dispatch') expect(dispatches).toHaveLength(1) const event = dispatches[0]! block = { diff --git a/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx b/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx index 4cbd33e0d1..cdb63394ff 100644 --- a/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx +++ b/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx @@ -66,10 +66,10 @@ describe('ToolCallTree', () => { expect(view.getByText('w1')).toBeTruthy() }) - it('recursively renders a selected leaf without selecting its ancestors', () => { + it('renders a selected current-ID leaf under its historical-ID parent without selecting ancestors', () => { const owners: ToolCallOwnerProps[] = [] const leaf = { - ...root('parent:code:1:code:1', { name: 'read', argsRaw: '{"path":"a.ts"}' }), + ...root('unrelated:ptc:7', { name: 'read', argsRaw: '{"path":"a.ts"}' }), parentCallId: 'parent:code:1', } const child = { @@ -87,12 +87,12 @@ describe('ToolCallTree', () => { expect(nests[1]?.parentElement).toBe(view.container.querySelector('[data-chat-call-id="parent:code:1"]')) expect(view.container.querySelector('[data-chat-call-id="parent"]')?.hasAttribute('data-selected')).toBe(false) expect(view.container.querySelector('[data-chat-call-id="parent:code:1"]')?.hasAttribute('data-selected')).toBe(false) - expect(view.container.querySelector('[data-chat-call-id="parent:code:1:code:1"]')?.getAttribute('data-selected')).toBe('true') + expect(view.container.querySelector('[data-chat-call-id="unrelated:ptc:7"]')?.getAttribute('data-selected')).toBe('true') expect(nests).toHaveLength(2) expect(owners.map(owner => [owner.callId, owner.block.parentCallId ?? null])).toEqual([ ['parent', null], ['parent:code:1', 'parent'], - ['parent:code:1:code:1', 'parent:code:1'], + ['unrelated:ptc:7', 'parent:code:1'], ]) }) diff --git a/packages/client/ui-trajectory/src/client/trajectory-tool-definition.ts b/packages/client/ui-trajectory/src/client/trajectory-tool-definition.ts index fb4ca15abc..b4c430def5 100644 --- a/packages/client/ui-trajectory/src/client/trajectory-tool-definition.ts +++ b/packages/client/ui-trajectory/src/client/trajectory-tool-definition.ts @@ -132,17 +132,17 @@ function acceptsEdge(state: ToolState, parent: string, child: string): boolean { function updateDispatch(state: ToolState, match: ConversationMatch): ToolState { const event = match.event - if (event.type !== 'tool/code-dispatch-start' && event.type !== 'tool/code-dispatch') return state + if (event.type !== 'tool/ptc-dispatch-start' && event.type !== 'tool/ptc-dispatch') return state const data = event.data const parentId = String(data.parentCallId) const childId = String(data.subCallId) const siblings = state.children.get(parentId) ?? [] const index = siblings.indexOf(childId) if (index < 0 && !acceptsEdge(state, parentId, childId)) return state - if (event.type === 'tool/code-dispatch-start' && index >= 0) return state + if (event.type === 'tool/ptc-dispatch-start' && index >= 0) return state const calls = new Map(state.calls) - calls.set(childId, event.type === 'tool/code-dispatch-start' + calls.set(childId, event.type === 'tool/ptc-dispatch-start' ? childCall(match, data) : childResult(match, data, calls.get(childId))) if (index >= 0) return { ...state, calls } @@ -210,7 +210,7 @@ function fallbackState(context: ConversationNodeContext): ToolState | return state } -/** Trajectory-owned root Tool lifecycle with nested Code Dispatch calls. */ +/** Trajectory-owned root Tool lifecycle with nested PTC dispatch calls. */ const trajectoryToolDefinition: ConversationNodeDefinition = { kind: 'trajectory-tool-call', target: 'trajectory', @@ -219,7 +219,7 @@ const trajectoryToolDefinition: ConversationNodeDefinition = { if (event.type === 'tool/result') { return { id: String(event.data.message.source.callId), role: 'update' } } - if (event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch') { + if (event.type === 'tool/ptc-dispatch-start' || event.type === 'tool/ptc-dispatch') { const rootCallId: unknown = event.data.rootCallId return typeof rootCallId === 'string' && rootCallId !== '' ? { id: rootCallId, role: 'update' } diff --git a/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts b/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts index 39e3b31a57..9b7c6b606c 100644 --- a/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts +++ b/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts @@ -399,7 +399,7 @@ describe('Trajectory conversation Definitions', () => { }]) }) - it('keeps parallel roots, raw Tool facts, and nested Code Dispatch results', () => { + it('keeps parallel roots, raw Tool facts, and mixed-ID PTC dispatch results', () => { const current = snapshot(assembler([ at(1, 'turn/start', { turn: 1 }), at(2, 'step/start', { turn: 1, step: 1 }), @@ -409,22 +409,31 @@ describe('Trajectory conversation Definitions', () => { at(4, 'tool/call', { turn: 1, step: 1, callId: 'root-b', name: 'parallel', arguments: '{}', }), - at(5, 'tool/code-dispatch-start', { + at(5, 'tool/ptc-dispatch-start', { rootCallId: 'root-a', parentCallId: 'root-a', - subCallId: 'child', + subCallId: 'root-b:code:1', name: 'read', arguments: { path: 'README.md' }, }), - at(6, 'tool/code-dispatch', { + at(6, 'tool/ptc-dispatch', { rootCallId: 'root-a', parentCallId: 'root-a', - subCallId: 'child', + subCallId: 'root-b:code:1', name: 'read', arguments: { path: 'README.md' }, content: [{ type: 'text', text: 'contents' }], }), - at(7, 'tool/result', { + at(7, 'tool/ptc-dispatch-start', { + rootCallId: 'root-a', parentCallId: 'root-b:code:1', subCallId: 'root-b:ptc:2', + name: 'read', arguments: { file_path: 'nested.txt' }, + }), + at(8, 'tool/ptc-dispatch', { + rootCallId: 'root-a', parentCallId: 'root-b:code:1', subCallId: 'root-b:ptc:2', + name: 'read', arguments: { file_path: 'nested.txt' }, + isError: false, content: [{ type: 'text', text: 'nested contents' }], + }), + at(9, 'tool/result', { turn: 1, step: 1, message: { @@ -441,7 +450,7 @@ describe('Trajectory conversation Definitions', () => { error: { name: 'ToolError', code: 'failed' }, meta: { presentation: 'raw' }, }, { surfaceOp: 'append' }), - at(8, 'step/end', { turn: 1, step: 1 }), + at(10, 'step/end', { turn: 1, step: 1 }), ])) const tools = current.eventNodes.filter(node => node.kind === 'tool-result') @@ -455,10 +464,15 @@ describe('Trajectory conversation Definitions', () => { error: { name: 'ToolError', code: 'failed' }, meta: { presentation: 'raw' }, subCalls: [{ - kind: 'tool-result', callId: 'child', parentCallId: 'root-a', call: { name: 'read' }, + kind: 'tool-result', callId: 'root-b:code:1', parentCallId: 'root-a', call: { name: 'read' }, + subCalls: [{ + kind: 'tool-result', callId: 'root-b:ptc:2', parentCallId: 'root-b:code:1', + callTime: 1_700_000_000_007, content: [{ type: 'text', text: 'nested contents' }], subCalls: [], + }], }], }) expect(tools.find(node => node.callId === 'root-b')).toMatchObject({ + subCalls: [], isError: true, error: { name: 'Interrupted', code: 'interrupted' }, }) diff --git a/packages/core/session/src/known-event-types.ts b/packages/core/session/src/known-event-types.ts index 95886580ba..b21b7ec576 100644 --- a/packages/core/session/src/known-event-types.ts +++ b/packages/core/session/src/known-event-types.ts @@ -67,8 +67,8 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet = new Set([ 'tool-workflow/run-end', 'tool-workflow/run-start', 'tool/call', - 'tool/code-dispatch', - 'tool/code-dispatch-start', + 'tool/ptc-dispatch', + 'tool/ptc-dispatch-start', 'tool/result', 'turn/end', 'turn/start', diff --git a/packages/core/tools/README.i18n.yaml b/packages/core/tools/README.i18n.yaml index feacbdc2e4..79f487b422 100644 --- a/packages/core/tools/README.i18n.yaml +++ b/packages/core/tools/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/tools/README.md -README.md: f586b926ec9a8e426f48b62e4d03b1661201a76a -README.zh.md: 5f422dd848ee9a6e8e0afe96d3dd03238ecc3857 +README.md: ac6133c92f99d210466e543dea2b5d0755b59168 +README.zh.md: 913c664722898dac6226f91c270438d959ecb56e diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index f586b926ec..ac6133c92f 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -124,6 +124,8 @@ Each typed invocation materializes and freezes parsed arguments, assigns an opaq Under `ptc` or `both`, the registry exposes the reserved `run_code` transport plus a deterministic SDK generated in the loaded runtime's language. Each SDK binding call re-enters the complete tool pipeline with logged correlation to the outer call, scheduled through a per-run pool that reuses the native concurrency contract. Under `ptc` alone, a model-direct call naming any other visible tool resolves to `UNKNOWN_TOOL` before policy — the announced surface and the callable surface stay the same. Intermediate binding values are execution-local; only the outer `run_code` result has a hard size cap. The [executor-collapse note](../../../.agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.md) owns the collapse contract. +New sub-calls use `:ptc:` ids. Consumers treat these ids as opaque and correlate events by exact equality; restored historical ids retain their original bytes. The [PTC mode decision](../../../.agents/notes/implemented/feature/2026-06-15-ptc.md) owns durable naming and restoration rules. + ### Extension points diff --git a/packages/core/tools/README.zh.md b/packages/core/tools/README.zh.md index 5f422dd848..913c664722 100644 --- a/packages/core/tools/README.zh.md +++ b/packages/core/tools/README.zh.md @@ -124,6 +124,8 @@ ctx.tools.register(defineTool({ 在 `ptc` 或 `both` 下,注册表公开保留的 `run_code` 传输以及按所加载运行时语言生成的确定性 SDK。每个 SDK 绑定调用都会在日志中与外层调用关联,重新进入完整工具流水线,并通过复用原生并发约定的每次运行独有池调度。在纯 `ptc` 下,模型直呼其他任何可见工具都会在策略之前解析为 `UNKNOWN_TOOL`——通告面与可调用面保持一致。中间绑定值只存在于执行局部;只有外层 `run_code` 结果有硬大小上限。[执行器塌缩 note](../../../.agents/notes/implemented/bug-fix/2026-08-07-ptc-executor-collapse.zh.md) 拥有该收束约定。 +新子调用使用 `:ptc:` 标识。消费者将这些标识视为不透明值,并通过精确相等关联事件;恢复的历史标识保留原始字节。[PTC mode 决策](../../../.agents/notes/implemented/feature/2026-06-15-ptc.zh.md) 负责持久化命名与恢复规则。 + ### 扩展点 diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 080722a254..bef11baf26 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -168,7 +168,7 @@ declare module '@deepseek-ai/cordis' { /** * Allow a listener to replace content in the DURABLE LOG COPY of one * `run_code` sub-dispatch outcome before the bridge appends its - * `tool/code-dispatch` event. `next()` keeps the + * `tool/ptc-dispatch` event. `next()` keeps the * content unchanged; a listener may return replacement blocks (e.g. the * spill policy's preview + locator for an oversized text result). Only the * logged copy is affected — the program already received the complete @@ -345,14 +345,14 @@ export type ToolExecutionMode = * copy a listener may reshape. `content` is the RENDERED result projection * (what a native `tool/result` would carry) — the program itself received * the structured `value` (or just the error message on failure); only the - * `tool/code-dispatch` event's copy changes. + * `tool/ptc-dispatch` event's copy changes. */ export interface PtcDispatchLog { /** The outer `run_code` execution. */ readonly exec: ToolExecution /** The calling agent (the scope routing key and the spill owner), when the outer call has one. */ readonly agent?: Agent - /** Deterministic sub-call id (`:code:`). */ + /** Opaque sub-call id; new calls use `:ptc:`. */ readonly subCallId: ToolCallId /** The dispatched sub-tool name. */ readonly name: string @@ -1276,7 +1276,7 @@ export class ToolRuntime extends Service { /** * Run the `tools/ptc-dispatch-log` waterfall over one settled sub-dispatch - * and return the content the bridge should log on `tool/code-dispatch`. + * and return the content the bridge should log on `tool/ptc-dispatch`. * Contained: when a listener throws, the method logs the original settled * content; that failure must not fail the dispatch or omit the settle event. Private: * the ONE consumer is the `run_code` bridge this registry constructs, which diff --git a/packages/core/tools/src/invariant.ts b/packages/core/tools/src/invariant.ts index 539fc43373..2fb5b5878c 100644 --- a/packages/core/tools/src/invariant.ts +++ b/packages/core/tools/src/invariant.ts @@ -29,13 +29,13 @@ function validateResult( } } -/** Install monotonic pipeline, final-snapshot, and code-dispatch enclosure checks. */ +/** Install monotonic pipeline, final-snapshot, and PTC dispatch enclosure checks. */ const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { const stages = new WeakMap() const openTurns = new WeakMap() const dispatchRoots = new WeakMap>() const validateDispatch = (session: Session, event: SessionEvent): void => { - if (event.type !== 'tool/code-dispatch-start' && event.type !== 'tool/code-dispatch') return + if (event.type !== 'tool/ptc-dispatch-start' && event.type !== 'tool/ptc-dispatch') return const root = String(event.data.rootCallId) const parent = String(event.data.parentCallId) const child = String(event.data.subCallId) @@ -51,7 +51,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant } } const commitDispatch = (session: Session, event: SessionEvent): void => { - if (event.type !== 'tool/code-dispatch-start' && event.type !== 'tool/code-dispatch') return + if (event.type !== 'tool/ptc-dispatch-start' && event.type !== 'tool/ptc-dispatch') return const roots = dispatchRoots.get(session) as Map roots.set(String(event.data.subCallId), String(event.data.rootCallId)) } @@ -63,7 +63,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant commitDispatch(session, event) if (event.type === 'turn/start') openTurn = event.data.turn else if (event.type === 'turn/end') openTurn = null - else if ((event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch') + else if ((event.type === 'tool/ptc-dispatch-start' || event.type === 'tool/ptc-dispatch') && openTurn === null) { fail(`${event.type} appended outside any open turn`) } @@ -85,7 +85,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant if (eventName === 'session/event') { const [session, event] = args as [Session, SessionEvent] validateDispatch(session, event) - if ((event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch') + if ((event.type === 'tool/ptc-dispatch-start' || event.type === 'tool/ptc-dispatch') && openTurnFor(session) === null) { fail(`${event.type} appended outside any open turn`) } diff --git a/packages/core/tools/src/ptc.ts b/packages/core/tools/src/ptc.ts index 8af4f87303..cc6e947997 100644 --- a/packages/core/tools/src/ptc.ts +++ b/packages/core/tools/src/ptc.ts @@ -466,7 +466,7 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO } const normalized = jsonNormalizeArgs(rawArgs) const n = ++dispatches - const subCallId = brandString(`${String(exec.callId)}:code:${n}`) + const subCallId = brandString(`${String(exec.callId)}:ptc:${n}`) const input = { callId: subCallId, rootCallId: exec.rootCallId, @@ -506,7 +506,7 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO // the log stays detached. content: result.content, }) - agent.session.append('tool/code-dispatch', { + agent.session.append('tool/ptc-dispatch', { rootCallId: exec.rootCallId, parentCallId: exec.callId, subCallId, @@ -531,7 +531,7 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO reject(new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`)) }, async start(): Promise { - exec.agent?.session.append('tool/code-dispatch-start', { + exec.agent?.session.append('tool/ptc-dispatch-start', { rootCallId: exec.rootCallId, parentCallId: exec.callId, subCallId, @@ -561,7 +561,7 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO if (!result.isError && result.content.some(block => block.type === 'image')) { exec.deferContext(createUserMessage({ content: result.content, - source: { kind: 'plugin', plugin: 'tools-code-mode' }, + source: { kind: 'plugin', plugin: 'tools-ptc' }, })) } for (const context of result.additionalContexts ?? []) { diff --git a/packages/core/tools/src/types.ts b/packages/core/tools/src/types.ts index 84052261ab..970a9cb9c0 100644 --- a/packages/core/tools/src/types.ts +++ b/packages/core/tools/src/types.ts @@ -26,21 +26,21 @@ declare module '@deepseek-ai/dsh-session/types' { interface SessionEventMap { /** * One sub-dispatch STARTING inside a `run_code` program: the parent - * `run_code` call id, the deterministic sub-call id (`:code:`, - * numbered in submission order), and the tool `name` with its + * `run_code` call id, the opaque sub-call id (new calls use + * `:ptc:`, numbered in submission order), and the tool `name` with its * JSON-normalized `arguments` — the exact value dispatched, normalized * BEFORE dispatch, so this append can never fail on payload shape. * Appended when the scheduler actually starts the call (not at * submission), so a start means the tool body pipeline was entered; a * call abandoned in the queue logs nothing. Log-only: `deriveMessages()` * ignores it; UIs use it for live per-sub-call running state and pair it - * with `tool/code-dispatch` by `subCallId` (timing = the two events' + * with `tool/ptc-dispatch` by `subCallId` (timing = the two events' * `time` fields). */ - 'tool/code-dispatch-start': PtcDispatchStartEventData + 'tool/ptc-dispatch-start': PtcDispatchStartEventData /** * One bridged sub-dispatch SETTLING: the pairing ids (matching the - * `tool/code-dispatch-start` with the same `subCallId`), the tool `name` + * `tool/ptc-dispatch-start` with the same `subCallId`), the tool `name` * with the same JSON-normalized `arguments`, and the sub-call's complete * model-facing outcome in `tool/result`'s own vocabulary * (`content` + `isError`), so UIs render a sub-call through the exact @@ -53,6 +53,6 @@ declare module '@deepseek-ai/dsh-session/types' { * before returning), so its execution-enclosure relation holds by * construction. */ - 'tool/code-dispatch': PtcDispatchEventData + 'tool/ptc-dispatch': PtcDispatchEventData } } diff --git a/packages/core/tools/tests/invariant.spec.ts b/packages/core/tools/tests/invariant.spec.ts index c68ae14073..202963379e 100644 --- a/packages/core/tools/tests/invariant.spec.ts +++ b/packages/core/tools/tests/invariant.spec.ts @@ -99,16 +99,16 @@ describe('tool-pipeline invariants', () => { name: 'echo', arguments: {}, } - expect(() => session.append('tool/code-dispatch-start', data)).toThrow(/outside any open turn/) + expect(() => session.append('tool/ptc-dispatch-start', data)).toThrow(/outside any open turn/) session.append('turn/start', { turn: 1 }) - expect(() => session.append('tool/code-dispatch-start', data)).not.toThrow() + expect(() => session.append('tool/ptc-dispatch-start', data)).not.toThrow() session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) }) it('does not commit a rejected dispatch edge into the root index', async () => { const ctx = await setup() const session = ctx.sessions.create() - expect(() => session.append('tool/code-dispatch-start', { + expect(() => session.append('tool/ptc-dispatch-start', { rootCallId: ToolCallId('rejected-root'), parentCallId: ToolCallId('rejected-root'), subCallId: ToolCallId('reused-child'), @@ -117,7 +117,7 @@ describe('tool-pipeline invariants', () => { })).toThrow(/outside any open turn/) session.append('turn/start', { turn: 1 }) - expect(() => session.append('tool/code-dispatch-start', { + expect(() => session.append('tool/ptc-dispatch-start', { rootCallId: ToolCallId('accepted-root'), parentCallId: ToolCallId('accepted-root'), subCallId: ToolCallId('reused-child'), @@ -130,14 +130,14 @@ describe('tool-pipeline invariants', () => { const ctx = await setup() const session = ctx.sessions.create() session.append('turn/start', { turn: 1 }) - session.append('tool/code-dispatch-start', { + session.append('tool/ptc-dispatch-start', { rootCallId: ToolCallId('root'), parentCallId: ToolCallId('root'), subCallId: ToolCallId('child'), name: 'run_code', arguments: {}, }) - session.append('tool/code-dispatch-start', { + session.append('tool/ptc-dispatch-start', { rootCallId: ToolCallId('root'), parentCallId: ToolCallId('child'), subCallId: ToolCallId('grandchild'), @@ -145,14 +145,14 @@ describe('tool-pipeline invariants', () => { arguments: {}, }) - expect(() => session.append('tool/code-dispatch-start', { + expect(() => session.append('tool/ptc-dispatch-start', { rootCallId: ToolCallId('another-root'), parentCallId: ToolCallId('child'), subCallId: ToolCallId('invalid-grandchild'), name: 'echo', arguments: {}, })).toThrow(/parentCallId child does not belong to rootCallId another-root/) - expect(session.snapshotEvents().some(event => event.type === 'tool/code-dispatch-start' + expect(session.snapshotEvents().some(event => event.type === 'tool/ptc-dispatch-start' && String(event.data.subCallId) === 'invalid-grandchild')).toBe(false) }) @@ -160,7 +160,7 @@ describe('tool-pipeline invariants', () => { const ctx = await setup() const session = ctx.sessions.create() session.append('turn/start', { turn: 1 }) - expect(() => session.append('tool/code-dispatch-start', { + expect(() => session.append('tool/ptc-dispatch-start', { rootCallId: ToolCallId(''), parentCallId: ToolCallId('root'), subCallId: ToolCallId('child'), @@ -168,14 +168,14 @@ describe('tool-pipeline invariants', () => { arguments: {}, })).toThrow(/must carry non-empty rootCallId/) - session.append('tool/code-dispatch-start', { + session.append('tool/ptc-dispatch-start', { rootCallId: ToolCallId('root'), parentCallId: ToolCallId('root'), subCallId: ToolCallId('child'), name: 'echo', arguments: {}, }) - expect(() => session.append('tool/code-dispatch-start', { + expect(() => session.append('tool/ptc-dispatch-start', { rootCallId: ToolCallId('other-root'), parentCallId: ToolCallId('other-root'), subCallId: ToolCallId('child'), @@ -190,7 +190,7 @@ describe('tool-pipeline invariants', () => { session.append('turn/start', { turn: 1 }) expect(() => { ctx.emit('session/event', session as never, { - type: 'tool/code-dispatch-start', + type: 'tool/ptc-dispatch-start', seq: 1, time: 1, data: { @@ -209,7 +209,7 @@ describe('tool-pipeline invariants', () => { await ctx.plugin(SessionStore) const session = ctx.sessions.create() session.append('turn/start', { turn: 1 }) - session.append('tool/code-dispatch', { + session.append('tool/ptc-dispatch', { rootCallId: ToolCallId('parent'), parentCallId: ToolCallId('parent'), subCallId: ToolCallId('child'), @@ -226,7 +226,7 @@ describe('tool-pipeline invariants', () => { it('rejects an unenclosed ptc-dispatch record on late registration', async () => { const ctx = new Context() await ctx.plugin(SessionStore) - ctx.sessions.create().append('tool/code-dispatch-start', { + ctx.sessions.create().append('tool/ptc-dispatch-start', { rootCallId: ToolCallId('parent'), parentCallId: ToolCallId('parent'), subCallId: ToolCallId('child'), diff --git a/packages/core/tools/tests/ptc.spec.ts b/packages/core/tools/tests/ptc.spec.ts index da0be57a2a..11bf14e01a 100644 --- a/packages/core/tools/tests/ptc.spec.ts +++ b/packages/core/tools/tests/ptc.spec.ts @@ -542,9 +542,9 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => { if (result.isError) throw new Error('expected success') expect(result.value).toMatchObject({ result: 'safe_read:a,safe_read:b,safe_read:c' }) // One start per dispatch, paired with its settle by subCallId, starts in submission order. - const starts = events.filter(event => event.type === 'tool/code-dispatch-start').map(event => event.data as { subCallId: string }) - const settles = events.filter(event => event.type === 'tool/code-dispatch').map(event => event.data as { subCallId: string }) - expect(starts.map(start => start.subCallId)).toEqual(['call-1:code:1', 'call-1:code:2', 'call-1:code:3']) + const starts = events.filter(event => event.type === 'tool/ptc-dispatch-start').map(event => event.data as { subCallId: string }) + const settles = events.filter(event => event.type === 'tool/ptc-dispatch').map(event => event.data as { subCallId: string }) + expect(starts.map(start => start.subCallId)).toEqual(['call-1:ptc:1', 'call-1:ptc:2', 'call-1:ptc:3']) expect(new Set(settles.map(settle => settle.subCallId))).toEqual(new Set(starts.map(start => start.subCallId))) }) @@ -656,7 +656,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => { // Both submissions are in; the second pre-execute must NOT have entered // while the first is still awaiting its policy decision. await expect.poll(() => stages.length).toBeGreaterThanOrEqual(1) - expect(stages).toEqual(['pre-enter:call-1:code:1']) + expect(stages).toEqual(['pre-enter:call-1:ptc:1']) releaseGate!() await expect.poll(() => gated.pending()).toBe(2) gated.releaseAll() @@ -666,8 +666,8 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => { const result = await runCode(ctx, 'program') expect(result.isError).toBe(false) expect(stages).toEqual([ - 'pre-enter:call-1:code:1', 'pre-exit:call-1:code:1', - 'pre-enter:call-1:code:2', 'pre-exit:call-1:code:2', + 'pre-enter:call-1:ptc:1', 'pre-exit:call-1:ptc:1', + 'pre-enter:call-1:ptc:2', 'pre-exit:call-1:ptc:2', ]) }) @@ -737,7 +737,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => { // its post-execute was mid-flight, so the native cancellation contract // replaces the successful outcome with the aborted result — the event is // still durable and in-turn, which is the invariant under test. - const settles = events.filter(event => event.type === 'tool/code-dispatch') + const settles = events.filter(event => event.type === 'tool/ptc-dispatch') expect(settles).toHaveLength(1) expect(settles[0]?.data).toMatchObject({ name: 'safe_read', isError: true }) }) @@ -772,10 +772,10 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => { const result = await runCode(ctx, 'program') expect(result.isError).toBe(false) // Post-execute observed submission order regardless of completion interleave. - expect(postOrder).toEqual(['call-1:code:1', 'call-1:code:2']) + expect(postOrder).toEqual(['call-1:ptc:1', 'call-1:ptc:2']) // Deferred contexts reach the outer result in the same order. expect(result.additionalContexts?.map(c => (c.content[0] as { text: string }).text)) - .toEqual(['ctx:call-1:code:1', 'ctx:call-1:code:2']) + .toEqual(['ctx:call-1:ptc:1', 'ctx:call-1:ptc:2']) }) it('a queued-unstarted call abandoned by run settlement logs no start event', async () => { @@ -798,13 +798,13 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => { } const result = await runCode(ctx, 'program', { agent }) expect(result.isError).toBe(true) - const starts = events.filter(event => event.type === 'tool/code-dispatch-start').map(event => (event.data as { subCallId: string }).subCallId) - const settles = events.filter(event => event.type === 'tool/code-dispatch').map(event => (event.data as { subCallId: string }).subCallId) + const starts = events.filter(event => event.type === 'tool/ptc-dispatch-start').map(event => (event.data as { subCallId: string }).subCallId) + const settles = events.filter(event => event.type === 'tool/ptc-dispatch').map(event => (event.data as { subCallId: string }).subCallId) // w1 started and settled under the abort; w2 never started and never // settled — no start event, no settle event, binding rejected with the // abandonment message at drain time. - expect(starts).toEqual(['call-1:code:1']) - expect(settles).toEqual(['call-1:code:1']) + expect(starts).toEqual(['call-1:ptc:1']) + expect(settles).toEqual(['call-1:ptc:1']) expect(abandoned).toEqual(['run_code run is over (run_code settled); writer tool call abandoned']) }) }) @@ -827,14 +827,14 @@ describe('the run_code dispatch bridge', () => { expect(result.value).toEqual({ logs: ['saw echo:one'], result: 'echo:two' }) expect(result.content).toEqual([{ type: 'text', text: 'saw echo:one\necho:two' }]) expect(calls).toEqual([{ value: 'one' }, { value: 'two' }]) - const dispatches = events.filter(event => event.type === 'tool/code-dispatch') + const dispatches = events.filter(event => event.type === 'tool/ptc-dispatch') expect(dispatches.map(event => event.data)).toEqual([ { - rootCallId: 'call-1', parentCallId: 'call-1', subCallId: 'call-1:code:1', name: 'echo', + rootCallId: 'call-1', parentCallId: 'call-1', subCallId: 'call-1:ptc:1', name: 'echo', arguments: { value: 'one' }, isError: false, content: [{ type: 'text', text: 'echo:one' }], }, { - rootCallId: 'call-1', parentCallId: 'call-1', subCallId: 'call-1:code:2', name: 'echo', + rootCallId: 'call-1', parentCallId: 'call-1', subCallId: 'call-1:ptc:2', name: 'echo', arguments: { value: 'two' }, isError: false, content: [{ type: 'text', text: 'echo:two' }], }, ]) @@ -977,7 +977,7 @@ describe('the run_code dispatch bridge', () => { } const result = await runCode(ctx, 'program', { agent }) expect(result.isError).toBe(false) - const settle = events.find(event => event.type === 'tool/code-dispatch') + const settle = events.find(event => event.type === 'tool/ptc-dispatch') expect(settle?.data).toMatchObject({ name: 'echo', isError: false, content: [{ type: 'text', text: 'echo:x' }] }) }) @@ -1007,7 +1007,7 @@ describe('the run_code dispatch bridge', () => { // skipped, yet the settle event still carries the error outcome. expect(calls).toEqual([]) expect(postExecuted).toEqual([]) - const settles = events.filter(event => event.type === 'tool/code-dispatch') + const settles = events.filter(event => event.type === 'tool/ptc-dispatch') expect(settles).toHaveLength(1) expect(settles[0]?.data).toMatchObject({ name: 'echo', isError: true }) }) @@ -1047,7 +1047,7 @@ describe('the run_code dispatch bridge', () => { const result = await runCode(ctx, 'program', { agent }) expect((result.content[0] as { text: string }).text).toContain('lossless JSON') expect(calls).toEqual([]) - expect(events.filter(event => event.type === 'tool/code-dispatch')).toEqual([]) + expect(events.filter(event => event.type === 'tool/ptc-dispatch')).toEqual([]) }) it('dispatches and logs independent snapshots of the same lossless JSON value', async () => { @@ -1061,7 +1061,7 @@ describe('the run_code dispatch bridge', () => { } await runCode(ctx, 'program', { agent }) expect(calls).toEqual([{ value: 'x', nested: ['same'] }]) - const dispatch = events.find(event => event.type === 'tool/code-dispatch')?.data as SessionEventMap['tool/code-dispatch'] + const dispatch = events.find(event => event.type === 'tool/ptc-dispatch')?.data as SessionEventMap['tool/ptc-dispatch'] expect(dispatch.arguments).toEqual({ value: 'x', nested: ['same'] }) }) @@ -1090,12 +1090,12 @@ describe('the run_code dispatch bridge', () => { expect(result.additionalContexts).toMatchObject([ { role: 'user', - content: [{ type: 'text', text: 'context for call-1:code:1' }], + content: [{ type: 'text', text: 'context for call-1:ptc:1' }], source: { kind: 'plugin', plugin: 'test' }, }, { role: 'user', - content: [{ type: 'text', text: 'context for call-1:code:2' }], + content: [{ type: 'text', text: 'context for call-1:ptc:2' }], source: { kind: 'plugin', plugin: 'test' }, }, ]) @@ -1127,7 +1127,7 @@ describe('the run_code dispatch bridge', () => { expect(result.additionalContexts).toMatchObject([{ role: 'user', - source: { kind: 'plugin', plugin: 'tools-code-mode' }, + source: { kind: 'plugin', plugin: 'tools-ptc' }, content: [ { type: 'text', text: 'image result' }, { type: 'image', attachment: { mediaType: 'image/png', bytes: 1, width: 1, height: 1 } }, @@ -1284,7 +1284,7 @@ describe('the run_code dispatch bridge', () => { // Quiescence held: the in-flight sub-dispatch was aborted and its event // logged INSIDE the run_code execution, not after it returned. expect(sawAbort).toBe(true) - expect(events.filter(event => event.type === 'tool/code-dispatch').map(event => (event.data as { name: string }).name)).toEqual(['slow']) + expect(events.filter(event => event.type === 'tool/ptc-dispatch').map(event => (event.data as { name: string }).name)).toEqual(['slow']) }) it('runs without an owning agent: dispatches work, event logging is skipped', async () => { @@ -1407,7 +1407,7 @@ describe('the run_code dispatch bridge', () => { const result = await runCode(ctx, 'program', { agent }) expect(result.isError).toBe(false) expect((result.content[0] as { text: string }).text).toBe('mixed-value') - const dispatch = events.find(event => event.type === 'tool/code-dispatch')?.data as SessionEventMap['tool/code-dispatch'] + const dispatch = events.find(event => event.type === 'tool/ptc-dispatch')?.data as SessionEventMap['tool/ptc-dispatch'] expect(dispatch.content).toEqual([ { type: 'text', text: long }, { type: 'reasoning', text: 'hidden' }, @@ -1443,7 +1443,7 @@ describe('the run_code dispatch bridge', () => { expect(text.match(/tool arguments must be lossless JSON/g)).toHaveLength(5) // None dispatched or logged. expect(calls).toEqual([]) - expect(events.filter(event => event.type === 'tool/code-dispatch')).toEqual([]) + expect(events.filter(event => event.type === 'tool/ptc-dispatch')).toEqual([]) }) it('dispatches and durably logs binding arguments deeper than the structured-clone call stack', async () => { @@ -1484,8 +1484,8 @@ describe('the run_code dispatch bridge', () => { expect(result.isError).toBe(false) expect(result.isError ? undefined : result.value).toEqual({ logs: [], result: depth }) expect({ observedDepth, observedLeaf }).toEqual({ observedDepth: depth, observedLeaf: 'leaf' }) - const dispatch = session.snapshotEvents().find(event => event.type === 'tool/code-dispatch') - if (dispatch === undefined) throw new Error('expected a durable tool/code-dispatch event') + const dispatch = session.snapshotEvents().find(event => event.type === 'tool/ptc-dispatch') + if (dispatch === undefined) throw new Error('expected a durable tool/ptc-dispatch event') const logged = dispatch.data.arguments as { nested: JsonValue } let loggedDepth = 0 let loggedCursor = logged.nested @@ -1517,7 +1517,7 @@ describe('the run_code dispatch bridge', () => { const result = await runCode(ctx, 'program', { agent }) expect(result.isError).toBe(false) expect(mutationSucceeded).toBe(false) - const dispatch = events.find(event => event.type === 'tool/code-dispatch')?.data as SessionEventMap['tool/code-dispatch'] + const dispatch = events.find(event => event.type === 'tool/ptc-dispatch')?.data as SessionEventMap['tool/ptc-dispatch'] expect(dispatch.arguments).toEqual({ list: ['original'] }) }) @@ -1632,15 +1632,15 @@ describe('the run_code dispatch bridge', () => { expect(calls).toEqual([]) }) - it('a tool/code-dispatch event never derives a model message', () => { + it('a tool/ptc-dispatch event never derives a model message', () => { const session = Session.create(SessionId('ptc-derive')) session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) - session.append('tool/code-dispatch', { + session.append('tool/ptc-dispatch', { rootCallId: ToolCallId('p1'), parentCallId: ToolCallId('p1'), - subCallId: ToolCallId('p1:code:1'), + subCallId: ToolCallId('p1:ptc:1'), name: 'echo', arguments: { value: 'x' }, isError: false, diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 07467c578f..03cd22657d 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -3397,8 +3397,8 @@ export const EVENT_API: readonly EventApiEntry[] = [ name: 'tools/ptc-dispatch-log', mode: 'waterfall', signature: '\'tools/ptc-dispatch-log\'(this: Scoped, dispatch: PtcDispatchLog, next: () => Promise): Promise', - summary: 'Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event.', - description: 'Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/code-dispatch` event. `next()` keeps the content unchanged; a listener may return replacement blocks (e.g. the spill policy\'s preview + locator for an oversized text result). Only the logged copy is affected — the program already received the complete value, and the model sees neither. A throwing listener is contained: the bridge falls back to logging the original settled content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent\'s dispatches.', + summary: 'Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/ptc-dispatch` event.', + description: 'Allow a listener to replace content in the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bridge appends its `tool/ptc-dispatch` event. `next()` keeps the content unchanged; a listener may return replacement blocks (e.g. the spill policy\'s preview + locator for an oversized text result). Only the logged copy is affected — the program already received the complete value, and the model sees neither. A throwing listener is contained: the bridge falls back to logging the original settled content. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent\'s dispatches.', parameters: [{ name: 'dispatch', description: 'the parent execution, sub-call identity, and the settled content to log.' }], }, { diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts index 0d2dded443..2e3bd89949 100644 --- a/packages/plan/plan-mode/tests/plan-mode.spec.ts +++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts @@ -966,7 +966,7 @@ describe('exit_plan_mode', () => { question: 'Approve this plan and leave plan mode?', detail: plan, }) - expect(agent.session.snapshotEvents().find(event => event.type === 'tool/code-dispatch')?.data).toMatchObject({ + expect(agent.session.snapshotEvents().find(event => event.type === 'tool/ptc-dispatch')?.data).toMatchObject({ name: EXIT_PLAN_MODE, arguments: { plan }, isError: false, diff --git a/packages/session/session-format-catalog/tests/catalog.spec.ts b/packages/session/session-format-catalog/tests/catalog.spec.ts index 7064e562b8..657bc49131 100644 --- a/packages/session/session-format-catalog/tests/catalog.spec.ts +++ b/packages/session/session-format-catalog/tests/catalog.spec.ts @@ -1,6 +1,15 @@ import { describe, expect, it } from 'vitest' +import type { SessionFormatEvent } from '@deepseek-ai/dsh-session-format' import { sessionFormatCatalog } from '../src/index.ts' +function deepFreeze(value: T): T { + if (value !== null && typeof value === 'object') { + for (const child of Object.values(value)) deepFreeze(child) + Object.freeze(value) + } + return value +} + describe('first-party Session format catalog', () => { it('statically owns the complete adjacent v0 to v3 chain', () => { const header = { @@ -129,6 +138,104 @@ describe('first-party Session format catalog', () => { expect(JSON.stringify({ header, row })).toBe(before) }) + it.each([0, 1, 2])('migrates frozen v%i PTC records and reopens the actual current representation without rewriting IDs', (version) => { + const sourceHeader = deepFreeze({ + type: 'session', version, id: 'tools-code-mode:session', createdAt: 1, delegationDepth: 0, + ...(version === 2 ? { isSeeded: false } : {}), + }) + const message = (id: string) => ({ + id, role: 'user', source: { kind: 'plugin', plugin: 'tools-code-mode' }, + content: [{ type: 'text', text: 'tool/code-dispatch-start tools-code-mode tools-ptc' }], + }) + const oldId = 'tools-code-mode:message' + const newId = 'tools-ptc:message' + const rootId = 'tools-code-mode:root' + const childId = 'tools-code-mode:child' + const dispatch = { + rootCallId: rootId, parentCallId: rootId, subCallId: childId, name: 'read', + arguments: { text: 'tools-code-mode', nested: { type: 'tool/code-dispatch' } }, + } + const rows: SessionFormatEvent[] = deepFreeze([ + { type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } }, + { type: 'step/start', seq: 1, time: 10, data: { turn: 1, step: 1 } }, + { type: 'user/message', seq: 2, time: 11, data: message(oldId), surfaceOp: 'append' }, + { type: 'user/message', seq: 3, time: 12, data: message(newId), surfaceOp: 'append' }, + { type: 'agent/inbox/spliced', seq: 4, time: 13, data: { target: 'next-turn', start: 0, inserted: [message(oldId), message(newId)] } }, + { type: 'tool/code-dispatch-start', seq: 5, time: 14, data: dispatch }, + { type: 'tool/code-dispatch', seq: 6, time: 15, data: { ...dispatch, isError: false, content: [{ type: 'text', text: childId }] } }, + { type: 'user/message', seq: 7, time: 16, data: message('tools-code-mode:replacement'), sourceEventSeqs: [2, 3], surfaceOp: { op: 'replace', start: 2, end: 3 } }, + { type: 'step/end', seq: 8, time: 17, data: { turn: 1, step: 1 } }, + { type: 'turn/end', seq: 9, time: 17, data: { turn: 1, reason: { kind: 'completed' } } }, + ]) + const before = JSON.stringify({ sourceHeader, rows }) + const restore = sessionFormatCatalog.createRestore(sourceHeader, { recovery: 'strict', validation: 'current' }) + for (const row of rows) restore.decodeRow(row) + const artifact = restore.finish() + const renamedMessage = (id: string) => ({ ...message(id), source: { kind: 'plugin', plugin: 'tools-ptc' } }) + const expected = [ + rows[0], rows[1], + expect.objectContaining({ type: 'system/message', seq: 2 }), + { ...rows[2], seq: 3, data: renamedMessage(oldId) }, + { ...rows[3], seq: 4, data: renamedMessage(newId) }, + { ...rows[4], seq: 5, data: { target: 'next-turn', start: 0, inserted: [renamedMessage(oldId), renamedMessage(newId)] } }, + { ...rows[5], seq: 6, type: 'tool/ptc-dispatch-start' }, + { ...rows[6], seq: 7, type: 'tool/ptc-dispatch' }, + { ...rows[7], seq: 8, sourceEventSeqs: [3, 4], surfaceOp: { op: 'replace', start: 3, end: 4 }, data: renamedMessage('tools-code-mode:replacement') }, + { ...rows[8], seq: 9 }, { ...rows[9], seq: 10 }, + ] + expect(artifact).toEqual({ + header: { version: 3, id: sourceHeader.id, createdAt: 1, isSeeded: false, delegationDepth: 0 }, + inheritedEventCount: 0, events: expected, + }) + const currentHeader = deepFreeze(sessionFormatCatalog.encodeCurrentHeader(artifact.header, artifact.inheritedEventCount)) + const currentRows = deepFreeze(artifact.events.map(event => sessionFormatCatalog.encodeCurrentEvent(event))) + const encodedBefore = JSON.stringify({ currentHeader, currentRows }) + const reopened = sessionFormatCatalog.createRestore(currentHeader, { recovery: 'strict', validation: 'current' }) + for (const row of currentRows) reopened.decodeRow(row) + expect(reopened.finish()).toEqual(artifact) + expect(JSON.stringify({ sourceHeader, rows })).toBe(before) + expect(JSON.stringify({ currentHeader, currentRows })).toBe(encodedBefore) + }) + + it.each(['current', 'transformed'] as const)('rejects native v3 obsolete required tags with %s validation and retains ignorable tags', (validation) => { + const header = deepFreeze({ type: 'session', version: 3, id: 'native-ptc', createdAt: 1, isSeeded: false, delegationDepth: 0 }) + for (const type of ['tool/code-dispatch-start', 'tool/code-dispatch']) { + const required = deepFreeze({ type, seq: 0, time: 1, data: null }) + const rejected = sessionFormatCatalog.createRestore(header, { recovery: 'strict', validation }) + expect(() => { rejected.decodeRow(required) }).toThrow(/unknown event type/) + const ignorable = deepFreeze({ ...required, ignorable: true, data: { text: 'tools-code-mode', source: { kind: 'plugin', plugin: 'tools-code-mode' } } }) + const accepted = sessionFormatCatalog.createRestore(header, { recovery: 'strict', validation }) + accepted.decodeRow(ignorable) + expect(accepted.finish().events).toEqual([ignorable]) + } + }) + + it.each(['current', 'transformed'] as const)('refuses V3 dispatch tag collisions in V2 input with %s validation', (validation) => { + const header = deepFreeze({ type: 'session', version: 2, id: 'v2-collision', createdAt: 1, isSeeded: false, delegationDepth: 0 }) + for (const type of ['tool/ptc-dispatch-start', 'tool/ptc-dispatch']) { + for (const ignorable of [false, true]) { + const restore = sessionFormatCatalog.createRestore(header, { recovery: 'strict', validation }) + const row = deepFreeze({ type, seq: 0, time: 1, data: null, ...(ignorable ? { ignorable: true } : {}) }) + expect(() => { restore.decodeRow(row) }).toThrow(/format v2.*ptc-dispatch/) + } + } + }) + + it.each(['current', 'transformed'] as const)('renames V2-owned ignorable dispatch tags with %s validation', (validation) => { + const header = deepFreeze({ type: 'session', version: 2, id: 'v2-owned', createdAt: 1, isSeeded: false, delegationDepth: 0 }) + const data = { rootCallId: 'root', parentCallId: 'root', subCallId: 'child', name: 'read', arguments: {} } + const rows = deepFreeze([ + { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } }, + { type: 'tool/code-dispatch-start', seq: 1, time: 2, data, ignorable: true }, + { type: 'tool/code-dispatch', seq: 2, time: 3, data: { ...data, isError: false, content: [] }, ignorable: true }, + ]) + const restore = sessionFormatCatalog.createRestore(header, { recovery: 'strict', validation }) + for (const row of rows) restore.decodeRow(row) + expect(restore.finish().events).toEqual([ + rows[0], { ...rows[1], type: 'tool/ptc-dispatch-start' }, { ...rows[2], type: 'tool/ptc-dispatch' }, + ]) + }) + it.each(['current', 'transformed'] as const)('refuses a v3 delivery marker in v2 input (%s)', (validation) => { const header = { type: 'session', version: 2, id: 'future-delivery', createdAt: 1, isSeeded: false, delegationDepth: 0 } const restore = sessionFormatCatalog.createRestore(header, { recovery: 'strict', validation }) diff --git a/packages/session/session-format-v0-to-v1/tests/validation.spec.ts b/packages/session/session-format-v0-to-v1/tests/validation.spec.ts index 852f810164..209576c636 100644 --- a/packages/session/session-format-v0-to-v1/tests/validation.spec.ts +++ b/packages/session/session-format-v0-to-v1/tests/validation.spec.ts @@ -215,10 +215,13 @@ describe('released event and payload inventory', () => { it('has an executable valid fixture for every frozen released-v0 event type', () => { expect(Object.keys(validPayloads).sort()).toEqual([...RELEASED_V0_EVENT_TYPES].sort()) expect(RELEASED_V0_EVENT_TYPES).toHaveLength(51) - expect(RELEASED_V0_EVENT_TYPES - .filter(type => type !== 'assistant/chunk') - .every(type => KNOWN_SESSION_EVENT_TYPES.has(type))).toBe(true) - expect(KNOWN_SESSION_EVENT_TYPES.has('assistant/chunk')).toBe(false) + expect(RELEASED_V0_EVENT_TYPES.filter(type => !KNOWN_SESSION_EVENT_TYPES.has(type))).toEqual([ + 'assistant/chunk', + 'tool/code-dispatch', + 'tool/code-dispatch-start', + ]) + expect(KNOWN_SESSION_EVENT_TYPES.has('tool/ptc-dispatch')).toBe(true) + expect(KNOWN_SESSION_EVENT_TYPES.has('tool/ptc-dispatch-start')).toBe(true) for (const [type, data] of Object.entries(validPayloads)) { expect(() => { assertPayload(type, data) }, type).not.toThrow() } diff --git a/packages/session/session-format-v2-to-v3/README.i18n.yaml b/packages/session/session-format-v2-to-v3/README.i18n.yaml index 83315f3786..46b184205c 100644 --- a/packages/session/session-format-v2-to-v3/README.i18n.yaml +++ b/packages/session/session-format-v2-to-v3/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-format-v2-to-v3/README.md -README.md: 159fc9af63fd9461b1fd7ab9c819e99ec45c22b4 -README.zh.md: 4b3f1375c0ced35ee5ee947e53e8721fbeb71dab +README.md: ee612ba2d69c332960977b5a18a92589fb561b8d +README.zh.md: 4e3c623e269cec67459e9e54a03d477d93850f9d diff --git a/packages/session/session-format-v2-to-v3/README.md b/packages/session/session-format-v2-to-v3/README.md index 159fc9af63..ee612ba2d6 100644 --- a/packages/session/session-format-v2-to-v3/README.md +++ b/packages/session/session-format-v2-to-v3/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -This library restores released-v2 system prompts as protected v3 messages while preserving historical request meaning, source-event chronology, timestamps, message identities, and inherited ownership. Persistence consumes it through the static Session format catalog. It does not publish or modify durable files. +This library restores released-v2 system prompts as protected v3 messages and translates durable PTC vocabulary while preserving historical request meaning, source-event chronology, timestamps, message identities, and inherited ownership. Persistence consumes it through the static Session format catalog. It does not publish or modify durable files. ## Table of Contents @@ -37,6 +37,8 @@ const targetHeader = sessionFormatV2ToV3.migrateHeader(sourceHeader) Session metadata changes only its version to 3. The stage inserts an empty `system/message` immediately after the first `step/start`, then replaces that protected head before each changed `request/header` prompt, including clears. It removes `header.system` from every request header without moving any source event. Metadata-only logs gain no head. V3 encoding and restoration accept empty system heads and reject retired `header.system`. +The stage maps `tool/code-dispatch-start` and `tool/code-dispatch` to `tool/ptc-dispatch-start` and `tool/ptc-dispatch`. It replaces the exact `tools-code-mode` plugin attribution with `tools-ptc` in user messages, inbox insertions, and title-request messages, without rewriting IDs, tool arguments, or content. Native V3 rejects required predecessor PTC tags, including after recoverable row corruption; ignorable predecessor tags remain opaque and cannot satisfy current PTC relationships. Reserved V3 PTC tags in V2 source input are refused even when ignorable. + ----- @@ -72,11 +74,11 @@ The [validator](src/validation.ts) checks system payloads, open-step ownership, #### Token effect -No model-visible content is added or removed. +No message content is added or removed. #### KV Cache effect -The model-message prefix remains unchanged. +The stage does not change message content or model configuration. ## Known Limitations and Deferred Work @@ -84,7 +86,7 @@ The model-message prefix remains unchanged. - **No file publication** — persistence owns immutable successor publication; this package never overwrites released generations. - **Chronology-preserving inputs** — a surface event before the first step, or a changed prompt outside an open step, is refused with `SessionFormatUnsupportedMigrationError`; moving events or inventing out-of-step system messages would violate reconstruction. -- **Audited migration vocabulary** — V2 events, including log-only Assistant attempts, and the installed message-feedback additions are classified explicitly. Agent relay attribution and file attachment metadata are preserved without interpreting their identifiers or byte counts as sequence references. Unknown events, even ignorable ones, and unknown message-source or content kinds are refused during migration because sequence dependencies cannot be inferred. Native equal-version reads retain ordinary ignorable-event admission and request-header extensions; only the retired `header.system` field is prohibited. +- **Audited migration vocabulary** — V2 events, including log-only Assistant attempts, and the installed message-feedback additions are classified explicitly. Agent relay attribution and file attachment metadata are preserved without interpreting their identifiers or byte counts as sequence references. Unknown events, even ignorable ones, and unknown message-source or content kinds are refused during migration because sequence dependencies cannot be inferred. Native equal-version reads retain ordinary ignorable-event admission and request-header extensions; retired `header.system` and required predecessor PTC tags are prohibited. ### Dev Note diff --git a/packages/session/session-format-v2-to-v3/README.zh.md b/packages/session/session-format-v2-to-v3/README.zh.md index 4b3f1375c0..4e3c623e26 100644 --- a/packages/session/session-format-v2-to-v3/README.zh.md +++ b/packages/session/session-format-v2-to-v3/README.zh.md @@ -9,7 +9,7 @@ kind: "package-library" ## 概述 -本库将已发布的 v2 系统提示恢复为受保护的 v3 消息,保留历史请求含义、源事件时序、时间戳、消息身份与继承归属。持久化通过静态 Session 格式目录使用本库。本库不发布或修改持久化文件。 +本库将已发布的 v2 系统提示恢复为受保护的 v3 消息,并转换持久化 PTC 词汇,保留历史请求含义、源事件时序、时间戳、消息身份与继承归属。持久化通过静态 Session 格式目录使用本库。本库不发布或修改持久化文件。 ## 目录 @@ -37,6 +37,8 @@ const targetHeader = sessionFormatV2ToV3.migrateHeader(sourceHeader) Session 元数据仅将版本改为 3。阶段在首个 `step/start` 后立即插入空 `system/message`,随后在每次提示发生变化的 `request/header` 前替换受保护的头节点,包括清空提示。每个请求头的 `header.system` 均被移除,不移动任何源事件。仅含元数据的日志不添加头节点。V3 编码和恢复接受空系统头节点,并拒绝已退役的 `header.system`。 +阶段将 `tool/code-dispatch-start` 和 `tool/code-dispatch` 映射为 `tool/ptc-dispatch-start` 和 `tool/ptc-dispatch`。它在用户消息、收件箱插入消息与标题请求消息中,将精确匹配的 `tools-code-mode` 插件归属替换为 `tools-ptc`,不改写 ID、工具参数或内容。原生 V3 拒绝必需的前代 PTC 标签,包括出现在可恢复行损坏之后的标签;可忽略的前代标签保持不透明,不能满足当前 PTC 关系。V2 源输入中的 V3 保留 PTC 标签即使可忽略也会被拒绝。 + ----- @@ -72,11 +74,11 @@ Session 元数据仅将版本改为 3。阶段在首个 `step/start` 后立即 #### Token 影响 -不添加或删除模型可见内容。 +不添加或删除消息内容。 #### KV Cache 影响 -模型消息前缀保持不变。 +阶段不改变消息内容或模型配置。 ## 已知限制与后续工作 @@ -84,7 +86,7 @@ Session 元数据仅将版本改为 3。阶段在首个 `step/start` 后立即 - **不发布文件** — 持久化负责不可变后继代的发布;本包绝不覆盖已发布代。 - **保持时序的输入** — 首个步骤前出现表面事件,或在开放步骤之外更改提示时,以 `SessionFormatUnsupportedMigrationError` 拒绝;移动事件或虚构步骤外的系统消息会破坏重建。 -- **经过审计的迁移词汇** — 显式分类 V2 事件(包括仅日志 Assistant 尝试)与已安装的消息反馈扩展。Agent 中继归属与文件附件元数据保持不变,其标识符与字节计数不被解释为序列引用。迁移拒绝未知事件(即使标为可忽略)及未知消息来源或内容种类,因为无法推断其序列依赖。原生同版本读取保留普通可忽略事件的准入规则及请求头扩展,仅禁止已退役的 `header.system` 字段。 +- **经过审计的迁移词汇** — 显式分类 V2 事件(包括仅日志 Assistant 尝试)与已安装的消息反馈扩展。Agent 中继归属与文件附件元数据保持不变,其标识符与字节计数不被解释为序列引用。迁移拒绝未知事件(即使标为可忽略)及未知消息来源或内容种类,因为无法推断其序列依赖。原生同版本读取保留普通可忽略事件的准入规则及请求头扩展,禁止已退役的 `header.system` 与必需的前代 PTC 标签。 ### 开发备注 diff --git a/packages/session/session-format-v2-to-v3/package.json b/packages/session/session-format-v2-to-v3/package.json index 193e543eec..fe71067e79 100644 --- a/packages/session/session-format-v2-to-v3/package.json +++ b/packages/session/session-format-v2-to-v3/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-session-format-v2-to-v3", - "description": "Streaming migration of released-v2 system prompts into the v3 protected message head", + "description": "Streaming migration of released-v2 system prompts and durable PTC vocabulary into v3", "version": "0.1.3-alpha.1", "publishConfig": { "access": "public" diff --git a/packages/session/session-format-v2-to-v3/src/codec.ts b/packages/session/session-format-v2-to-v3/src/codec.ts index a9f8709e21..2460788046 100644 --- a/packages/session/session-format-v2-to-v3/src/codec.ts +++ b/packages/session/session-format-v2-to-v3/src/codec.ts @@ -4,10 +4,11 @@ import { SessionFormatError, isSessionFormatJsonObject, snapshotSessionFormatJso import type { SessionFormatCodec, SessionFormatCurrentEncoder, + SessionFormatEvent, SessionFormatHeader, } from '@deepseek-ai/dsh-session-format' import { releasedV2SessionFormatCodec } from '@deepseek-ai/dsh-session-format-v1-to-v2' -import { assertReleasedV3Header } from './validation.ts' +import { assertReleasedV3Header, assertV3EventAdmission } from './validation.ts' import { assertEvent, assertV3StructuralRow } from './payload.ts' /** Physical V3 codec with protected system-message payload admission and retired header.system refusal. */ @@ -21,7 +22,8 @@ export const releasedV3SessionFormatCodec = Object.freeze({ return { ...decoder, header: { ...decoder.header, version: 3 }, decodeRow(row, context) { - assertV3StructuralRow(row) + assertV3RowAdmission(row) + decoder.decodeRow(row, { emitEvent(event) { if (event.type === 'system/message' || event.type === 'request/header') assertEvent(event, 3) @@ -40,11 +42,22 @@ export const releasedV3SessionFormatCodec = Object.freeze({ } }, encodeEvent(event) { + assertV3EventAdmission(event) if (event.type === 'system/message' || event.type === 'request/header') assertEvent(event, 3) return releasedV2SessionFormatCodec.encodeEvent(event) }, } satisfies SessionFormatCodec & SessionFormatCurrentEncoder) +/** + * Validate owned V3 admission rules before a scanner or codec can discard a recoverable tail. + * This checks only identified structural payloads; physical provenance still belongs to decoding. + * @param row - parsed physical row, before envelope or compressed-range decoding. + */ +export function assertV3RowAdmission(row: unknown): void { + assertV3StructuralRow(row) + if (typeof row === 'object' && row !== null && !Array.isArray(row)) assertV3EventAdmission(row as SessionFormatEvent) +} + function v2PhysicalHeader(value: unknown): SessionFormatHeader { const header = snapshotSessionFormatJson(value, 'format v3 physical header') if (!isSessionFormatJsonObject(header) || header['version'] !== 3) { diff --git a/packages/session/session-format-v2-to-v3/src/index.ts b/packages/session/session-format-v2-to-v3/src/index.ts index 465177f39a..0705b23029 100644 --- a/packages/session/session-format-v2-to-v3/src/index.ts +++ b/packages/session/session-format-v2-to-v3/src/index.ts @@ -1,4 +1,4 @@ -/** Released-v2 codec and streaming system-prompt migration into the V3 protected message head. */ +/** Released-v2 codec and streaming system-prompt/PTC migration into V3. */ export { releasedV2SessionFormatCodec } from '@deepseek-ai/dsh-session-format-v1-to-v2' export * from './codec.ts' diff --git a/packages/session/session-format-v2-to-v3/src/migration.ts b/packages/session/session-format-v2-to-v3/src/migration.ts index e0bad890c4..23ca572d5c 100644 --- a/packages/session/session-format-v2-to-v3/src/migration.ts +++ b/packages/session/session-format-v2-to-v3/src/migration.ts @@ -2,7 +2,7 @@ import { createHash } from 'node:crypto' import { SessionFormatError, SessionFormatUnsupportedMigrationError, defineSessionFormatMigration, sessionFormatCount } from '@deepseek-ai/dsh-session-format' -import type { SessionFormatEvent, SessionFormatEventRun, SessionFormatJsonObject, SessionFormatMigrationContext, SessionFormatMigrationStage, SessionFormatMigrationStageInput } from '@deepseek-ai/dsh-session-format' +import type { SessionFormatEvent, SessionFormatEventRun, SessionFormatJsonObject, SessionFormatJsonValue, SessionFormatMigrationContext, SessionFormatMigrationStage, SessionFormatMigrationStageInput } from '@deepseek-ai/dsh-session-format' import { assertReleasedV2Header } from '@deepseek-ai/dsh-session-format-v1-to-v2' import { assertEvent, record, SURFACE_TYPES } from './payload.ts' import { remapEvent } from './references.ts' @@ -67,7 +67,7 @@ class ReleasedV2ToV3Stage implements SessionFormatMigrationStage { } const target = remapEvent(source, this.targetSeq, this.mapping) this.mapping.push(this.targetSeq++) - context.emitEvent(target) + context.emitEvent(renamePtcEvent(target)) if (event.type === 'step/start') { this.step = { turn: data['turn'] as number, step: data['step'] as number } if (this.head === undefined) this.emitSystem('', event, context) @@ -135,3 +135,36 @@ class ReleasedV2ToV3Stage implements SessionFormatMigrationStage { this.prompt = prompt } } + +/** Source admission precedes renaming, so these payloads have exact audited fields. */ +function renamePtcEvent(event: SessionFormatEvent): SessionFormatEvent { + switch (event.type) { + case 'tool/code-dispatch-start': + return { ...event, type: 'tool/ptc-dispatch-start' } + case 'tool/code-dispatch': + return { ...event, type: 'tool/ptc-dispatch' } + case 'user/message': { + const data = renameMessageSource(event.data as SessionFormatJsonObject) + return data === event.data ? event : { ...event, data } + } + case 'agent/inbox/spliced': + case 'session/title-llm-request': { + const data = event.data as SessionFormatJsonObject + const key = event.type === 'agent/inbox/spliced' ? 'inserted' : 'messages' + const messages = data[key] as readonly SessionFormatJsonObject[] + const renamed = messages.map(renameMessageSource) + return renamed.every((message, index) => message === messages[index]) + ? event + : { ...event, data: { ...data, [key]: renamed } } + } + default: + // Content, tool arguments, message IDs, and other payloads are not plugin attribution slots. + return event + } +} + +function renameMessageSource(message: SessionFormatJsonObject): SessionFormatJsonValue { + const source = message['source'] as SessionFormatJsonObject + if (source['kind'] !== 'plugin' || source['plugin'] !== 'tools-code-mode') return message + return { ...message, source: { ...source, plugin: 'tools-ptc' } } +} diff --git a/packages/session/session-format-v2-to-v3/src/payload.ts b/packages/session/session-format-v2-to-v3/src/payload.ts index 3b48e7a083..3620d15f0e 100644 --- a/packages/session/session-format-v2-to-v3/src/payload.ts +++ b/packages/session/session-format-v2-to-v3/src/payload.ts @@ -166,7 +166,7 @@ export function assertV3StructuralRow(value: unknown): void { if (row['type'] === 'request/header') { const data = record(row['data'], 'request/header data') if (Object.hasOwn(record(data['header'], 'request header'), 'system')) { - throw new SessionFormatError('format v3 request/header rejects retired header.system') + throw new SessionFormatUnsupportedMigrationError('format v3 request/header rejects retired header.system') } } else if (row['type'] === 'system/message') { const data = record(row['data'], 'system/message data') diff --git a/packages/session/session-format-v2-to-v3/src/validation.ts b/packages/session/session-format-v2-to-v3/src/validation.ts index d9d0fe5647..baf41affbd 100644 --- a/packages/session/session-format-v2-to-v3/src/validation.ts +++ b/packages/session/session-format-v2-to-v3/src/validation.ts @@ -1,6 +1,6 @@ /** Native V3 system-head validation with a private view for frozen non-system relationships. */ -import { SessionFormatError } from '@deepseek-ai/dsh-session-format' +import { SessionFormatError, SessionFormatUnsupportedMigrationError } from '@deepseek-ai/dsh-session-format' import type { SessionFormatArtifact, SessionFormatEvent, SessionFormatHeader } from '@deepseek-ai/dsh-session-format' import { assertReleasedV2Header, restoreReleasedV2Artifact } from '@deepseek-ai/dsh-session-format-v1-to-v2' import { assertEvent, isRepairIdentity, record, SURFACE_TYPES } from './payload.ts' @@ -69,7 +69,31 @@ export function restoreReleasedV3Artifact(artifact: SessionFormatArtifact, known return artifact } +/** + * Refuse required predecessor PTC tags without interpreting native extension payloads. + * @param event - event envelope whose type and ignorable admission markers are available. + */ +export function assertV3EventAdmission(event: SessionFormatEvent): void { + if ((event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch') + && event['ignorable'] !== true) { + throw new SessionFormatUnsupportedMigrationError( + 'format v3 contains unknown event type ' + JSON.stringify(event.type) + ' at seq ' + String(event.seq), + ) + } +} + function relationshipEvent(event: SessionFormatEvent): SessionFormatEvent { + switch (event.type) { + case 'tool/ptc-dispatch-start': + return { ...event, type: 'tool/code-dispatch-start' } + case 'tool/ptc-dispatch': + return { ...event, type: 'tool/code-dispatch' } + case 'tool/code-dispatch-start': + case 'tool/code-dispatch': + assertV3EventAdmission(event) + // Obsolete ignorable events do not participate in released PTC lifecycle validation. + return { ...event, type: 'v3/opaque-released-event' } + } if (event.type === 'system/message') { const message = record(record(event.data, 'system data')['message'], 'system message') // The frozen validator needs a surface-eligible event, not a model-visible substitute. diff --git a/packages/session/session-format-v2-to-v3/tests/migration.spec.ts b/packages/session/session-format-v2-to-v3/tests/migration.spec.ts index 81a3d7d26d..405a2661e3 100644 --- a/packages/session/session-format-v2-to-v3/tests/migration.spec.ts +++ b/packages/session/session-format-v2-to-v3/tests/migration.spec.ts @@ -5,6 +5,14 @@ import { releasedV0SessionFormatCodec, releasedV1SessionFormatCodec, sessionForm import { sessionFormatV1ToV2 } from '@deepseek-ai/dsh-session-format-v1-to-v2' import { assertReleasedV3Header, releasedV2SessionFormatCodec, releasedV3SessionFormatCodec, restoreReleasedV3Artifact, sessionFormatV2ToV3 } from '../src/index.ts' +function deepFreeze(value: T): T { + if (value !== null && typeof value === 'object') { + for (const child of Object.values(value)) deepFreeze(child) + Object.freeze(value) + } + return value +} + const header: SessionFormatHeader = { version: 2, id: 'identity', createdAt: 1, isSeeded: false, delegationDepth: 0 } const request = (system?: string) => ({ header: { config: { provider: 'mock', model: 'mock' }, ...(system === undefined ? {} : { system }) }, reason: 'initial' }) const user = (id = 'user') => ({ role: 'user', id, source: { kind: 'user' }, content: [{ type: 'text', text: id }] }) @@ -351,3 +359,142 @@ describe('native V3 codec and restorer', () => { expect(() => releasedV3SessionFormatCodec.decodeHeader(value)).toThrow(/format v3 physical/) }) }) + + +describe('composed V3 system and PTC migration', () => { + const message = (plugin: string, id = plugin) => ({ ...user(id), source: { kind: 'plugin', plugin } }) + const dispatch = { rootCallId: 'tools-code-mode:root', parentCallId: 'tools-code-mode:root', subCallId: 'tools-code-mode:child', name: 'read', arguments: { text: 'tools-code-mode', type: 'tool/code-dispatch' } } + + it('renames PTC after system insertion and remaps local references without touching tool JSON or identities', () => { + const input = dense([ + ...opening(), event('user/message', message('tools-code-mode', 'tools-code-mode:message'), 'append'), + event('request/header', request('prompt')), event('tool/code-dispatch-start', dispatch), + event('tool/code-dispatch', { ...dispatch, isError: false, content: [{ type: 'text', text: 'tool/code-dispatch tools-code-mode' }] }), + event('command/run', { commandId: 'cmd', name: 'test', source: { kind: 'user' } }), + event('command/done', { commandId: 'cmd', kind: 'success', sourceEventSeq: 5 }), + { ...event('user/message', message('tools-ptc', 'tools-ptc:message'), { op: 'replace', start: 2, end: 2 }), sourceEventSeqs: [2, 5] }, + event('request/header', request()), + ]) + deepFreeze(input) + const before = JSON.stringify(input) + const output = migrate(input) + expect(output.events.find(e => e.type === 'tool/ptc-dispatch-start')?.data).toEqual(dispatch) + const settle = output.events.find(e => e.type === 'tool/ptc-dispatch')! + expect(settle.data).toEqual(input[5]?.data) + expect(output.events.find(e => e.type === 'command/done')?.data).toMatchObject({ sourceEventSeq: settle.seq }) + const users = output.events.filter(e => e.type === 'user/message') + expect(users[0]?.data).toEqual(message('tools-ptc', 'tools-code-mode:message')) + expect(users[1]?.data).toEqual(message('tools-ptc', 'tools-ptc:message')) + expect(users[1]?.['sourceEventSeqs']).toEqual([users[0]?.seq, settle.seq]) + expect(requests(output.events, 3)).toEqual(requests(input, 2)) + expect(JSON.stringify(input)).toBe(before) + }) + + it('renames exact attribution slots in inbox and title messages but not similar plugin labels or text', () => { + const old = message('tools-code-mode') + const untouched = message('tools-code-mode-extra') + const input = [ + event('agent/inbox/spliced', { target: 'next-turn', start: 0, inserted: [old, untouched] }), + ...opening(), event('user/message', user('human'), 'append'), + event('session/title-llm-request', { titleProvider: 'mock', messageSeqs: [3], route: { provider: 'mock', model: 'mock' }, system: 'tools-code-mode', messages: [old, untouched], maxTokens: 10 }), + ] + const h = stage() + for (const e of dense(input)) h.value.transformEvent(e, h.collector) + expect((h.collector.values[0]!.data as SessionFormatJsonObject)['inserted']).toEqual([message('tools-ptc', 'tools-code-mode'), untouched]) + const title = h.collector.values.at(-1)!.data as SessionFormatJsonObject + expect(title['messages']).toEqual([message('tools-ptc', 'tools-code-mode'), untouched]) + expect(title['system']).toBe('tools-code-mode') + }) + + it.each(['decoded', 'transformed'] as const)('refuses both required and ignorable reserved PTC tags in %s sources', (sourceKind) => { + for (const type of ['tool/ptc-dispatch-start', 'tool/ptc-dispatch']) { + for (const ignorable of [false, true]) { + const value = sessionFormatV2ToV3.createStage({ + sourceHeader: header, targetHeader: { ...header, version: 3 }, sourceInheritedEventCount: 0, sourceKind, + }) + expect(() => { value.transformEvent(event(type, null, undefined), new SessionFormatEventCollector()) }).toThrow(/unclassified/) + expect(() => migrate([{ ...event(type, null), ...(ignorable ? { ignorable: true } : {}) }])).toThrow(/unclassified/) + } + } + }) + + it.each(['tool/code-dispatch-start', 'tool/code-dispatch'])('refuses retired required %s after recoverable corruption', (type) => { + const decoder = releasedV3SessionFormatCodec.createDecoder({ type: 'session', ...header, version: 3 }, 'recoverable') + const collector = new SessionFormatEventCollector() + decoder.decodeRow(null, collector) + expect(() => { decoder.decodeRow({ type, seq: 0, time: 1, data: null }, collector) }).toThrow(/unknown event type/) + expect(() => releasedV3SessionFormatCodec.encodeEvent(event(type, null))).toThrow(/unknown event type/) + }) +}) + +describe('v3 PTC event admission and relationships', () => { + const turn = { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } } + const dispatch = { + rootCallId: 'tools-code-mode:root', parentCallId: 'tools-code-mode:root', subCallId: 'tools-code-mode:child', + name: 'read', arguments: { path: 'tools-code-mode', nested: [1, { text: 'tool/code-dispatch' }] }, + } + const start = { type: 'tool/ptc-dispatch-start', seq: 1, time: 2, data: dispatch } + const settle = { + type: 'tool/ptc-dispatch', seq: 2, time: 3, + data: { ...dispatch, isError: false, content: [{ type: 'text', text: 'tools-code-mode' }] }, + } + const artifact = (events: SessionFormatEvent[]) => deepFreeze({ + header: { ...header, version: 3 }, inheritedEventCount: 0, events, + }) + + it('validates a frozen complete PTC lifecycle and returns original names, IDs, and references', () => { + const source = artifact([turn, start, settle, { type: 'turn/end', seq: 3, time: 4, data: { turn: 1, reason: { kind: 'completed' } } }]) + const before = JSON.stringify(source) + const restored = restoreReleasedV3Artifact(source, new Set(['tool/ptc-dispatch-start', 'tool/ptc-dispatch'])) + expect(restored).toBe(source) + expect(restored.events).toBe(source.events) + for (const [index, event] of source.events.entries()) expect(restored.events[index]).toBe(event) + expect(JSON.stringify(source)).toBe(before) + }) + + it('accepts an unfinished PTC start without requiring a fabricated settlement', () => { + const source = artifact([turn, start]) + expect(restoreReleasedV3Artifact(source, new Set())).toBe(source) + }) + + it('rejects an orphan PTC settlement', () => { + expect(() => restoreReleasedV3Artifact(artifact([turn, { ...settle, seq: 1 }]), new Set())).toThrow(/no unique start/) + }) + + it.each([ + { name: 'other' }, { arguments: { path: 'different' } }, { subCallId: 'other-child' }, + { parentCallId: 'missing-parent' }, { rootCallId: 'different-root' }, + ])('rejects a PTC settlement whose identity or input disagrees with its start: %j', (override) => { + expect(() => restoreReleasedV3Artifact(artifact([turn, start, { ...settle, data: { ...settle.data, ...override } }]), new Set())) + .toThrow(/does not match|no unique start|parentCallId|rootCallId/) + }) + + it.each([start, settle])('rejects $type outside an open turn', (event) => { + expect(() => restoreReleasedV3Artifact(artifact([{ ...event, seq: 0 }]), new Set())).toThrow(/outside an open turn/) + }) + + it('rejects duplicate PTC starts and settlements', () => { + expect(() => restoreReleasedV3Artifact(artifact([turn, start, { ...start, seq: 2 }]), new Set())).toThrow(/repeats subCallId/) + expect(() => restoreReleasedV3Artifact(artifact([turn, start, settle, { ...settle, seq: 3 }]), new Set())).toThrow(/no unique start/) + }) + + it.each(['tool/code-dispatch-start', 'tool/code-dispatch'])('rejects required obsolete %s even when installed', (type) => { + expect(() => restoreReleasedV3Artifact(artifact([{ type, seq: 0, time: 1, data: null }]), new Set([type]))) + .toThrow(/format v3 contains unknown event type/) + }) + + it.each(['tool/code-dispatch-start', 'tool/code-dispatch', 'external/future'])('preserves ignorable %s as opaque data outside a turn', (type) => { + const source = artifact([{ + type, seq: 0, time: -5, ignorable: true, + data: { source: { kind: 'plugin', plugin: 'tools-code-mode' }, invalidLifecycle: true, content: ['tool/code-dispatch'] }, + sourceEventSeqs: [17], surfaceOp: { unknown: ['tools-code-mode'] }, + }]) + expect(restoreReleasedV3Artifact(source, new Set())).toBe(source) + expect(source.events[0]?.type).toBe(type) + }) + + it('does not let an ignorable obsolete start satisfy a current PTC settlement', () => { + const obsolete = { ...start, type: 'tool/code-dispatch-start', ignorable: true } + expect(() => restoreReleasedV3Artifact(artifact([turn, obsolete, settle]), new Set())).toThrow(/no unique start/) + }) +}) diff --git a/packages/session/session-persistence-jsonl/README.i18n.yaml b/packages/session/session-persistence-jsonl/README.i18n.yaml index b2095d6116..1ad458bf11 100644 --- a/packages/session/session-persistence-jsonl/README.i18n.yaml +++ b/packages/session/session-persistence-jsonl/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-persistence-jsonl/README.md -README.md: cce62bf98f1e906d8800b71e215e53b96bee8044 -README.zh.md: 6bc5f574ecd7266375039858cc9974e47618cc74 +README.md: 1683e6eaf828f36019631e4cb1bde962a087819b +README.zh.md: f83b0b9f0dea8a4daf46884a700317f773f8f863 diff --git a/packages/session/session-persistence-jsonl/README.md b/packages/session/session-persistence-jsonl/README.md index cce62bf98f..1683e6eaf8 100644 --- a/packages/session/session-persistence-jsonl/README.md +++ b/packages/session/session-persistence-jsonl/README.md @@ -73,6 +73,8 @@ Session ids are injectively escaped to one safe path segment before use (no trav A session is materialized lazily: `create(header)` writes nothing and returns the owned write handle, and the handle's first `append` writes and `fsync`s the encoded header and first batch through a no-overwrite publish — so a created-but-never-appended session leaves nothing on disk unless its owner calls `handle.flush()`, which publishes one header frame without an event. Each subsequent batch appends lines or one compressed frame and `fsync`s before the append resolves; a caught write or sync failure rolls the file back to its prior length. Committed events are never rewritten. After a crash, the stored log keeps its interrupted final turn — every record in the committed prefix survives, and the resuming reader appends synthetic closers through its write handle. An incomplete final raw line is discarded. A torn final Zstandard frame contributes only its complete decoded JSONL records; a write handle truncates the torn bytes and durably rewrites those recovered records before its first new batch. Checksum, decompression, or structural failure in a complete committed frame rejects as corruption. +The current-generation scanner applies the current codec owner’s structural admission checks before recoverable-tail handling. Retired required PTC tags and `request/header.header.system` refuse the file even after an earlier malformed row; recovery never truncates them as ordinary damaged tail data. + ### Reading the logs `open(id, 'read'|'write')` selects the highest canonical generation. Current input follows the ordinary fast path. For historical input, a read open decodes and migrates the source once, validates the current logical result, and returns it without publishing a successor. A write open reuses that revision-keyed preparation when available, or performs the same preparation, then encodes a same-directory temporary file in bounded chunks, verifies it in a Worker Thread, rechecks the source revision, and publishes the current successor without overwrite before returning. The source remains byte-identical. Source drift after preparation rejects that write open without replacing the logical history already returned to readers; a later write open prepares the new revision. The backend marks decoded event graphs `shared-frozen` when it freezes them before memoization; handle reads and slices preserve that state, including empty slices. Only an unmaterialized pending log reports `detached`. `stat(id)` and `list()` select and translate only the highest generation header without reading event rows or starting migration; snapshots carry `sizeBytes` and a best-effort stat-derived revision for the selected file. With `compression: 'none'`, the log is newline-delimited text an external reader can consume directly; the compressed default must be read through the backend. diff --git a/packages/session/session-persistence-jsonl/README.zh.md b/packages/session/session-persistence-jsonl/README.zh.md index 6bc5f574ec..f83b0b9f0d 100644 --- a/packages/session/session-persistence-jsonl/README.zh.md +++ b/packages/session/session-persistence-jsonl/README.zh.md @@ -73,6 +73,8 @@ kind: "package-reference" 会话延迟实体化:`create(header)` 不写入任何内容并返回持有的写句柄,句柄的第一次 `append` 通过无覆盖发布写入并 `fsync` 编码后的 header 与第一批——因此已创建但从未 append 的会话不留下任何磁盘内容,除非其所有者调用 `handle.flush()`,以无事件的单个 header 帧发布它。后续每个批次追加行或一个压缩帧,并在 append 完成前 `fsync`;捕获到写入或同步失败时把文件回滚到之前的字节长度。已提交事件绝不重写。崩溃后,已存储日志保留被中断的最终轮次——已提交前缀中的每条记录都保留下来,由执行恢复的读方通过其写句柄追加合成 closer。不完整的最终原始行会被丢弃。撕裂的最终 Zstandard 帧只贡献其中完整解码出的 JSONL 记录;写句柄会截掉撕裂字节,并在第一次新批次之前持久重写这些恢复出的记录。完整已提交帧中的校验和、解压或结构失败以损坏拒绝。 +当前代际扫描器在处理可恢复尾部之前,执行当前编解码器所有者的结构准入检查。已退役的必需 PTC 标签与 `request/header.header.system` 即使出现在较早的畸形行之后也会导致文件被拒绝;恢复绝不将它们作为普通损坏尾部数据截断。 + ### 读取日志 `open(id, 'read'|'write')` 选择最高规范 generation。当前格式输入走普通快速路径。对于历史输入,只读 open 会单遍解码并迁移源、校验当前逻辑结果,然后在不发布后继的情况下返回。写 open 会在可用时复用按 revision 为键的 preparation,否则执行同一套 preparation,再按有界分片编码同目录临时文件、在 Worker Thread 中校验、复查源修订,并在返回前以不覆盖方式发布当前后继。源保持逐字节不变。如果源在 preparation 后发生变化,该次写 open 会失败,已经返回给读方的逻辑历史不会被替换;后续写 open 会针对新的 revision 重新执行 preparation。Backend 在 memo 化前冻结已解码的 event graph,并在此时将其标记为 `shared-frozen`;句柄读取和 slice 即使为空也保留该状态。只有尚未实体化的 pending 空日志报告 `detached`。`stat(id)` 与 `list()` 只选择并转换最高 generation 的 header,不读取事件行,也不启动迁移;快照携带所选文件的 `sizeBytes` 与尽力而为的 stat 派生修订号。选择 `compression: 'none'` 后,日志是外部读取方可直接消费的换行分隔文本;压缩默认值必须经后端读取。 diff --git a/packages/session/session-persistence-jsonl/package.json b/packages/session/session-persistence-jsonl/package.json index bd99d447c6..9950bdcc61 100644 --- a/packages/session/session-persistence-jsonl/package.json +++ b/packages/session/session-persistence-jsonl/package.json @@ -36,6 +36,7 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session-format": "workspace:^", "@deepseek-ai/dsh-session-format-catalog": "workspace:^", + "@deepseek-ai/dsh-session-format-v2-to-v3": "workspace:^", "fs-ext": "2.1.1", "koffi": "^3.1.0", "@deepseek-ai/schemastery": "workspace:^" diff --git a/packages/session/session-persistence-jsonl/src/format.ts b/packages/session/session-persistence-jsonl/src/format.ts index 93a5a7ffd5..82ab5efcc8 100644 --- a/packages/session/session-persistence-jsonl/src/format.ts +++ b/packages/session/session-persistence-jsonl/src/format.ts @@ -19,10 +19,11 @@ import type { SessionId, SessionLogOffset as SessionLogOffsetType, } from '@deepseek-ai/dsh-session' -import { parseSessionFormatLogFilename, sessionFormatLogFilename } from '@deepseek-ai/dsh-session-format' +import { parseSessionFormatLogFilename, sessionFormatLogFilename, SessionFormatUnsupportedMigrationError } from '@deepseek-ai/dsh-session-format' import type { SessionFormatEvent } from '@deepseek-ai/dsh-session-format' import type { SessionFormatRecovery, SessionFormatRestore } from '@deepseek-ai/dsh-session-format' import { sessionFormatCatalog } from '@deepseek-ai/dsh-session-format-catalog' +import { assertV3RowAdmission } from '@deepseek-ai/dsh-session-format-v2-to-v3' import { SessionFormatUnsupportedError, sessionFormatVersionRefusal, @@ -484,6 +485,15 @@ export class SessionLogScanner { return } + // This scanner accepts only current-generation files. Owned structural refusal must + // precede its recoverable-tail suppression, independently of the strict decoder state. + try { + assertV3RowAdmission(decoded) + } catch (error: unknown) { + if (error instanceof SessionFormatUnsupportedMigrationError) throw new SessionFormatUnsupportedError(error.message) + throw error + } + if (this.issue !== undefined) { if (typeof decoded === 'object' && decoded !== null && (decoded as { type?: unknown }).type === 'turn/end') throw this.issue @@ -492,6 +502,9 @@ export class SessionLogScanner { try { this.restore.decodeRow(decoded) } catch (error: unknown) { + if (error instanceof SessionFormatUnsupportedMigrationError) { + throw new SessionFormatUnsupportedError(error.message) + } /* v8 ignore next -- every production Session format decoder rejects with Error. */ const detail = error instanceof Error ? error.message : String(error) const issue = new Error(`corrupt session log: invalid committed event at line ${this.eventLine}: ${detail}`, { diff --git a/packages/session/session-persistence-jsonl/tests/jsonl.spec.ts b/packages/session/session-persistence-jsonl/tests/jsonl.spec.ts index e63bd37b02..7e7a21a0c4 100644 --- a/packages/session/session-persistence-jsonl/tests/jsonl.spec.ts +++ b/packages/session/session-persistence-jsonl/tests/jsonl.spec.ts @@ -821,18 +821,31 @@ describe('JsonlSessionPersistence: immutable format generations', () => { readTally.enabled = true const controller = new AbortController() const reason = new Error('first historical waiter cancelled') + const secondController = new AbortController() + const firstListener = vi.spyOn(controller.signal, 'addEventListener') + const secondListener = vi.spyOn(secondController.signal, 'addEventListener') const first = ctx.sessionPersistence.open(header.id, 'read', { signal: controller.signal }) - const second = ctx.sessionPersistence.open(header.id, 'read') - await pause.entered - await scheduler.yield() - controller.abort(reason) - await expect(first).rejects.toBe(reason) - pause.release() - const handle = await second - expect((await handle.read()).events).toEqual([]) - expect(readTally.bySuffix.get(sourcePath)).toBe(1) - await handle.close() + const second = ctx.sessionPersistence.open(header.id, 'read', { signal: secondController.signal }) + try { + await pause.entered + // Both callers must be waiting on preparation, not still discovering the source. + await vi.waitFor(() => { + expect(firstListener).toHaveBeenCalledWith('abort', expect.any(Function), { once: true }) + expect(secondListener).toHaveBeenCalledWith('abort', expect.any(Function), { once: true }) + }) + controller.abort(reason) + await expect(first).rejects.toBe(reason) + pause.release() + const handle = await second + expect((await handle.read()).events).toEqual([]) + expect(readTally.bySuffix.get(sourcePath)).toBe(1) + } finally { + pause.release() + for (const outcome of await Promise.allSettled([first, second])) { + if (outcome.status === 'fulfilled') await outcome.value.close() + } + } }) it('cancels shared historical preparation after its last waiter leaves', async () => { diff --git a/packages/session/session-persistence-jsonl/tests/v2-ptc-migration.spec.ts b/packages/session/session-persistence-jsonl/tests/v2-ptc-migration.spec.ts new file mode 100644 index 0000000000..e22acf4aef --- /dev/null +++ b/packages/session/session-persistence-jsonl/tests/v2-ptc-migration.spec.ts @@ -0,0 +1,206 @@ +/** Real JSONL publication and provider-neutral message preservation across the V2 PTC rename. */ + +import { Context } from '@deepseek-ai/cordis' +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import type { SessionFormatEvent } from '@deepseek-ai/dsh-session-format' +import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' +import { createHash } from 'node:crypto' +import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { generationLogPath } from '../src/format.ts' + +const userMessage = { + id: 'tools-code-mode:user', role: 'user', source: { kind: 'user' }, + content: [{ type: 'text', text: 'Keep tools-code-mode and tool/code-dispatch in this text. 图片' }], +} +const titleMessage = { + id: 'tools-code-mode:title-input', role: 'user', + source: { kind: 'plugin', plugin: 'dsh-session-title-llm' }, + content: [{ type: 'text', text: 'Generate the session title from this JSON array of human messages:\n[{"seq":2,"text":"Keep tools-code-mode and tool/code-dispatch in this text. 图片"}]' }], +} +const toolCall = { + type: 'tool-call', id: 'tools-code-mode:root-call', name: 'run_code', + arguments: '{"code":"return tools.read_image(input)","literal":"tools-code-mode"}', +} +const replayState = { + response: { id: 'tools-code-mode:response', opaque: 'tool/code-dispatch' }, + blocks: [{ signature: 'tools-code-mode:signature' }], +} +const assistantMessage = { + id: 'tools-code-mode:assistant', role: 'assistant', content: [toolCall], + source: { kind: 'model', provider: 'historical', model: 'historical-model', replayState }, +} +const toolMessage = { + id: 'tools-code-mode:result', role: 'user', + source: { kind: 'tool', callId: toolCall.id }, + content: [{ + type: 'tool-result', toolCallId: toolCall.id, isError: false, + content: [{ type: 'text', text: 'tools-code-mode: image attached; tool/code-dispatch' }], + }], +} +const imageMessage = { + id: 'tools-code-mode:root-call:image-context', role: 'user', + source: { kind: 'plugin', plugin: 'tools-code-mode', form: 'notice', summary: 'Image from tools-code-mode' }, + content: [ + { type: 'text', text: 'Image from tools-code-mode:child-call (tool/code-dispatch)' }, + { type: 'image', attachment: { + attachmentId: 'tools-code-mode:image', mediaType: 'image/png', bytes: 68, + width: 1, height: 1, name: 'tools-code-mode.png', + originalDimensions: { width: 2, height: 2 }, + } }, + ], +} +const currentImageMessage = { + ...imageMessage, source: { ...imageMessage.source, plugin: 'tools-ptc' }, +} +const dispatch = { + rootCallId: toolCall.id, parentCallId: toolCall.id, subCallId: 'tools-code-mode:child-call', + name: 'read_image', arguments: { path: 'tools-code-mode.png', literal: 'tool/code-dispatch-start' }, +} + +/** Historical rows are literal V2 data, independent of the current event-name vocabulary. */ +function releasedV2Events(): SessionFormatEvent[] { + return [ + { type: 'turn/start', seq: 0, time: 1001, data: { turn: 1 } }, + { type: 'step/start', seq: 1, time: 1002, data: { turn: 1, step: 1 } }, + { type: 'user/message', seq: 2, time: 1003, data: userMessage, surfaceOp: 'append' }, + { type: 'assistant/message', seq: 3, time: 1005, surfaceOp: 'append', data: { + turn: 1, step: 1, message: assistantMessage, + stream: [ + { type: 'tool-call-chunks', time0: 1004, index: 0, id: toolCall.id, + name: toolCall.name, dt: [], args: [toolCall.arguments] }, + { type: 'chunk', time: 1005, chunk: { type: 'finish', reason: { kind: 'tool-calls' }, replayState } }, + ], + } }, + { type: 'tool/call', seq: 4, time: 1006, data: { + turn: 1, step: 1, callId: toolCall.id, name: toolCall.name, arguments: toolCall.arguments, + } }, + { type: 'tool/code-dispatch-start', seq: 5, time: 1007, data: dispatch }, + { type: 'tool/code-dispatch', seq: 6, time: 1008, data: { + ...dispatch, isError: false, content: imageMessage.content, + } }, + { type: 'tool/result', seq: 7, time: 1009, surfaceOp: 'append', data: { + turn: 1, step: 1, message: toolMessage, + } }, + { type: 'agent/inbox/spliced', seq: 8, time: 1010, data: { + target: 'next-step', start: 0, removedCount: 0, inserted: [imageMessage], + } }, + { type: 'user/message', seq: 9, time: 1011, data: imageMessage, surfaceOp: 'append' }, + { type: 'session/title-llm-request', seq: 10, time: 1012, data: { + titleProvider: 'tools-code-mode:title', messageSeqs: [2], + route: { provider: 'historical', model: 'historical-model' }, + system: 'Keep tools-code-mode literal.', maxTokens: 32, + messages: [titleMessage], + } }, + { type: 'step/end', seq: 11, time: 1013, data: { turn: 1, step: 1 } }, + { type: 'turn/end', seq: 12, time: 1014, data: { turn: 1, reason: { kind: 'completed' } } }, + ] +} + +describe('JSONL V2 PTC publication and restore', () => { + let root: string | undefined + let ctx: Context | undefined + + beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'dsh-jsonl-v2-ptc-')) + ctx = new Context() + await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' }) + }) + + afterEach(async () => { + try { + await ctx?.fiber.dispose() + } finally { + if (root !== undefined) await rm(root, { recursive: true, force: true }) + } + }) + + it('publishes V3 without changing V2 bytes and preserves restored IDs, roles, content, and replay state', async () => { + if (root === undefined || ctx === undefined) throw new Error('persistence fixture is not initialized') + const id = SessionId('tools-code-mode:session') + const header = { type: 'session', version: 2, id, createdAt: 1000, isSeeded: false, delegationDepth: 0 } + const events = releasedV2Events() + const source = Buffer.from([header, ...events].map(row => JSON.stringify(row)).join('\n') + '\n') + const predecessor = generationLogPath(root, undefined, id, 2, 'none') + const successor = join(dirname(predecessor), 'session.v3.jsonl') + await mkdir(dirname(predecessor), { recursive: true }) + await writeFile(predecessor, source) + const sourceStat = await stat(predecessor) + + const reader = await ctx.sessionPersistence.open(id, 'read') + try { + expect(reader.header).toEqual({ version: 3, id, createdAt: 1000, isSeeded: false, delegationDepth: 0 }) + expect((await reader.read()).events.map(event => event.type)).toEqual([ + 'turn/start', 'step/start', 'system/message', 'user/message', 'assistant/message', 'tool/call', + 'tool/ptc-dispatch-start', 'tool/ptc-dispatch', 'tool/result', 'agent/inbox/spliced', + 'user/message', 'session/title-llm-request', 'step/end', 'turn/end', + ]) + } finally { + await reader.close() + } + expect(await readFile(predecessor)).toEqual(source) + expect(await stat(predecessor)).toMatchObject({ dev: sourceStat.dev, ino: sourceStat.ino }) + await expect(readFile(successor)).rejects.toMatchObject({ code: 'ENOENT' }) + + const writer = await ctx.sessionPersistence.open(id, 'write') + try { + await writer.flush() + } finally { + await writer.close() + } + const published = await readFile(successor) + const [publishedHeader, ...publishedEvents] = published.toString('utf8').trimEnd().split('\n') + .map((row): unknown => JSON.parse(row)) + expect(publishedHeader).toEqual({ ...header, version: 3 }) + const expectedEvents: SessionFormatEvent[] = events.map(event => ({ ...event, seq: event.seq < 2 ? event.seq : event.seq + 1 })) + expectedEvents[5] = { ...expectedEvents[5], type: 'tool/ptc-dispatch-start' } as SessionFormatEvent + expectedEvents[6] = { ...expectedEvents[6], type: 'tool/ptc-dispatch' } as SessionFormatEvent + expectedEvents[8] = { ...expectedEvents[8], data: { + target: 'next-step', start: 0, removedCount: 0, inserted: [currentImageMessage], + } } as SessionFormatEvent + expectedEvents[9] = { ...expectedEvents[9], data: currentImageMessage } as SessionFormatEvent + expectedEvents[10] = { ...expectedEvents[10], data: { + ...(events[10]?.data as Record), messageSeqs: [3], + } } as SessionFormatEvent + const systemMessage = { + id: 'v2-to-v3-system-' + createHash('sha256') + .update(JSON.stringify(['session-format-v2-to-v3', id, 1, 'step/start'])).digest('hex'), + role: 'system', source: { kind: 'plugin', plugin: '@deepseek-ai/dsh-system-prompt' }, content: [], + } + expectedEvents.splice(2, 0, { + type: 'system/message', seq: 2, time: 1002, surfaceOp: 'append', + data: { turn: 1, step: 1, message: systemMessage }, + }) + expect(publishedEvents).toEqual(expectedEvents) + expect(await readFile(predecessor)).toEqual(source) + expect(await stat(predecessor)).toMatchObject({ dev: sourceStat.dev, ino: sourceStat.ino }) + + await ctx.fiber.dispose() + ctx = new Context() + await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' }) + const reloaded = await ctx.sessionPersistence.open(id, 'read') + try { + const restored = await reloaded.read() + expect(restored.events).toEqual(expectedEvents) + const session = Session.fromRestore(id, restored.events, reloaded.header, reloaded.inheritedEventCount, restored.eventState) + const messages = session.deriveMessages() + expect(messages).toEqual([userMessage, assistantMessage, toolMessage, currentImageMessage]) + // Provider-neutral input evidence: no provider converter is a declared dependency here. + expect(messages.map(({ id, role, content }) => ({ id, role, content }))).toEqual( + [userMessage, assistantMessage, toolMessage, imageMessage].map(({ id, role, content }) => ({ id, role, content })), + ) + expect(messages[1]?.source).toEqual(assistantMessage.source) + expect(messages[2]?.source).toEqual({ kind: 'tool', callId: 'tools-code-mode:root-call' }) + expect(messages[3]?.source).toEqual(currentImageMessage.source) + } finally { + await reloaded.close() + } + expect(await readFile(predecessor)).toEqual(source) + expect(await stat(predecessor)).toMatchObject({ dev: sourceStat.dev, ino: sourceStat.ino }) + expect(await readFile(successor)).toEqual(published) + expect((await readdir(dirname(predecessor))).filter(name => name.endsWith('.jsonl')).sort()) + .toEqual(['session.v2.jsonl', 'session.v3.jsonl']) + }) +}) diff --git a/packages/session/session-persistence-jsonl/tests/v3-event-admission.spec.ts b/packages/session/session-persistence-jsonl/tests/v3-event-admission.spec.ts new file mode 100644 index 0000000000..99e8992a30 --- /dev/null +++ b/packages/session/session-persistence-jsonl/tests/v3-event-admission.spec.ts @@ -0,0 +1,107 @@ +import { Context } from '@deepseek-ai/cordis' +import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session' +import { SessionFormatUnsupportedError } from '@deepseek-ai/dsh-session-persistence' +import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' +import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { generationLogPath, scanLog } from '../src/format.ts' + +const id = SessionId('v3-admission') +const header = { type: 'session', version: 3, id, createdAt: 1000, isSeeded: false, delegationDepth: 0 } +const start = { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } } +const prefix = [header, start].map(row => JSON.stringify(row)).join('\n') + '\n' +const obsoleteTypes = ['tool/code-dispatch-start', 'tool/code-dispatch'] as const + +function obsoleteEvent(type: string, ignorable?: true) { + return { + type, seq: 1, time: 2, + data: { rootCallId: 'root', parentCallId: 'root', subCallId: 'child', name: 'read', arguments: {} }, + ...(ignorable ? { ignorable } : {}), + } +} + +describe('native V3 event admission at EOF', () => { + let root: string + let ctx: Context + + beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'dsh-v3-admission-')) + ctx = new Context() + await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' }) + }) + + afterEach(async () => { + try { + await ctx?.fiber.dispose() + } finally { + if (root !== undefined) await rm(root, { recursive: true, force: true }) + } + }) + + async function store(bytes: Buffer): Promise { + const path = generationLogPath(root, undefined, id, 3, 'none') + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, bytes) + return path + } + + it.each(obsoleteTypes)('scanLog refuses a complete required %s EOF row', (type) => { + const bytes = Buffer.from(prefix + JSON.stringify(obsoleteEvent(type)) + '\n') + expect(() => scanLog(bytes)).toThrow(SessionFormatUnsupportedError) + expect(() => scanLog(bytes)).toThrow('format v3 contains unknown event type') + }) + + it.each(obsoleteTypes.flatMap(type => ['', '{not json\n', 'null\n'].map(corruption => ({ type, corruption }))))( + 'scan, read and write refuse required $type after "$corruption" without changing bytes or inode', async ({ type, corruption }) => { + const bytes = Buffer.from(prefix + corruption + JSON.stringify(obsoleteEvent(type)) + '\n') + expect(() => scanLog(bytes)).toThrow(SessionFormatUnsupportedError) + expect(() => scanLog(bytes)).toThrow('format v3 contains unknown event type') + const path = await store(bytes) + const sourceStat = await stat(path) + for (const access of ['read', 'write'] as const) { + const opened = ctx.sessionPersistence.open(id, access).then(async (handle) => { + await handle.close() + }) + await expect(opened).rejects.toThrow(SessionFormatUnsupportedError) + expect(await readFile(path)).toEqual(bytes) + expect(await stat(path)).toMatchObject({ dev: sourceStat.dev, ino: sourceStat.ino }) + } + }, + ) + + it.each(obsoleteTypes)('retains ignorable %s through scanning and a provider append', async (type) => { + const event = obsoleteEvent(type, true) + const bytes = Buffer.from(prefix + JSON.stringify(event) + '\n') + expect(scanLog(bytes)).toMatchObject({ events: [start, event], committedBytes: bytes.length }) + const path = await store(bytes) + const writer = await ctx.sessionPersistence.open(id, 'write') + try { + expect((await writer.read()).events).toEqual([start, event]) + await writer.append([{ + type: 'turn/end', seq: SessionSeq(2), time: 3, data: { turn: 1, reason: { kind: 'completed' } }, + }]) + } finally { + await writer.close() + } + expect((await readFile(path)).subarray(0, bytes.length)).toEqual(bytes) + }) + + it.each(['{not json', 'null'])('still recovers an ordinary malformed EOF row: %s', async (tail) => { + const bytes = Buffer.from(prefix + tail + '\n') + expect(scanLog(bytes)).toMatchObject({ events: [start], committedBytes: Buffer.byteLength(prefix) }) + const path = await store(bytes) + const writer = await ctx.sessionPersistence.open(id, 'write') + const end = { + type: 'turn/end' as const, seq: SessionSeq(1), time: 3, data: { turn: 1, reason: { kind: 'completed' as const } }, + } + try { + expect((await writer.read()).events).toEqual([start]) + await writer.append([end]) + } finally { + await writer.close() + } + expect(await readFile(path, 'utf8')).toBe(prefix + JSON.stringify(end) + '\n') + }) +}) diff --git a/packages/session/session-persistence-jsonl/tsconfig.json b/packages/session/session-persistence-jsonl/tsconfig.json index c8c0b2e138..c0eab86dcd 100644 --- a/packages/session/session-persistence-jsonl/tsconfig.json +++ b/packages/session/session-persistence-jsonl/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../session-format-catalog" + }, + { + "path": "../session-format-v2-to-v3" } ] } diff --git a/packages/spill/spill-policy/src/index.ts b/packages/spill/spill-policy/src/index.ts index 9f94ace8e5..742d5f9d57 100644 --- a/packages/spill/spill-policy/src/index.ts +++ b/packages/spill/spill-policy/src/index.ts @@ -11,7 +11,7 @@ * The policy only decides WHEN to spill and composes the notice. * * A second arm applies the SAME cap to the durable log: the - * `tools/ptc-dispatch-log` waterfall bounds the `tool/code-dispatch` event's + * `tools/ptc-dispatch-log` waterfall bounds the `tool/ptc-dispatch` event's * copy of an oversized `run_code` sub-call result (the program's value is * untouched; UIs and replay read the full text through the spill artifact). * @@ -203,7 +203,7 @@ export function apply(ctx: Context, config: Config): void { return { kind: 'accept', content: replaced, ...decision.additionalContexts ? { additionalContexts: decision.additionalContexts } : {} } }, { prepend: true }) - // The durable-log arm: bound the `tool/code-dispatch` event's copy of an + // The durable-log arm: bound the `tool/ptc-dispatch` event's copy of an // oversized sub-call result the same way the model-facing arm bounds an // outer result. The program's returned value is untouched (it already // crossed the worker boundary whole); only the session log's copy shrinks diff --git a/packages/spill/spill-policy/tests/spill-policy.spec.ts b/packages/spill/spill-policy/tests/spill-policy.spec.ts index eef94038e6..5f3555839e 100644 --- a/packages/spill/spill-policy/tests/spill-policy.spec.ts +++ b/packages/spill/spill-policy/tests/spill-policy.spec.ts @@ -263,7 +263,7 @@ describe('the durable dispatch-log arm', () => { return { ctx, result, events, spill: ctx.spillStore as StubStore } } - it('bounds the tool/code-dispatch copy of an oversized sub-result while the program value stays whole', async () => { + it('bounds the tool/ptc-dispatch copy of an oversized sub-result while the program value stays whole', async () => { const { result, events, spill } = await runCodeWith( 'const blocks = await tools.huge_read({});\nreturn blocks[0].text.length', 200) expect(result.isError).toBe(false) @@ -271,7 +271,7 @@ describe('the durable dispatch-log arm', () => { // The program received the COMPLETE text (length 2000), untouched by spill. expect(result.value).toMatchObject({ result: 2_000 }) // The durable settle event carries the bounded projection + locator. - const settle = events.find(event => event.type === 'tool/code-dispatch') + const settle = events.find(event => event.type === 'tool/ptc-dispatch') expect(settle).toBeDefined() const logged = (settle!.data as { content: { type: string; text: string }[] }).content expect(logged).toHaveLength(1) @@ -281,7 +281,7 @@ describe('the durable dispatch-log arm', () => { // The artifact holds the full text under the dispatch label and sub-call id. const save = spill.saves.find(entry => entry.source.label === 'dispatch') expect(save).toMatchObject({ - source: { kind: 'tool', toolName: 'huge_read', callId: 'parent-1:code:1', label: 'dispatch' }, + source: { kind: 'tool', toolName: 'huge_read', callId: 'parent-1:ptc:1', label: 'dispatch' }, }) expect(save?.content).toBe('H'.repeat(2_000)) }) @@ -296,7 +296,7 @@ describe('the durable dispatch-log arm', () => { return [{ type: 'text', text: 'x'.repeat(100) }, { type: 'reasoning', text: 'why' }] }, })]) - const settle = events.find(event => event.type === 'tool/code-dispatch') + const settle = events.find(event => event.type === 'tool/ptc-dispatch') expect((settle!.data as { content: unknown[] }).content).toHaveLength(2) expect(spill.saves.filter(entry => entry.source.label === 'dispatch')).toHaveLength(0) }) @@ -304,7 +304,7 @@ describe('the durable dispatch-log arm', () => { it('leaves a within-cap sub-result log untouched and saves nothing for it', async () => { const { events, spill } = await runCodeWith( 'return await tools.small_read({})', 200) - const settle = events.find(event => event.type === 'tool/code-dispatch') + const settle = events.find(event => event.type === 'tool/ptc-dispatch') expect((settle!.data as { content: { type: string; text: string }[] }).content) .toEqual([{ type: 'text', text: 'tiny' }]) expect(spill.saves.filter(entry => entry.source.label === 'dispatch')).toHaveLength(0) @@ -356,7 +356,7 @@ describe('the durable dispatch-log arm', () => { // the backend and observe the settle events land inside the turn. await vi.waitFor(() => { // The second dispatch STARTED while the first one's spill hung. - smallAfterHuge = events.some(event => event.type === 'tool/code-dispatch-start' + smallAfterHuge = events.some(event => event.type === 'tool/ptc-dispatch-start' && (event.data as { name: string }).name === 'small_read') if (!smallAfterHuge) throw new Error('small_read not started yet') }) @@ -365,7 +365,7 @@ describe('the durable dispatch-log arm', () => { expect(result.isError).toBe(false) if (result.isError) throw new Error('expected success') expect(result.value).toMatchObject({ result: 2_004 }) - const settles = events.filter(event => event.type === 'tool/code-dispatch') + const settles = events.filter(event => event.type === 'tool/ptc-dispatch') expect(settles).toHaveLength(2) expect(smallAfterHuge).toBe(true) }) @@ -392,8 +392,8 @@ describe('the durable dispatch-log arm', () => { }, } ctx.tools.register(textTool('huge_read', 'H'.repeat(2_000))) - const started = (n: number): boolean => events.some(event => event.type === 'tool/code-dispatch-start' - && (event.data as { subCallId: string }).subCallId.endsWith(`:code:${n}`)) + const started = (n: number): boolean => events.some(event => event.type === 'tool/ptc-dispatch-start' + && (event.data as { subCallId: string }).subCallId.endsWith(`:ptc:${n}`)) const runPromise = ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('parent-bound'), @@ -421,7 +421,7 @@ describe('the durable dispatch-log arm', () => { expect(result.isError).toBe(false) await vi.waitFor(() => { if (releases.length > 0) { while (releases.length > 0) releases.shift()!() } - if (events.filter(event => event.type === 'tool/code-dispatch').length !== 3) { + if (events.filter(event => event.type === 'tool/ptc-dispatch').length !== 3) { throw new Error('settle events still pending') } }) @@ -452,7 +452,7 @@ describe('the durable dispatch-log arm', () => { agent: agent as never, }) expect(result.isError).toBe(false) - const settle = events.find(event => event.type === 'tool/code-dispatch') + const settle = events.find(event => event.type === 'tool/ptc-dispatch') expect((settle!.data as { content: { text: string }[] }).content[0]!.text).toBe('H'.repeat(2_000)) expect(warn).toHaveBeenCalled() }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66af44c880..3ca9020935 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7425,6 +7425,9 @@ importers: '@deepseek-ai/dsh-session-format-catalog': specifier: workspace:^ version: link:../session-format-catalog + '@deepseek-ai/dsh-session-format-v2-to-v3': + specifier: workspace:^ + version: link:../session-format-v2-to-v3 '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 28a6b07f25..ccaa44514a 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -1404,7 +1404,7 @@ function renderToolPipeline(): string { ` around["${mermaidCode('tools/execute')} waterfall
timeout, retry, metrics (around dispatch)"]`, ' toolBody["Registered tool execute() body"]', ` fsGate["${mermaidCode('fs/write-intent')} or ${mermaidCode('fs/edit-intent')}
tool-fs mutations only"]`, - ` owned["Tool-owned session events
${mermaidCode('todo/write')}, ${mermaidCode('fs/observed')}, ${mermaidCode('hook/invoked')}, ${mermaidCode('hook/result')}, ${mermaidCode('tool/code-dispatch')}"]`, + ` owned["Tool-owned session events
${mermaidCode('todo/write')}, ${mermaidCode('fs/observed')}, ${mermaidCode('hook/invoked')}, ${mermaidCode('hook/result')}, ${mermaidCode('tool/ptc-dispatch')}"]`, ` post["${mermaidCode('tools/post-execute')} waterfall
accept, block, replace, add context"]`, ' normalized["Registry outer normalization
pipeline/result snapshot throws become isError"]', ' finalize["ToolDefinition.finalizeContent
last content-only invariant"]', @@ -1444,7 +1444,7 @@ function renderToolPipeline(): string { ' allResults --> context', '```', '', - 'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition\'s snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. PTC mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/code-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.', + 'Filesystem read-before-edit checks stay below `tool-fs` on `fs/*` events. Generic pre/post waterfalls host hooks and approval policy; `ctx.approval` resolves asks before monotonic guards, and owner policy that must not be reordered remains a registered guard. Around-dispatch concerns such as timeouts wrap `tools/execute`. The registry losslessly snapshots the candidate result and normalizes a snapshot failure before the visible definition\'s snapshotted `finalizeContent` callback enforces its synchronous content-only invariant. `tools/result` then observes the immutable, lossless-JSON outcome. This lets hooks span tool families without coupling the tools to one policy service. PTC mode sends both the reserved `run_code` transport and its serialized sub-calls through the pipeline; sub-calls carry the parent token, log `tool/ptc-dispatch`, return denials as binding rejections, and omit `additionalContexts` to preserve call/result adjacency.', '', ...maintenanceFooter(maintenance), ].join('\n') diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 0bcff2f4ed..0fcf1c41fe 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -205,7 +205,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ dir: 'tools', source: 'packages/core/tools/src/ptc.ts', requires: ['ctx.tools', 'ctx.codeRuntime (execution time)', 'ctx.systemPrompt'], - writes: ['tool/call', 'one tool/code-dispatch-start + tool/code-dispatch pair per bridged sub-call', 'tool/result'], + writes: ['tool/call', 'one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call', 'tool/result'], // The registry's OWN tool: run_code exists only under a non-native mode // (the registry registers it in its constructor; the code runtime is read // at assembly/execution time, so the schema harvest needs none mounted). diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index fb96b7bf71..a97b5970d7 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -1302,8 +1302,15 @@ def smoke_sdk_snapshot(base_url: str, executable: Path, update_snapshots: bool) methods = [notification.method for notification in result.notifications] if methods.count("subagent.started") != 2 or methods.count("subagent.finished") != 2: raise AssertionError(f"advanced snapshot emitted unexpected subagent lifecycle: {methods}") - if not any(event.get("type") == "tool/code-dispatch" for event in result.events): - raise AssertionError("advanced snapshot emitted no tool/code-dispatch event") + ptc_events = [event for event in result.events + if event.get("type") in ("tool/ptc-dispatch-start", "tool/ptc-dispatch")] + if [event["type"] for event in ptc_events] != ["tool/ptc-dispatch-start", "tool/ptc-dispatch"]: + raise AssertionError(f"advanced snapshot emitted unexpected PTC dispatch events: {ptc_events}") + for event in ptc_events: + data = event["data"] + identity = (data.get("rootCallId"), data.get("parentCallId"), data.get("subCallId")) + if identity != ("advanced-code", "advanced-code", "advanced-code:ptc:1"): + raise AssertionError(f"advanced snapshot emitted unexpected PTC dispatch identity: {identity}") logs = read_session_logs(sessions) child_ids = snapshot_child_ids(result) diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index fc4f46a86d..0cad5fbc65 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -705,13 +705,13 @@ } }, { - "type": "tool/code-dispatch-start", + "type": "tool/ptc-dispatch-start", "seq": 30, "time": 0, "data": { "rootCallId": "advanced-code", "parentCallId": "advanced-code", - "subCallId": "advanced-code:code:1", + "subCallId": "advanced-code:ptc:1", "name": "snapshot_double", "arguments": { "value": 21 @@ -719,13 +719,13 @@ } }, { - "type": "tool/code-dispatch", + "type": "tool/ptc-dispatch", "seq": 31, "time": 0, "data": { "rootCallId": "advanced-code", "parentCallId": "advanced-code", - "subCallId": "advanced-code:code:1", + "subCallId": "advanced-code:ptc:1", "name": "snapshot_double", "arguments": { "value": 21 @@ -2453,13 +2453,13 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "tool/code-dispatch-start", + "type": "tool/ptc-dispatch-start", "seq": 30, "time": 0, "data": { "rootCallId": "advanced-code", "parentCallId": "advanced-code", - "subCallId": "advanced-code:code:1", + "subCallId": "advanced-code:ptc:1", "name": "snapshot_double", "arguments": { "value": 21 @@ -2473,13 +2473,13 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "tool/code-dispatch", + "type": "tool/ptc-dispatch", "seq": 31, "time": 0, "data": { "rootCallId": "advanced-code", "parentCallId": "advanced-code", - "subCallId": "advanced-code:code:1", + "subCallId": "advanced-code:ptc:1", "name": "snapshot_double", "arguments": { "value": 21 diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl index 4f88681850..5040997926 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl @@ -29,8 +29,8 @@ {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":26}} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"snapshot_double","arguments":{"value":21}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}} {"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[29],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} diff --git a/snapshots/sdk/ptc-turn/notifications.expected.jsonl b/snapshots/sdk/ptc-turn/notifications.expected.jsonl new file mode 100644 index 0000000000..63bea89e21 --- /dev/null +++ b/snapshots/sdk/ptc-turn/notifications.expected.jsonl @@ -0,0 +1,23 @@ +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":3,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool twice — exactly `echo CODE_ONE` then exactly `echo CODE_TWO`. Inside that same program, console.log exactly `captured output`, then return the two outputs joined with a plus sign. Reply with that joined string only and stop."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}}} +{"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"running"}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":4,"time":0,"data":{"turn":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":5,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":7,"time":0,"data":{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool twice — exactly `echo CODE_ONE` then exactly `echo CODE_TWO`. Inside that same program, console.log exactly `captured output`, then return the two outputs joined with a plus sign. Reply with that joined string only and stop."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":9,"time":0,"data":{"title":"Using ONE run_code program: call","messageSeqs":[7],"source":{"kind":"fallback"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":12,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to write a single run_code program that:\n1. Calls bash tool twice: `echo CODE_ONE` and `echo CODE_TWO`\n2. console.log exactly `captured output`\n3. Return the two outputs joined with a plus sign\n\nLet me write this."},{"type":"tool-call","id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\": \"\\nconst out1 = await tools.bash({command: \\\"echo CODE_ONE\\\", description: \\\"Print CODE_ONE\\\"});\\nconst out2 = await tools.bash({command: \\\"echo CODE_TWO\\\", description: \\\"Print CODE_TWO\\\"});\\nconsole.log(\\\"captured output\\\");\\nconst text1 = out1.stdout.text.trim();\\nconst text2 = out2.stdout.text.trim();\\nreturn text1 + \\\"+\\\" + text2;\\n\", \"description\": \"Run two echo commands and join outputs\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," write"," a"," single"," run","_code"," program"," that",":\n","1","."," Calls"," bash"," tool"," twice",":"," `","echo"," CODE","_","ONE","`"," and"," `","echo"," CODE","_T","WO","`\n","2","."," console",".log"," exactly"," `","capt","ured"," output","`\n","3","."," Return"," the"," two"," outputs"," joined"," with"," a"," plus"," sign","\n\n","Let"," me"," write"," this","."]},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","args":["","{","\"","code","\"",": ","\"","\\n","const"," out","1"," ="," await"," tools",".b","ash","({","command",":"," \\\"","echo"," CODE","_","ONE","\\\","," description",":"," \\\"","Print"," CODE","_","ONE","\\\"","});\\n","const"," out","2"," ="," await"," tools",".b","ash","({","command",":"," \\\"","echo"," CODE","_T","WO","\\\","," description",":"," \\\"","Print"," CODE","_T","WO","\\\"","});\\n","console",".log","(\\\"","capt","ured"," output","\\\");\\n","const"," text","1"," ="," out","1",".stdout",".text",".trim","();\\n","const"," text","2"," ="," out","2",".stdout",".text",".trim","();\\n","return"," text","1"," +"," \\\"+","\\\""," +"," text","2",";\\n","\"",", ","\"","description","\"",": ","\"","Run"," two"," echo"," commands"," and"," join"," outputs","\"","}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single run_code program that:\n1. Calls bash tool twice: `echo CODE_ONE` and `echo CODE_TWO`\n2. console.log exactly `captured output`\n3. Return the two outputs joined with a plus sign\n\nLet me write this."}}},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\": \"\\nconst out1 = await tools.bash({command: \\\"echo CODE_ONE\\\", description: \\\"Print CODE_ONE\\\"});\\nconst out2 = await tools.bash({command: \\\"echo CODE_TWO\\\", description: \\\"Print CODE_TWO\\\"});\\nconsole.log(\\\"captured output\\\");\\nconst text1 = out1.stdout.text.trim();\\nconst text2 = out2.stdout.text.trim();\\nreturn text1 + \\\"+\\\" + text2;\\n\", \"description\": \"Run two echo commands and join outputs\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":13,"time":0,"data":{"turn":1,"step":1,"callId":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\": \"\\nconst out1 = await tools.bash({command: \\\"echo CODE_ONE\\\", description: \\\"Print CODE_ONE\\\"});\\nconst out2 = await tools.bash({command: \\\"echo CODE_TWO\\\", description: \\\"Print CODE_TWO\\\"});\\nconsole.log(\\\"captured output\\\");\\nconst text1 = out1.stdout.text.trim();\\nconst text2 = out2.stdout.text.trim();\\nreturn text1 + \\\"+\\\" + text2;\\n\", \"description\": \"Run two echo commands and join outputs\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/ptc-dispatch-start","seq":14,"time":0,"data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/ptc-dispatch","seq":15,"time":0,"data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"},"isError":false,"content":[{"type":"text","text":"CODE_ONE\n"}]}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/ptc-dispatch-start","seq":16,"time":0,"data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/ptc-dispatch","seq":17,"time":0,"data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"},"isError":false,"content":[{"type":"text","text":"CODE_TWO\n"}]}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_UiQPVqoELyzBZCY5pm1z7875"},"content":[{"type":"tool-result","toolCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","content":[{"type":"text","text":"captured output\nCODE_ONE+CODE_TWO"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":19,"time":0,"data":{"turn":1,"step":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":20,"time":0,"data":{"turn":1,"step":2}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The program ran successfully. The console.log output \"captured output\" appeared, and the return value is \"CODE_ONE+CODE_TWO\". The user asked me to reply with that joined string only."},{"type":"text","text":"CODE_ONE+CODE_TWO"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":117,"outputTokens":50,"cacheReadTokens":6272,"reasoningTokens":42},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":0,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," program"," ran"," successfully","."," The"," console",".log"," output"," \"","capt","ured"," output","\""," appeared",","," and"," the"," return"," value"," is"," \"","CODE","_","ONE","+","CODE","_T","WO","\"."," The"," user"," asked"," me"," to"," reply"," with"," that"," joined"," string"," only","."]},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":1,"dt":[0,0,0,0,0,0],"texts":["CODE","_","ONE","+","CODE","_T","WO"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The program ran successfully. The console.log output \"captured output\" appeared, and the return value is \"CODE_ONE+CODE_TWO\". The user asked me to reply with that joined string only."}}},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CODE_ONE+CODE_TWO"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":117,"outputTokens":50,"cacheReadTokens":6272,"reasoningTokens":42}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":22,"time":0,"data":{"turn":1,"step":2}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":23,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} +{"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} diff --git a/snapshots/sdk/ptc-turn/result.expected.json b/snapshots/sdk/ptc-turn/result.expected.json new file mode 100644 index 0000000000..b066969ade --- /dev/null +++ b/snapshots/sdk/ptc-turn/result.expected.json @@ -0,0 +1 @@ +{"sessionId":"{{sessionId}}","finalResponse":"CODE_ONE+CODE_TWO"} diff --git a/snapshots/sdk/ptc-turn/runtime.cordis.yml b/snapshots/sdk/ptc-turn/runtime.cordis.yml new file mode 100644 index 0000000000..53db9d9c95 --- /dev/null +++ b/snapshots/sdk/ptc-turn/runtime.cordis.yml @@ -0,0 +1,3 @@ +- insert: + - id: code-runtime + name: '@deepseek-ai/dsh-code-runtime-worker-thread' diff --git a/snapshots/sdk/ptc-turn/session.v3.jsonl b/snapshots/sdk/ptc-turn/session.v3.jsonl new file mode 100644 index 0000000000..f0092054a7 --- /dev/null +++ b/snapshots/sdk/ptc-turn/session.v3.jsonl @@ -0,0 +1,26 @@ +{"type":"session","version":3,"id":"{{session:1}}","createdAt":1785014439563,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool twice — exactly `echo CODE_ONE` then exactly `echo CODE_TWO`. Inside that same program, console.log exactly `captured output`, then return the two outputs joined with a plus sign. Reply with that joined string only and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool twice — exactly `echo CODE_ONE` then exactly `echo CODE_TWO`. Inside that same program, console.log exactly `captured output`, then return the two outputs joined with a plus sign. Reply with that joined string only and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Using ONE run_code program: call","messageSeqs":[8],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to write a single run_code program that:\n1. Calls bash tool twice: `echo CODE_ONE` and `echo CODE_TWO`\n2. console.log exactly `captured output`\n3. Return the two outputs joined with a plus sign\n\nLet me write this."},{"type":"tool-call","id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\": \"\\nconst out1 = await tools.bash({command: \\\"echo CODE_ONE\\\", description: \\\"Print CODE_ONE\\\"});\\nconst out2 = await tools.bash({command: \\\"echo CODE_TWO\\\", description: \\\"Print CODE_TWO\\\"});\\nconsole.log(\\\"captured output\\\");\\nconst text1 = out1.stdout.text.trim();\\nconst text2 = out2.stdout.text.trim();\\nreturn text1 + \\\"+\\\" + text2;\\n\", \"description\": \"Run two echo commands and join outputs\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60},"stream":[{"type":"chunk","time":1788233513446,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233513446,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," write"," a"," single"," run","_code"," program"," that",":\n","1","."," Calls"," bash"," tool"," twice",":"," `","echo"," CODE","_","ONE","`"," and"," `","echo"," CODE","_T","WO","`\n","2","."," console",".log"," exactly"," `","capt","ured"," output","`\n","3","."," Return"," the"," two"," outputs"," joined"," with"," a"," plus"," sign","\n\n","Let"," me"," write"," this","."]},{"type":"chunk","time":1788233513447,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233513447,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","args":["","{","\"","code","\"",": ","\"","\\n","const"," out","1"," ="," await"," tools",".b","ash","({","command",":"," \\\"","echo"," CODE","_","ONE","\\\","," description",":"," \\\"","Print"," CODE","_","ONE","\\\"","});\\n","const"," out","2"," ="," await"," tools",".b","ash","({","command",":"," \\\"","echo"," CODE","_T","WO","\\\","," description",":"," \\\"","Print"," CODE","_T","WO","\\\"","});\\n","console",".log","(\\\"","capt","ured"," output","\\\");\\n","const"," text","1"," ="," out","1",".stdout",".text",".trim","();\\n","const"," text","2"," ="," out","2",".stdout",".text",".trim","();\\n","return"," text","1"," +"," \\\"+","\\\""," +"," text","2",";\\n","\"",", ","\"","description","\"",": ","\"","Run"," two"," echo"," commands"," and"," join"," outputs","\"","}"]},{"type":"chunk","time":1788233513447,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single run_code program that:\n1. Calls bash tool twice: `echo CODE_ONE` and `echo CODE_TWO`\n2. console.log exactly `captured output`\n3. Return the two outputs joined with a plus sign\n\nLet me write this."}}},{"type":"chunk","time":1788233513447,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\": \"\\nconst out1 = await tools.bash({command: \\\"echo CODE_ONE\\\", description: \\\"Print CODE_ONE\\\"});\\nconst out2 = await tools.bash({command: \\\"echo CODE_TWO\\\", description: \\\"Print CODE_TWO\\\"});\\nconsole.log(\\\"captured output\\\");\\nconst text1 = out1.stdout.text.trim();\\nconst text2 = out2.stdout.text.trim();\\nreturn text1 + \\\"+\\\" + text2;\\n\", \"description\": \"Run two echo commands and join outputs\"}"}}},{"type":"chunk","time":1788233513448,"chunk":{"type":"usage","usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60}}},{"type":"chunk","time":1788233513448,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\": \"\\nconst out1 = await tools.bash({command: \\\"echo CODE_ONE\\\", description: \\\"Print CODE_ONE\\\"});\\nconst out2 = await tools.bash({command: \\\"echo CODE_TWO\\\", description: \\\"Print CODE_TWO\\\"});\\nconsole.log(\\\"captured output\\\");\\nconst text1 = out1.stdout.text.trim();\\nconst text2 = out2.stdout.text.trim();\\nreturn text1 + \\\"+\\\" + text2;\\n\", \"description\": \"Run two echo commands and join outputs\"}"}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"},"isError":false,"content":[{"type":"text","text":"CODE_ONE\n"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"},"isError":false,"content":[{"type":"text","text":"CODE_TWO\n"}]}} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_UiQPVqoELyzBZCY5pm1z7875"},"content":[{"type":"tool-result","toolCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","content":[{"type":"text","text":"captured output\nCODE_ONE+CODE_TWO"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"step/start","data":{"turn":1,"step":2}} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The program ran successfully. The console.log output \"captured output\" appeared, and the return value is \"CODE_ONE+CODE_TWO\". The user asked me to reply with that joined string only."},{"type":"text","text":"CODE_ONE+CODE_TWO"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":117,"outputTokens":50,"cacheReadTokens":6272,"reasoningTokens":42},"stream":[{"type":"chunk","time":1788233513557,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233513557,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0],"texts":["The"," program"," ran"," successfully","."," The"," console",".log"," output"," \"","capt","ured"," output","\""," appeared",","," and"," the"," return"," value"," is"," \"","CODE","_","ONE","+","CODE","_T","WO","\"."," The"," user"," asked"," me"," to"," reply"," with"," that"," joined"," string"," only","."]},{"type":"chunk","time":1788233513558,"chunk":{"type":"block-start","index":1,"blockType":"text"}},{"type":"text-chunks","time0":1788233513558,"index":1,"dt":[0,0,0,0,0,0],"texts":["CODE","_","ONE","+","CODE","_T","WO"]},{"type":"chunk","time":1788233513558,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The program ran successfully. The console.log output \"captured output\" appeared, and the return value is \"CODE_ONE+CODE_TWO\". The user asked me to reply with that joined string only."}}},{"type":"chunk","time":1788233513558,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CODE_ONE+CODE_TWO"}}},{"type":"chunk","time":1788233513558,"chunk":{"type":"usage","usage":{"inputTokens":117,"outputTokens":50,"cacheReadTokens":6272,"reasoningTokens":42}}},{"type":"chunk","time":1788233513558,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":2}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/ptc-turn/snapshot.yml b/snapshots/sdk/ptc-turn/snapshot.yml new file mode 100644 index 0000000000..e5a31c881e --- /dev/null +++ b/snapshots/sdk/ptc-turn/snapshot.yml @@ -0,0 +1,7 @@ +version: 1 +scenario: ptc-turn +profile: sdk +composition: ptc +recording: live +header: + class: ptc diff --git a/snapshots/sdk/sdk.snapshot.ts b/snapshots/sdk/sdk.snapshot.ts index fbfaa4578a..2bb0b65254 100644 --- a/snapshots/sdk/sdk.snapshot.ts +++ b/snapshots/sdk/sdk.snapshot.ts @@ -100,6 +100,10 @@ function dirOf(url: string): string { } interface SdkAssertions { + /** Additional profile patches applied after the shared composition. */ + patches?: readonly string[] + /** Final response required from a completed turn before updating goldens. */ + expectedFinalResponse?: string /** Environment overrides passed to the runtime subprocess. */ environment?: Readonly> /** A separate DSH SDK child whose persisted session joins the evidence. */ @@ -120,6 +124,11 @@ interface SdkAssertions { } const SDK_ASSERTIONS: Readonly> = { + 'ptc-turn': { + patches: [fileURLToPath(new URL('./ptc-turn/runtime.cordis.yml', import.meta.url))], + expectedFinalResponse: 'CODE_ONE+CODE_TWO', + expectedTools: { run_code: ['code', 'description'] }, + }, 'subagent-dsh-sdk-diagnostic': { environment: { DSH_TEST_CHILD_PATCH: dshSdkDiagnosticChildPatch }, }, @@ -525,9 +534,9 @@ async function runScenario(scenario: CorpusScenario): Promise<{ const route = modelFromSession(primaryFixture) const patchRoot = join(cwd, '.snapshot-patches') await mkdir(patchRoot, { recursive: true }) - const patches = authoredPatches(scenario, !recording) - .map((patch, index) => materializeProfilePatch(patch, cwd, patchRoot, index)) const assertions = SDK_ASSERTIONS[scenario.name] ?? {} + const patches = [...authoredPatches(scenario, !recording), ...assertions.patches ?? []] + .map((patch, index) => materializeProfilePatch(patch, cwd, patchRoot, index)) let childSessionsRoot: string | undefined let childEnvironment: Record = {} if (assertions.dshSdkChild !== undefined) { @@ -796,6 +805,14 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { assertions.dshSdkChild !== undefined, ) const actualContext = contextOf(ordered, cwd) + if (assertions.expectedFinalResponse !== undefined) { + expect(results.at(-1)?.finalResponse, `${scenario.name}: final response`).toBe(assertions.expectedFinalResponse) + const parent = ordered[0] + if (parent === undefined) throw new Error(`${scenario.name}: no primary session log`) + const turnEnds = records(parent.content).filter(record => record.type === 'turn/end') + expect(turnEnds, `${scenario.name}: completed turns`).toHaveLength(results.length) + for (const turnEnd of turnEnds) expect(turnEnd).toMatchObject({ data: { reason: { kind: 'completed' } } }) + } let expectedContents = retained && !refreshing ? await Promise.all(files.map((_, index) => readFile(join(scenarioDir, writerSnapshotName(index)), 'utf8'))) diff --git a/snapshots/session/advanced-toolchain/session.v3.jsonl b/snapshots/session/advanced-toolchain/session.v3.jsonl index d1172f8b1c..0be85d574c 100644 --- a/snapshots/session/advanced-toolchain/session.v3.jsonl +++ b/snapshots/session/advanced-toolchain/session.v3.jsonl @@ -19,10 +19,10 @@ {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788604619336,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788604619336,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"]},{"type":"chunk","time":1788604619336,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}}},{"type":"chunk","time":1788604619336,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788604619336,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\":\"const run = await tools.cordis_run({ pluginId: 'snap-1', packageId: 'pkg-1', mode: 'run' });\\nconst inspected = await tools.cordis_inspect_self({ pluginId: 'snap-1' });\\nreturn { run, inspected };\",\"description\":\"Run and inspect the dynamic Cordis Package\"}"}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"},"isError":false,"content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}]}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:2","name":"cordis_inspect_self","arguments":{"pluginId":"snap-1"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:2","name":"cordis_inspect_self","arguments":{"pluginId":"snap-1"},"isError":false,"content":[{"type":"text","text":"{\n \"mode\": \"plugin\",\n \"pluginId\": \"snap-1\",\n \"name\": \"Snapshot Marker\",\n \"packageCount\": 1,\n \"state\": \"running\",\n \"currentPackageId\": \"pkg-1\",\n \"activeRun\": {\n \"pluginRunId\": \"run-1\",\n \"packageId\": \"pkg-1\"\n },\n \"packages\": [\n {\n \"packageId\": \"pkg-1\",\n \"name\": \"Snapshot Marker\",\n \"purpose\": \"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\n \"hasHostHalf\": true,\n \"hasClientHalf\": false,\n \"isCurrent\": true,\n \"isNext\": false\n }\n ]\n}"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"cordis_run","arguments":{"pluginId":"snap-1","packageId":"pkg-1","mode":"run"},"isError":false,"content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:2","name":"cordis_inspect_self","arguments":{"pluginId":"snap-1"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:2","name":"cordis_inspect_self","arguments":{"pluginId":"snap-1"},"isError":false,"content":[{"type":"text","text":"{\n \"mode\": \"plugin\",\n \"pluginId\": \"snap-1\",\n \"name\": \"Snapshot Marker\",\n \"packageCount\": 1,\n \"state\": \"running\",\n \"currentPackageId\": \"pkg-1\",\n \"activeRun\": {\n \"pluginRunId\": \"run-1\",\n \"packageId\": \"pkg-1\"\n },\n \"packages\": [\n {\n \"packageId\": \"pkg-1\",\n \"name\": \"Snapshot Marker\",\n \"purpose\": \"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\n \"hasHostHalf\": true,\n \"hasClientHalf\": false,\n \"isCurrent\": true,\n \"isNext\": false\n }\n ]\n}"}]}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"{\n \"run\": {\n \"status\": \"running\",\n \"pluginId\": \"snap-1\",\n \"packageId\": \"pkg-1\",\n \"pluginRunId\": \"run-1\",\n \"currentPackageId\": \"pkg-1\",\n \"host\": {\n \"status\": \"running\",\n \"provides\": [],\n \"waitingFor\": []\n },\n \"client\": {\n \"status\": \"absent\",\n \"waitingFor\": []\n }\n },\n \"inspected\": {\n \"mode\": \"plugin\",\n \"pluginId\": \"snap-1\",\n \"name\": \"Snapshot Marker\",\n \"packageCount\": 1,\n \"state\": \"running\",\n \"currentPackageId\": \"pkg-1\",\n \"activeRun\": {\n \"pluginRunId\": \"run-1\",\n \"packageId\": \"pkg-1\"\n },\n \"packages\": [\n {\n \"packageId\": \"pkg-1\",\n \"name\": \"Snapshot Marker\",\n \"purpose\": \"Exercise the dynamic Cordis Package lifecycle in the snapshot.\",\n \"hasHostHalf\": true,\n \"hasClientHalf\": false,\n \"isCurrent\": true,\n \"isNext\": false\n }\n ]\n }\n}"}],"isError":false}],"role":"user","id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} diff --git a/snapshots/session/both-mode-turn/session.v3.jsonl b/snapshots/session/both-mode-turn/session.v3.jsonl index 978f8db8c1..9f6d7d73b5 100644 --- a/snapshots/session/both-mode-turn/session.v3.jsonl +++ b/snapshots/session/both-mode-turn/session.v3.jsonl @@ -14,8 +14,8 @@ {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to call the run_code tool with a TypeScript program that runs `echo BOTH_OK` via `tools.bash` and returns its output."},{"type":"tool-call","id":"call_00_Era4M5eh79bvNOIey5q90401","name":"run_code","arguments":"{\"code\": \"const result = await tools.bash({ command: \\\"echo BOTH_OK\\\", description: \\\"Print BOTH_OK\\\" });\\nreturn result.stdout.text;\", \"description\": \"Run echo BOTH_OK via tools.bash\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10400,"outputTokens":130,"cacheReadTokens":0,"reasoningTokens":34},"stream":[{"type":"chunk","time":1788604627042,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788604627042,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," call"," the"," run","_code"," tool"," with"," a"," Type","Script"," program"," that"," runs"," `","echo"," B","OTH","_OK","`"," via"," `","tools",".b","ash","`"," and"," returns"," its"," output","."]},{"type":"chunk","time":1788604627043,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788604627043,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_Era4M5eh79bvNOIey5q90401","name":"run_code","args":["","{","\"","code","\"",": ","\"","const"," result"," ="," await"," tools",".b","ash","({"," command",":"," \\\"","echo"," B","OTH","_OK","\\\","," description",":"," \\\"","Print"," B","OTH","_OK","\\\""," });\\n","return"," result",".stdout",".text",";","\"",", ","\"","description","\"",": ","\"","Run"," echo"," B","OTH","_OK"," via"," tools",".b","ash","\"","}"]},{"type":"chunk","time":1788604627043,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to call the run_code tool with a TypeScript program that runs `echo BOTH_OK` via `tools.bash` and returns its output."}}},{"type":"chunk","time":1788604627043,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_Era4M5eh79bvNOIey5q90401","name":"run_code","arguments":"{\"code\": \"const result = await tools.bash({ command: \\\"echo BOTH_OK\\\", description: \\\"Print BOTH_OK\\\" });\\nreturn result.stdout.text;\", \"description\": \"Run echo BOTH_OK via tools.bash\"}"}}},{"type":"chunk","time":1788604627043,"chunk":{"type":"usage","usage":{"inputTokens":10400,"outputTokens":130,"cacheReadTokens":0,"reasoningTokens":34}}},{"type":"chunk","time":1788604627043,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_Era4M5eh79bvNOIey5q90401","name":"run_code","arguments":"{\"code\": \"const result = await tools.bash({ command: \\\"echo BOTH_OK\\\", description: \\\"Print BOTH_OK\\\" });\\nreturn result.stdout.text;\", \"description\": \"Run echo BOTH_OK via tools.bash\"}"}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_00_Era4M5eh79bvNOIey5q90401","parentCallId":"call_00_Era4M5eh79bvNOIey5q90401","subCallId":"call_00_Era4M5eh79bvNOIey5q90401:code:1","name":"bash","arguments":{"command":"echo BOTH_OK","description":"Print BOTH_OK"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"call_00_Era4M5eh79bvNOIey5q90401","parentCallId":"call_00_Era4M5eh79bvNOIey5q90401","subCallId":"call_00_Era4M5eh79bvNOIey5q90401:code:1","name":"bash","arguments":{"command":"echo BOTH_OK","description":"Print BOTH_OK"},"isError":false,"content":[{"type":"text","text":"BOTH_OK\n"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_00_Era4M5eh79bvNOIey5q90401","parentCallId":"call_00_Era4M5eh79bvNOIey5q90401","subCallId":"call_00_Era4M5eh79bvNOIey5q90401:ptc:1","name":"bash","arguments":{"command":"echo BOTH_OK","description":"Print BOTH_OK"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_00_Era4M5eh79bvNOIey5q90401","parentCallId":"call_00_Era4M5eh79bvNOIey5q90401","subCallId":"call_00_Era4M5eh79bvNOIey5q90401:ptc:1","name":"bash","arguments":{"command":"echo BOTH_OK","description":"Print BOTH_OK"},"isError":false,"content":[{"type":"text","text":"BOTH_OK\n"}]}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_Era4M5eh79bvNOIey5q90401"},"content":[{"type":"tool-result","toolCallId":"call_00_Era4M5eh79bvNOIey5q90401","content":[{"type":"text","text":"BOTH_OK\n"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} diff --git a/snapshots/session/ptc-python-turn/session.v3.jsonl b/snapshots/session/ptc-python-turn/session.v3.jsonl index 5723c9e006..d7454674d9 100644 --- a/snapshots/session/ptc-python-turn/session.v3.jsonl +++ b/snapshots/session/ptc-python-turn/session.v3.jsonl @@ -14,10 +14,10 @@ {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to write a single Python run_code program that:\n1. Calls bash tool twice: `echo CODE_ONE` and `echo CODE_TWO`\n2. print exactly `captured output`\n3. Return the two outputs joined with a plus sign\n\nLet me write this."},{"type":"tool-call","id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\":\"out1 = await tools.bash({\\\"command\\\": \\\"echo CODE_ONE\\\", \\\"description\\\": \\\"Print CODE_ONE\\\"})\\nout2 = await tools.bash({\\\"command\\\": \\\"echo CODE_TWO\\\", \\\"description\\\": \\\"Print CODE_TWO\\\"})\\nprint(\\\"captured output\\\")\\ntext1 = out1[\\\"stdout\\\"][\\\"text\\\"].strip()\\ntext2 = out2[\\\"stdout\\\"][\\\"text\\\"].strip()\\nreturn text1 + \\\"+\\\" + text2\",\"description\":\"Run two echo commands and join outputs\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60},"stream":[{"type":"chunk","time":1788604686777,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788604686777,"index":0,"dt":[0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The user wants me to write a single Python run_code program that:\n1. Calls bash tool twice: `echo CODE_ONE` and `echo CODE_TWO`\n2. print exactly `captured output`\n3. Return the two outputs joined with a plus sign\n\nLet me write this.","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""]},{"type":"chunk","time":1788604686778,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788604686778,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","args":["{\"code\":\"out1 = await tools.bash({\\\"command\\\": \\\"echo CODE_ONE\\\", \\\"description\\\": \\\"Print CODE_ONE\\\"})\\nout2 = await tools.bash({\\\"command\\\": \\\"echo CODE_TWO\\\", \\\"description\\\": \\\"Print CODE_TWO\\\"})\\nprint(\\\"captured output\\\")\\ntext1 = out1[\\\"stdout\\\"][\\\"text\\\"].strip()\\ntext2 = out2[\\\"stdout\\\"][\\\"text\\\"].strip()\\nreturn text1 + \\\"+\\\" + text2\",\"description\":\"Run two echo commands and join outputs\"}","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","","",""]},{"type":"chunk","time":1788604686779,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single Python run_code program that:\n1. Calls bash tool twice: `echo CODE_ONE` and `echo CODE_TWO`\n2. print exactly `captured output`\n3. Return the two outputs joined with a plus sign\n\nLet me write this."}}},{"type":"chunk","time":1788604686779,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\":\"out1 = await tools.bash({\\\"command\\\": \\\"echo CODE_ONE\\\", \\\"description\\\": \\\"Print CODE_ONE\\\"})\\nout2 = await tools.bash({\\\"command\\\": \\\"echo CODE_TWO\\\", \\\"description\\\": \\\"Print CODE_TWO\\\"})\\nprint(\\\"captured output\\\")\\ntext1 = out1[\\\"stdout\\\"][\\\"text\\\"].strip()\\ntext2 = out2[\\\"stdout\\\"][\\\"text\\\"].strip()\\nreturn text1 + \\\"+\\\" + text2\",\"description\":\"Run two echo commands and join outputs\"}"}}},{"type":"chunk","time":1788604686779,"chunk":{"type":"usage","usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60}}},{"type":"chunk","time":1788604686779,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\":\"out1 = await tools.bash({\\\"command\\\": \\\"echo CODE_ONE\\\", \\\"description\\\": \\\"Print CODE_ONE\\\"})\\nout2 = await tools.bash({\\\"command\\\": \\\"echo CODE_TWO\\\", \\\"description\\\": \\\"Print CODE_TWO\\\"})\\nprint(\\\"captured output\\\")\\ntext1 = out1[\\\"stdout\\\"][\\\"text\\\"].strip()\\ntext2 = out2[\\\"stdout\\\"][\\\"text\\\"].strip()\\nreturn text1 + \\\"+\\\" + text2\",\"description\":\"Run two echo commands and join outputs\"}"}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:code:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:code:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"},"isError":false,"content":[{"type":"text","text":"CODE_ONE\n"}]}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"},"isError":false,"content":[{"type":"text","text":"CODE_TWO\n"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"},"isError":false,"content":[{"type":"text","text":"CODE_ONE\n"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"},"isError":false,"content":[{"type":"text","text":"CODE_TWO\n"}]}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_UiQPVqoELyzBZCY5pm1z7875"},"content":[{"type":"tool-result","toolCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","content":[{"type":"text","text":"captured output\nCODE_ONE+CODE_TWO"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} diff --git a/snapshots/session/ptc-read-image-attachment-path/session.v3.jsonl b/snapshots/session/ptc-read-image-attachment-path/session.v3.jsonl index 7bf7577ab6..a9bde4e0f0 100644 --- a/snapshots/session/ptc-read-image-attachment-path/session.v3.jsonl +++ b/snapshots/session/ptc-read-image-attachment-path/session.v3.jsonl @@ -14,24 +14,24 @@ {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"code-image-source","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: 'red.png' })\",\"description\":\"Read the source image\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788604689790,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788604689791,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"code-image-source","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: 'red.png' })\",\"description\":\"Read the source image\"}"}}},{"type":"chunk","time":1788604689791,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788604689791,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"code-image-source","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: 'red.png' })\",\"description\":\"Read the source image\"}"}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"code-image-source","parentCallId":"code-image-source","subCallId":"code-image-source:code:1","name":"read_image","arguments":{"file_path":"red.png"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"code-image-source","parentCallId":"code-image-source","subCallId":"code-image-source:code:1","name":"read_image","arguments":{"file_path":"red.png"},"isError":false,"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"code-image-source","parentCallId":"code-image-source","subCallId":"code-image-source:ptc:1","name":"read_image","arguments":{"file_path":"red.png"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"code-image-source","parentCallId":"code-image-source","subCallId":"code-image-source:ptc:1","name":"read_image","arguments":{"file_path":"red.png"},"isError":false,"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}]}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"code-image-source"},"content":[{"type":"tool-result","toolCallId":"code-image-source","content":[{"type":"text","text":"{\n \"path\": \"{{cwd}}/red.png\",\n \"image\": {\n \"attachmentId\": \"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\",\n \"mediaType\": \"image/png\",\n \"bytes\": 69,\n \"width\": 1,\n \"height\": 1,\n \"name\": \"red.png\"\n }\n}"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} -{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"source":{"kind":"plugin","plugin":"tools-code-mode"},"role":"user","id":"{{message:6}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"source":{"kind":"plugin","plugin":"tools-ptc"},"role":"user","id":"{{message:6}}"}]}} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"user/message","data":{"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"source":{"kind":"plugin","plugin":"tools-code-mode"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"source":{"kind":"plugin","plugin":"tools-ptc"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"code-image-object","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: '{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640' })\",\"description\":\"Read the normalized attachment object directly\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788604689916,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788604689916,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"code-image-object","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: '{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640' })\",\"description\":\"Read the normalized attachment object directly\"}"}}},{"type":"chunk","time":1788604689916,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788604689916,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"code-image-object","name":"run_code","arguments":"{\"code\":\"return await tools.read_image({ file_path: '{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640' })\",\"description\":\"Read the normalized attachment object directly\"}"}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"code-image-object","parentCallId":"code-image-object","subCallId":"code-image-object:code:1","name":"read_image","arguments":{"file_path":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"code-image-object","parentCallId":"code-image-object","subCallId":"code-image-object:code:1","name":"read_image","arguments":{"file_path":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"},"isError":false,"content":[{"type":"text","text":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"code-image-object","parentCallId":"code-image-object","subCallId":"code-image-object:ptc:1","name":"read_image","arguments":{"file_path":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"code-image-object","parentCallId":"code-image-object","subCallId":"code-image-object:ptc:1","name":"read_image","arguments":{"file_path":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"},"isError":false,"content":[{"type":"text","text":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}]}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"code-image-object"},"content":[{"type":"tool-result","toolCallId":"code-image-object","content":[{"type":"text","text":"{\n \"path\": \"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\",\n \"image\": {\n \"attachmentId\": \"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\",\n \"mediaType\": \"image/png\",\n \"bytes\": 69,\n \"width\": 1,\n \"height\": 1,\n \"name\": \"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\"\n }\n}"}],"isError":false}],"role":"user","id":"{{message:8}}"}},"sourceEventSeqs":[24],"surfaceOp":"append"} -{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}],"source":{"kind":"plugin","plugin":"tools-code-mode"},"role":"user","id":"{{message:9}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}],"source":{"kind":"plugin","plugin":"tools-ptc"},"role":"user","id":"{{message:9}}"}]}} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"user/message","data":{"content":[{"type":"text","text":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}],"source":{"kind":"plugin","plugin":"tools-code-mode"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"{{cwd}}/.dsh/attachments/v1/objects/b1/b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640"}}],"source":{"kind":"plugin","plugin":"tools-ptc"},"role":"user","id":"{{message:9}}"},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:10}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788604689985,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788604689985,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788604689985,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788604689985,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/ptc-read-image/session.v3.jsonl b/snapshots/session/ptc-read-image/session.v3.jsonl index 031b8e5700..d3b84bf94c 100644 --- a/snapshots/session/ptc-read-image/session.v3.jsonl +++ b/snapshots/session/ptc-read-image/session.v3.jsonl @@ -14,16 +14,16 @@ {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"code-image-call","name":"run_code","arguments":"{\"code\":\"const bytes = [137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,2,0,0,0,144,119,83,222,0,0,0,12,73,68,65,84,120,156,99,248,207,192,0,0,3,1,1,0,201,254,146,239,0,0,0,0,73,69,78,68,174,66,96,130];\\nawait tools.bash({ command: \\\"node -e \\\\\\\"require('node:fs').writeFileSync('red.png',Buffer.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,2,0,0,0,144,119,83,222,0,0,0,12,73,68,65,84,120,156,99,248,207,192,0,0,3,1,1,0,201,254,146,239,0,0,0,0,73,69,78,68,174,66,96,130]));\\\\\\\"\\\", description: \\\"Create a one pixel PNG\\\" });\\nconst image = await tools.read_image({ file_path: \\\"red.png\\\" });\\nreturn image.path;\",\"description\":\"Create and inspect one image\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788604688279,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788604688279,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"code-image-call","name":"run_code","arguments":"{\"code\":\"const bytes = [137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,2,0,0,0,144,119,83,222,0,0,0,12,73,68,65,84,120,156,99,248,207,192,0,0,3,1,1,0,201,254,146,239,0,0,0,0,73,69,78,68,174,66,96,130];\\nawait tools.bash({ command: \\\"node -e \\\\\\\"require('node:fs').writeFileSync('red.png',Buffer.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,2,0,0,0,144,119,83,222,0,0,0,12,73,68,65,84,120,156,99,248,207,192,0,0,3,1,1,0,201,254,146,239,0,0,0,0,73,69,78,68,174,66,96,130]));\\\\\\\"\\\", description: \\\"Create a one pixel PNG\\\" });\\nconst image = await tools.read_image({ file_path: \\\"red.png\\\" });\\nreturn image.path;\",\"description\":\"Create and inspect one image\"}"}}},{"type":"chunk","time":1788604688279,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788604688279,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"code-image-call","name":"run_code","arguments":"{\"code\":\"const bytes = [137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,2,0,0,0,144,119,83,222,0,0,0,12,73,68,65,84,120,156,99,248,207,192,0,0,3,1,1,0,201,254,146,239,0,0,0,0,73,69,78,68,174,66,96,130];\\nawait tools.bash({ command: \\\"node -e \\\\\\\"require('node:fs').writeFileSync('red.png',Buffer.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,2,0,0,0,144,119,83,222,0,0,0,12,73,68,65,84,120,156,99,248,207,192,0,0,3,1,1,0,201,254,146,239,0,0,0,0,73,69,78,68,174,66,96,130]));\\\\\\\"\\\", description: \\\"Create a one pixel PNG\\\" });\\nconst image = await tools.read_image({ file_path: \\\"red.png\\\" });\\nreturn image.path;\",\"description\":\"Create and inspect one image\"}"}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"code-image-call","parentCallId":"code-image-call","subCallId":"code-image-call:code:1","name":"bash","arguments":{"command":"node -e \"require('node:fs').writeFileSync('red.png',Buffer.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,2,0,0,0,144,119,83,222,0,0,0,12,73,68,65,84,120,156,99,248,207,192,0,0,3,1,1,0,201,254,146,239,0,0,0,0,73,69,78,68,174,66,96,130]));\"","description":"Create a one pixel PNG"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"code-image-call","parentCallId":"code-image-call","subCallId":"code-image-call:code:1","name":"bash","arguments":{"command":"node -e \"require('node:fs').writeFileSync('red.png',Buffer.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,2,0,0,0,144,119,83,222,0,0,0,12,73,68,65,84,120,156,99,248,207,192,0,0,3,1,1,0,201,254,146,239,0,0,0,0,73,69,78,68,174,66,96,130]));\"","description":"Create a one pixel PNG"},"isError":false,"content":[{"type":"text","text":"(no output)"}]}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"code-image-call","parentCallId":"code-image-call","subCallId":"code-image-call:code:2","name":"read_image","arguments":{"file_path":"red.png"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"code-image-call","parentCallId":"code-image-call","subCallId":"code-image-call:code:2","name":"read_image","arguments":{"file_path":"red.png"},"isError":false,"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"code-image-call","parentCallId":"code-image-call","subCallId":"code-image-call:ptc:1","name":"bash","arguments":{"command":"node -e \"require('node:fs').writeFileSync('red.png',Buffer.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,2,0,0,0,144,119,83,222,0,0,0,12,73,68,65,84,120,156,99,248,207,192,0,0,3,1,1,0,201,254,146,239,0,0,0,0,73,69,78,68,174,66,96,130]));\"","description":"Create a one pixel PNG"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"code-image-call","parentCallId":"code-image-call","subCallId":"code-image-call:ptc:1","name":"bash","arguments":{"command":"node -e \"require('node:fs').writeFileSync('red.png',Buffer.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,2,0,0,0,144,119,83,222,0,0,0,12,73,68,65,84,120,156,99,248,207,192,0,0,3,1,1,0,201,254,146,239,0,0,0,0,73,69,78,68,174,66,96,130]));\"","description":"Create a one pixel PNG"},"isError":false,"content":[{"type":"text","text":"(no output)"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"code-image-call","parentCallId":"code-image-call","subCallId":"code-image-call:ptc:2","name":"read_image","arguments":{"file_path":"red.png"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"code-image-call","parentCallId":"code-image-call","subCallId":"code-image-call:ptc:2","name":"read_image","arguments":{"file_path":"red.png"},"isError":false,"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}]}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"code-image-call"},"content":[{"type":"tool-result","toolCallId":"code-image-call","content":[{"type":"text","text":"{{cwd}}/red.png"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} -{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"source":{"kind":"plugin","plugin":"tools-code-mode"},"role":"user","id":"{{message:6}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"source":{"kind":"plugin","plugin":"tools-ptc"},"role":"user","id":"{{message:6}}"}]}} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"user/message","data":{"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"source":{"kind":"plugin","plugin":"tools-code-mode"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"{{cwd}}/red.png\nimage\n\nimage/png image, 1x1 px, 69 bytes\n"},{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","bytes":69,"width":1,"height":1,"name":"red.png"}}],"source":{"kind":"plugin","plugin":"tools-ptc"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash-vision-exp"},"id":"{{message:7}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788604688448,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788604688448,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788604688448,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788604688448,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/ptc-turn/session.v3.jsonl b/snapshots/session/ptc-turn/session.v3.jsonl index 308786f0b4..b54eef8942 100644 --- a/snapshots/session/ptc-turn/session.v3.jsonl +++ b/snapshots/session/ptc-turn/session.v3.jsonl @@ -14,10 +14,10 @@ {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to write a single run_code program that:\n1. Calls bash tool twice: `echo CODE_ONE` and `echo CODE_TWO`\n2. console.log exactly `captured output`\n3. Return the two outputs joined with a plus sign\n\nLet me write this."},{"type":"tool-call","id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\": \"\\nconst out1 = await tools.bash({command: \\\"echo CODE_ONE\\\", description: \\\"Print CODE_ONE\\\"});\\nconst out2 = await tools.bash({command: \\\"echo CODE_TWO\\\", description: \\\"Print CODE_TWO\\\"});\\nconsole.log(\\\"captured output\\\");\\nconst text1 = out1.stdout.text.trim();\\nconst text2 = out2.stdout.text.trim();\\nreturn text1 + \\\"+\\\" + text2;\\n\", \"description\": \"Run two echo commands and join outputs\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60},"stream":[{"type":"chunk","time":1788604691264,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788604691264,"index":0,"dt":[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," write"," a"," single"," run","_code"," program"," that",":\n","1","."," Calls"," bash"," tool"," twice",":"," `","echo"," CODE","_","ONE","`"," and"," `","echo"," CODE","_T","WO","`\n","2","."," console",".log"," exactly"," `","capt","ured"," output","`\n","3","."," Return"," the"," two"," outputs"," joined"," with"," a"," plus"," sign","\n\n","Let"," me"," write"," this","."]},{"type":"chunk","time":1788604691265,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788604691265,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","args":["","{","\"","code","\"",": ","\"","\\n","const"," out","1"," ="," await"," tools",".b","ash","({","command",":"," \\\"","echo"," CODE","_","ONE","\\\","," description",":"," \\\"","Print"," CODE","_","ONE","\\\"","});\\n","const"," out","2"," ="," await"," tools",".b","ash","({","command",":"," \\\"","echo"," CODE","_T","WO","\\\","," description",":"," \\\"","Print"," CODE","_T","WO","\\\"","});\\n","console",".log","(\\\"","capt","ured"," output","\\\");\\n","const"," text","1"," ="," out","1",".stdout",".text",".trim","();\\n","const"," text","2"," ="," out","2",".stdout",".text",".trim","();\\n","return"," text","1"," +"," \\\"+","\\\""," +"," text","2",";\\n","\"",", ","\"","description","\"",": ","\"","Run"," two"," echo"," commands"," and"," join"," outputs","\"","}"]},{"type":"chunk","time":1788604691266,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single run_code program that:\n1. Calls bash tool twice: `echo CODE_ONE` and `echo CODE_TWO`\n2. console.log exactly `captured output`\n3. Return the two outputs joined with a plus sign\n\nLet me write this."}}},{"type":"chunk","time":1788604691266,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\": \"\\nconst out1 = await tools.bash({command: \\\"echo CODE_ONE\\\", description: \\\"Print CODE_ONE\\\"});\\nconst out2 = await tools.bash({command: \\\"echo CODE_TWO\\\", description: \\\"Print CODE_TWO\\\"});\\nconsole.log(\\\"captured output\\\");\\nconst text1 = out1.stdout.text.trim();\\nconst text2 = out2.stdout.text.trim();\\nreturn text1 + \\\"+\\\" + text2;\\n\", \"description\": \"Run two echo commands and join outputs\"}"}}},{"type":"chunk","time":1788604691266,"chunk":{"type":"usage","usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60}}},{"type":"chunk","time":1788604691266,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_UiQPVqoELyzBZCY5pm1z7875","name":"run_code","arguments":"{\"code\": \"\\nconst out1 = await tools.bash({command: \\\"echo CODE_ONE\\\", description: \\\"Print CODE_ONE\\\"});\\nconst out2 = await tools.bash({command: \\\"echo CODE_TWO\\\", description: \\\"Print CODE_TWO\\\"});\\nconsole.log(\\\"captured output\\\");\\nconst text1 = out1.stdout.text.trim();\\nconst text2 = out2.stdout.text.trim();\\nreturn text1 + \\\"+\\\" + text2;\\n\", \"description\": \"Run two echo commands and join outputs\"}"}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:code:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:code:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"},"isError":false,"content":[{"type":"text","text":"CODE_ONE\n"}]}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:code:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"},"isError":false,"content":[{"type":"text","text":"CODE_TWO\n"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:1","name":"bash","arguments":{"command":"echo CODE_ONE","description":"Print CODE_ONE"},"isError":false,"content":[{"type":"text","text":"CODE_ONE\n"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","parentCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","subCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875:ptc:2","name":"bash","arguments":{"command":"echo CODE_TWO","description":"Print CODE_TWO"},"isError":false,"content":[{"type":"text","text":"CODE_TWO\n"}]}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_UiQPVqoELyzBZCY5pm1z7875"},"content":[{"type":"tool-result","toolCallId":"call_00_UiQPVqoELyzBZCY5pm1z7875","content":[{"type":"text","text":"captured output\nCODE_ONE+CODE_TWO"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} diff --git a/snapshots/session/ptc-workspace-context/session.v3.jsonl b/snapshots/session/ptc-workspace-context/session.v3.jsonl index 6d144a8286..0e8cc1c65a 100644 --- a/snapshots/session/ptc-workspace-context/session.v3.jsonl +++ b/snapshots/session/ptc-workspace-context/session.v3.jsonl @@ -15,8 +15,8 @@ {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_workspace_read","name":"run_code","arguments":"{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:5}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788604692667,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788604692667,"index":0,"dt":[],"id":"call_workspace_read","name":"run_code","args":["{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"]},{"type":"chunk","time":1788604692667,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_read","name":"run_code","arguments":"{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"}}},{"type":"chunk","time":1788604692667,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788604692667,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_workspace_read","name":"run_code","arguments":"{\"code\":\"return await tools.read({ file_path: 'nested/task.txt' })\",\"description\":\"Read nested/task.txt\"}"}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_workspace_read","parentCallId":"call_workspace_read","subCallId":"call_workspace_read:code:1","name":"read","arguments":{"file_path":"nested/task.txt"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"call_workspace_read","parentCallId":"call_workspace_read","subCallId":"call_workspace_read:code:1","name":"read","arguments":{"file_path":"nested/task.txt"},"isError":false,"content":[{"type":"text","text":"{{cwd}}/nested/task.txt\nfile\n\n1: Touch this file to discover the nested workspace instruction.\n\n(End of file - total 1 lines)\n"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_workspace_read","parentCallId":"call_workspace_read","subCallId":"call_workspace_read:ptc:1","name":"read","arguments":{"file_path":"nested/task.txt"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_workspace_read","parentCallId":"call_workspace_read","subCallId":"call_workspace_read:ptc:1","name":"read","arguments":{"file_path":"nested/task.txt"},"isError":false,"content":[{"type":"text","text":"{{cwd}}/nested/task.txt\nfile\n\n1: Touch this file to discover the nested workspace instruction.\n\n(End of file - total 1 lines)\n"}]}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_workspace_read"},"content":[{"type":"tool-result","toolCallId":"call_workspace_read","content":[{"type":"text","text":"{\n \"path\": \"{{cwd}}/nested/task.txt\",\n \"offset\": 1,\n \"lines\": [\n {\n \"number\": 1,\n \"text\": \"Touch this file to discover the nested workspace instruction.\"\n }\n ],\n \"totalLines\": 1\n}"}],"isError":false}],"role":"user","id":"{{message:6}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"agent-instructions","form":"instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]},"role":"user","id":"{{message:7}}"}]}} diff --git a/snapshots/web/ptc-round/session.v3.jsonl b/snapshots/web/ptc-round/session.v3.jsonl index 99761974e7..52bdf3d900 100644 --- a/snapshots/web/ptc-round/session.v3.jsonl +++ b/snapshots/web/ptc-round/session.v3.jsonl @@ -14,10 +14,10 @@ {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":128000}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to write a single `run_code` program that:\n1. Runs bash to echo \"CODE_ROUND_OK\"\n2. Tries to read a file \"missing.txt\" and catches the error\n3. Returns an object with both outcomes\n4. They also want me to reply \"DONE\" and stop after\n\nLet me write this program."},{"type":"tool-call","id":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":8082,"outputTokens":235,"cacheReadTokens":384,"reasoningTokens":75},"stream":[{"type":"chunk","time":1788240623185,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788240623203,"index":0,"dt":[18,17,17,17,15,17,17,18,17,17,17,17,16,17,17,17,18,17,17,17,17,17,17,16,17,17,16,17,17,17,17,18,17,16,18,18,17,17,18,18,17,16,17,18,16,17,16,17,17,17,18,17,16,17,17,17,17,18,17,17,17,17,17,17,18,15,16,17,16,17,17,17,16,18],"texts":["The"," user"," wants"," me"," to"," write"," a"," single"," `","run","_code","`"," program"," that",":\n","1","."," Runs"," bash"," to"," echo"," \"","CODE","_RO","UND","_OK","\"\n","2","."," T","ries"," to"," read"," a"," file"," \"","missing",".txt","\""," and"," catches"," the"," error","\n","3","."," Returns"," an"," object"," with"," both"," outcomes","\n","4","."," They"," also"," want"," me"," to"," reply"," \"","D","ONE","\""," and"," stop"," after","\n\n","Let"," me"," write"," this"," program","."]},{"type":"chunk","time":1788240624476,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788240624492,"index":1,"dt":[17,15,17,18,16,16,16,17,17,17,17,17,16,17,17,16,16,16,17,17,16,16,17,15,17,17,17,16,17,17,16,17,17,16,18,17,17,16,17,15,17,17,18,16,18,18,18,16,18,18,15,18,16,16,17,17,16,17,16,17,16,17,17,17,16,17,17,17,18,17,17,18,17,17,17,18,15,17,17,16,18,16,18,17,18,16,16,18,17,17,18,15,18,18,16,18,17,15,17,16,17,17,18,16,16,17,17,18,17,17,17,16,18,16,16,17,18,17],"id":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","args":["","{","\"","description","\"",": ","\"","Run"," bash"," echo"," and"," catch"," missing"," file"," read","\"",", ","\"","code","\"",": ","\"","\\n","const"," bash","Result"," ="," await"," tools",".b","ash","({\\n"," "," command",":"," \\\"","echo"," CODE","_RO","UND","_OK","\\\",\\n"," "," description",":"," \\\"","E","cho"," CODE","_RO","UND","_OK","\\\"\\n","});\\n\\n","let"," read","Error"," ="," null",";\\n","try"," {\\n"," "," await"," tools",".read","({"," file","_path",":"," \\\"","missing",".txt","\\\""," });\\n","}"," catch"," (","e",")"," {\\n"," "," read","Error"," ="," {\\n"," "," tool","Name",":"," e",".t","ool","Name",",\\n"," "," message",":"," e",".message","\\n"," "," };\\n","}\\n\\n","return"," {"," bash",":"," bash","Result",".stdout",".text",".trim","(),"," read","Error"," };\\n","\"","}"]},{"type":"chunk","time":1788240626493,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single `run_code` program that:\n1. Runs bash to echo \"CODE_ROUND_OK\"\n2. Tries to read a file \"missing.txt\" and catches the error\n3. Returns an object with both outcomes\n4. They also want me to reply \"DONE\" and stop after\n\nLet me write this program."}}},{"type":"chunk","time":1788240626508,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}}},{"type":"chunk","time":1788240626526,"chunk":{"type":"usage","usage":{"inputTokens":8082,"outputTokens":235,"cacheReadTokens":384,"reasoningTokens":75}}},{"type":"chunk","time":1788240626543,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_00_6VNoF1gDSerTBKoCfYSH3765","name":"run_code","arguments":"{\"description\": \"Run bash echo and catch missing file read\", \"code\": \"\\nconst bashResult = await tools.bash({\\n command: \\\"echo CODE_ROUND_OK\\\",\\n description: \\\"Echo CODE_ROUND_OK\\\"\\n});\\n\\nlet readError = null;\\ntry {\\n await tools.read({ file_path: \\\"missing.txt\\\" });\\n} catch (e) {\\n readError = {\\n toolName: e.toolName,\\n message: e.message\\n };\\n}\\n\\nreturn { bash: bashResult.stdout.text.trim(), readError };\\n\"}"}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"},"isError":false,"content":[{"type":"text","text":"CODE_ROUND_OK\n"}]}} -{"type":"tool/code-dispatch-start","data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:2","name":"read","arguments":{"file_path":"missing.txt"}}} -{"type":"tool/code-dispatch","data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:code:2","name":"read","arguments":{"file_path":"missing.txt"},"isError":true,"content":[{"type":"text","text":"Error: cannot read \"{{cwd}}/missing.txt\": not found"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:ptc:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:ptc:1","name":"bash","arguments":{"command":"echo CODE_ROUND_OK","description":"Echo CODE_ROUND_OK"},"isError":false,"content":[{"type":"text","text":"CODE_ROUND_OK\n"}]}} +{"type":"tool/ptc-dispatch-start","data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:ptc:2","name":"read","arguments":{"file_path":"missing.txt"}}} +{"type":"tool/ptc-dispatch","data":{"rootCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","parentCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","subCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765:ptc:2","name":"read","arguments":{"file_path":"missing.txt"},"isError":true,"content":[{"type":"text","text":"Error: cannot read \"{{cwd}}/missing.txt\": not found"}]}} {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_6VNoF1gDSerTBKoCfYSH3765"},"content":[{"type":"tool-result","toolCallId":"call_00_6VNoF1gDSerTBKoCfYSH3765","content":[{"type":"text","text":"{\n \"bash\": \"CODE_ROUND_OK\",\n \"readError\": {\n \"toolName\": \"read\",\n \"message\": \"cannot read \\\"{{cwd}}/missing.txt\\\": not found\"\n }\n}"}],"isError":false}],"role":"user","id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}}