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..de38d5367e 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: 003213fb4c0b85eb1c7764e5cea48f430d982035 +2026-06-15-ptc.zh.md: 3568748d0ee6a3e477d262e2f3c090557e28d424 diff --git a/.agents/notes/implemented/feature/2026-06-15-ptc.md b/.agents/notes/implemented/feature/2026-06-15-ptc.md index 96dda525c6..003213fb4c 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 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..3568748d0e 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 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 66ff93de30..d21cebd77f 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 2671f9fa0c..8a7f608e29 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: c47cd8a09d4c7a5179bb36d9c62611564b95a5cc -persistence-catalog.zh.md: c67d315c91565cee5855ee5b0667672151b83323 +persistence-catalog.md: c273e174b5d032b12270452b6f017f6caccf452c +persistence-catalog.zh.md: 35b693aa7ee23f80cbc4a1c2e571736f74f63922 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index c47cd8a09d..c273e174b5 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -893,14 +893,14 @@ Types: [ToolCallId](subsystems/core.md) Source: [`packages/core/session/src/types.ts:325`](../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 @@ -913,30 +913,30 @@ Source: [`packages/core/session/src/types.ts:325`](../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 c67d315c91..35b693aa7e 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -895,14 +895,14 @@ export type SessionEvent = { 来源:[`packages/core/session/src/types.ts:325`](../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 @@ -915,30 +915,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 932ec1da3e..20a0f80d9b 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -855,13 +855,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 658e7e80fb..8b7e844c6c 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 @@ -1086,14 +1086,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', @@ -1133,7 +1133,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', @@ -1145,6 +1145,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 }), @@ -2006,13 +2055,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', @@ -2157,10 +2206,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 af961ede65..479459fb19 100644 --- a/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts +++ b/packages/client/ui-trajectory/tests/conversation-definitions.client.spec.ts @@ -390,7 +390,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 }), @@ -400,22 +400,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: { @@ -432,7 +441,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') @@ -446,10 +455,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 3bc31ec32d..d1c59e682c 100644 --- a/packages/core/session/src/known-event-types.ts +++ b/packages/core/session/src/known-event-types.ts @@ -66,8 +66,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..316ddb3368 100644 --- a/packages/core/tools/src/invariant.ts +++ b/packages/core/tools/src/invariant.ts @@ -35,7 +35,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant 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 9241dc2492..4ee42f7f55 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 f96a7d4d50..4f6e69efc0 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 = { @@ -102,6 +111,103 @@ describe('first-party Session format catalog', () => { expect(JSON.stringify({ header, rows })).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: 'user/message', seq: 1, time: 11, data: message(oldId), surfaceOp: 'append' }, + { type: 'user/message', seq: 2, time: 12, data: message(newId), surfaceOp: 'append' }, + { type: 'agent/inbox/spliced', seq: 3, time: 13, data: { target: 'next-turn', start: 0, inserted: [message(oldId), message(newId)] } }, + { type: 'tool/code-dispatch-start', seq: 4, time: 14, data: dispatch }, + { type: 'tool/code-dispatch', seq: 5, time: 15, data: { ...dispatch, isError: false, content: [{ type: 'text', text: childId }] } }, + { type: 'user/message', seq: 6, time: 16, data: message('tools-code-mode:replacement'), sourceEventSeqs: [1, 2], surfaceOp: { op: 'replace', start: 1, end: 2 } }, + { type: 'turn/end', seq: 7, 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], data: renamedMessage(oldId) }, + { ...rows[2], data: renamedMessage(newId) }, + { ...rows[3], data: { target: 'next-turn', start: 0, inserted: [renamedMessage(oldId), renamedMessage(newId)] } }, + { ...rows[4], type: 'tool/ptc-dispatch-start' }, + { ...rows[5], type: 'tool/ptc-dispatch' }, + { ...rows[6], data: renamedMessage('tools-code-mode:replacement') }, + rows[7], + ] + 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('validates complete relationships after streaming migration', () => { const stream = sessionFormatCatalog.createRestore({ type: 'session', version: 1, id: 'invalid-stream', createdAt: 1, delegationDepth: 0, 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 9de563d0d6..d5d4e91c37 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: e13c20e55cab2dd043318e8d3a6feaaa3583f3a1 -README.zh.md: 10cf1162d9be4b6a3e9feb455d74b6e845bf817d +README.md: c8248e80d42911bbb29fd92f0a8e95839a777b10 +README.zh.md: ee35b218e5ae0a846354e206354afc57a10cc2b0 diff --git a/packages/session/session-format-v2-to-v3/README.md b/packages/session/session-format-v2-to-v3/README.md index e13c20e55c..c8248e80d4 100644 --- a/packages/session/session-format-v2-to-v3/README.md +++ b/packages/session/session-format-v2-to-v3/README.md @@ -1,5 +1,5 @@ --- -description: "Restore released-v2 Session logs as v3 without changing their events." +description: "Restore released-v2 Session logs as v3 with current PTC event names and preserved historical identities." kind: "package-library" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -This library restores released-v2 Session records as v3 while preserving event payloads, sequence references, timestamps, ordering, and inherited prefixes. Persistence consumes it through the static Session format catalog. It does not publish or modify durable files. +This library restores released-v2 Session records as v3 by translating durable PTC event names and plugin-source labels. It preserves every historical id, event order, sequence reference, timestamp, and inherited cut. Persistence consumes it through the static Session format catalog. It does not publish or modify durable files. ## Table of Contents @@ -35,7 +35,9 @@ Use the [catalog](../session-format-catalog/README.md) for restoration. Direct i const targetHeader = sessionFormatV2ToV3.migrateHeader(sourceHeader) ``` -The header version becomes 3; all other header fields remain unchanged. The stage forwards events and compact runs synchronously. Scalar inherited end-seed markers determine the exact cut at EOF. V3 record encoding and validation reuse the frozen released-v2 implementation without modifying it. Unknown required events remain refusals; installed event types and ignorable unknown events retain their admission rules. +The header version becomes 3; all other header fields remain unchanged. The stage synchronously maps `tool/code-dispatch-start` and `tool/code-dispatch` to `tool/ptc-dispatch-start` and `tool/ptc-dispatch`. It maps the exact `tools-code-mode` plugin label 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`. Every other value remains unchanged, including ids containing `:code:`, message content, tool arguments, and opaque payloads. + +V3 validation accepts current PTC tags, not required legacy aliases. Unknown events marked `ignorable` retain their admission policy, except that source-v2 `tool/ptc-dispatch` and `tool/ptc-dispatch-start` events are rejected even when ignorable: these names are reserved in v3, so migration cannot reinterpret an opaque extension as a PTC lifecycle event. Physical record encoding remains the released-v2 encoding; only the header version and the named logical fields change. ----- @@ -45,7 +47,7 @@ The header version becomes 3; all other header fields remain unchanged. The stag
Implementation internals — click to expand -The [stage](src/migration.ts) tracks the inherited cut and source delivery ownership without changing event values. The [codec](src/codec.ts) changes physical header versions and shares v2 record encoding. The [validator](src/validation.ts) checks the v3 version before applying released-v2 event validation. No runtime invariant companion is published because this library owns no independently observable runtime registrations or state replicas. +The [stage](src/migration.ts) applies the bounded PTC transformation while tracking inherited cuts and source delivery ownership. Scalar inherited end-seed markers determine the exact cut at EOF. The [codec](src/codec.ts) shares frozen v2 physical record encoding. The [validator](src/validation.ts) checks v3 event admission and relationships without modifying frozen predecessor modules. No runtime invariant companion is published because this library owns no independently observable runtime registrations or state replicas.
@@ -66,22 +68,22 @@ The [stage](src/migration.ts) tracks the inherited cut and source delivery owner #### What the model sees -`sessionFormatV2ToV3` preserves every model-visible event and its payload. +`sessionFormatV2ToV3` preserves message content and tool results. PTC plugin-source attribution uses `tools-ptc`; log-only dispatch events do not add model messages. #### 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 - **No file publication** — persistence owns immutable successor publication; this package never overwrites released generations. -- **Identity conversion only** — the stage introduces no structural event transformations. +- **Bounded conversion only** — only the named event tags and plugin-source slots are transformed; arbitrary strings, unknown payloads, and historical ids are not rewritten. ### 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 10cf1162d9..ee35b218e5 100644 --- a/packages/session/session-format-v2-to-v3/README.zh.md +++ b/packages/session/session-format-v2-to-v3/README.zh.md @@ -1,5 +1,5 @@ --- -description: "将已发布的 v2 Session 日志恢复为 v3,保留所有事件。" +description: "将已发布的 v2 Session 日志恢复为 v3,采用当前 PTC 事件名称并保留历史标识。" kind: "package-library" --- @@ -9,7 +9,7 @@ kind: "package-library" ## 概述 -本库将已发布的 v2 Session 记录恢复为 v3,保留事件载荷、序列引用、时间戳、顺序和继承前缀。持久化通过静态 Session 格式目录使用本库。本库不发布或修改持久化文件。 +本库通过转换持久化 PTC 事件名称和插件来源标签,将已发布的 v2 Session 记录恢复为 v3。它保留每个历史标识、事件顺序、序列引用、时间戳和继承切点。持久化通过静态 Session 格式目录使用本库。本库不发布或修改持久化文件。 ## 目录 @@ -35,7 +35,9 @@ kind: "package-library" const targetHeader = sessionFormatV2ToV3.migrateHeader(sourceHeader) ``` -头部版本变为 3,其余头部字段保持不变。阶段同步转发事件和紧凑事件段。标量继承 end-seed 标记在 EOF 确定精确切点。V3 记录编码和校验复用冻结的已发布 v2 实现,不修改该实现。未知必需事件仍被拒绝;已安装事件类型和可忽略的未知事件保留其准入规则。 +头部版本变为 3,其余头部字段保持不变。阶段同步将 `tool/code-dispatch-start` 和 `tool/code-dispatch` 映射为 `tool/ptc-dispatch-start` 和 `tool/ptc-dispatch`。它仅在 `user/message.data.source`、`agent/inbox/spliced.data.inserted[].source` 和 `session/title-llm-request.data.messages[].source` 的 plugin 类型来源中,将精确匹配的 `tools-code-mode` 插件标签映射为 `tools-ptc`。其他所有值保持不变,包括含 `:code:` 的标识、消息内容、工具参数和不透明载荷。 + +V3 校验接受当前 PTC 标签,不接受必需的旧别名。标记为 `ignorable` 的未知事件保留其准入规则,但源 v2 中的 `tool/ptc-dispatch` 和 `tool/ptc-dispatch-start` 事件即使可忽略也会被拒绝:这些名称在 v3 中保留,迁移不能将不透明扩展重新解释为 PTC 生命周期事件。物理记录编码仍为已发布的 v2 编码;仅头部版本和上述逻辑字段发生变化。 ----- @@ -45,7 +47,7 @@ const targetHeader = sessionFormatV2ToV3.migrateHeader(sourceHeader)
实现细节 — 点击展开 -[阶段](src/migration.ts)跟踪继承切点和源投递归属,不改变事件值。[编解码器](src/codec.ts)改变物理头部版本并共享 v2 记录编码。[校验器](src/validation.ts)先检查 v3 版本,再执行已发布 v2 事件校验。本库不拥有可独立观察的运行时注册或状态副本,因此不发布运行时不变量伴随入口。 +[阶段](src/migration.ts)在跟踪继承切点和源投递归属的同时,执行限定范围的 PTC 转换。标量继承 end-seed 标记在 EOF 确定精确切点。[编解码器](src/codec.ts)共享冻结的 v2 物理记录编码。[校验器](src/validation.ts)检查 v3 事件准入和关联关系,不修改冻结的前代模块。本库不拥有可独立观察的运行时注册或状态副本,因此不发布运行时不变量伴随入口。
@@ -66,22 +68,22 @@ const targetHeader = sessionFormatV2ToV3.migrateHeader(sourceHeader) #### 模型看到什么 -`sessionFormatV2ToV3` 保留每个模型可见事件及其载荷。 +`sessionFormatV2ToV3` 保留消息内容和工具结果。PTC 插件来源使用 `tools-ptc`;仅日志的分发事件不会添加模型消息。 #### Token 影响 -不添加或删除模型可见内容。 +不添加或删除消息内容。 #### KV Cache 影响 -模型消息前缀保持不变。 +阶段不改变消息内容或模型配置。 ## 已知限制与后续工作 - **不发布文件** — 持久化负责不可变后继代的发布;本包绝不覆盖已发布代。 -- **仅恒等转换** — 阶段不引入结构性事件转换。 +- **仅限定范围的转换** — 只转换指定的事件标签和插件来源位置;不改写任意字符串、未知载荷和历史标识。 ### 开发备注 diff --git a/packages/session/session-format-v2-to-v3/package.json b/packages/session/session-format-v2-to-v3/package.json index a6225ecc71..7597baa8cc 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": "Released-v2 Session codec and identity streaming migration to v3", + "description": "Released-v2 Session codec and durable PTC streaming migration to 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 8b1b2d7126..ddf2b4d5bd 100644 --- a/packages/session/session-format-v2-to-v3/src/codec.ts +++ b/packages/session/session-format-v2-to-v3/src/codec.ts @@ -4,12 +4,13 @@ import { SessionFormatError, isSessionFormatJsonObject, snapshotSessionFormatJso import type { SessionFormatCodec, SessionFormatCurrentEncoder, + SessionFormatMigrationContext, 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' -/** Physical v3 codec: only the header version differs from released v2. */ +/** Physical v3 codec with current PTC event admission and released-v2 record encoding. */ export const releasedV3SessionFormatCodec = Object.freeze({ version: 3, decodeHeader(value: unknown) { @@ -17,7 +18,19 @@ export const releasedV3SessionFormatCodec = Object.freeze({ }, createDecoder(value, recovery) { const decoder = releasedV2SessionFormatCodec.createDecoder(v2PhysicalHeader(value), recovery) - return { ...decoder, header: { ...decoder.header, version: 3 } } + return { + ...decoder, + header: { ...decoder.header, version: 3 }, + decodeRow(row: unknown, context: SessionFormatMigrationContext) { + decoder.decodeRow(row, { + emitEvent(event) { + assertV3EventAdmission(event) + context.emitEvent(event) + }, + emitRun: context.emitRun.bind(context), + }) + }, + } }, encodeHeader(header, inheritedEventCount) { assertReleasedV3Header(header) 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 de6f05a179..7bb383e714 100644 --- a/packages/session/session-format-v2-to-v3/src/migration.ts +++ b/packages/session/session-format-v2-to-v3/src/migration.ts @@ -1,9 +1,10 @@ -/** Identity adjacent stage; events and compact runs retain their values and ordering. */ +/** Adjacent PTC event and plugin-attribution migration with stable historical identities. */ -import { SessionFormatError, defineSessionFormatMigration, isSessionFormatJsonObject, sessionFormatCount } from '@deepseek-ai/dsh-session-format' +import { SessionFormatError, SessionFormatUnsupportedMigrationError, defineSessionFormatMigration, isSessionFormatJsonObject, sessionFormatCount } from '@deepseek-ai/dsh-session-format' import type { SessionFormatEvent, SessionFormatEventRun, + SessionFormatJsonValue, SessionFormatMigrationContext, SessionFormatMigrationStage, SessionFormatMigrationStageInput, @@ -11,7 +12,7 @@ import type { import { assertReleasedV2Header } from '@deepseek-ai/dsh-session-format-v1-to-v2' import { assertReleasedV3Header } from './validation.ts' -/** Adjacent identity migration from released v2 to v3. */ +/** Rename released-v2 PTC dispatch events and owned plugin attribution without changing ids or content. */ export const sessionFormatV2ToV3 = defineSessionFormatMigration({ name: '@deepseek-ai/dsh-session-format-v2-to-v3', fromVersion: 2, @@ -46,7 +47,7 @@ class ReleasedV2ToV3Stage implements SessionFormatMigrationStage { && event.data['sessionId'] !== this.input.sourceHeader.id) { this.lastForeignDeliverySeq = event.seq } - context.emitEvent(event) + context.emitEvent(renamePtcEvent(event)) } transformRun(run: SessionFormatEventRun, context: SessionFormatMigrationContext): void { @@ -68,3 +69,43 @@ class ReleasedV2ToV3Stage implements SessionFormatMigrationStage { return cut } } + +function renamePtcEvent(event: SessionFormatEvent): SessionFormatEvent { + switch (event.type) { + case 'tool/ptc-dispatch-start': + case 'tool/ptc-dispatch': + throw new SessionFormatUnsupportedMigrationError( + 'format v2 event collides with reserved v3 type ' + JSON.stringify(event.type) + ' at seq ' + String(event.seq), + ) + 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) + return data === event.data ? event : { ...event, data } + } + case 'agent/inbox/spliced': + case 'session/title-llm-request': { + if (!isSessionFormatJsonObject(event.data)) return event + const key = event.type === 'agent/inbox/spliced' ? 'inserted' : 'messages' + const messages = event.data[key] + if (!Array.isArray(messages)) return event + const renamed = messages.map(renameMessageSource) + return renamed.every((message, index) => message === messages[index]) + ? event + : { ...event, data: { ...event.data, [key]: renamed } } + } + default: + // Other event payloads and merge-extensible content remain owner-opaque. + return event + } +} + +function renameMessageSource(message: SessionFormatJsonValue): SessionFormatJsonValue { + if (!isSessionFormatJsonObject(message)) return message + const source = message['source'] + if (!isSessionFormatJsonObject(source) + || 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/validation.ts b/packages/session/session-format-v2-to-v3/src/validation.ts index 01186ac43f..79c991fd00 100644 --- a/packages/session/session-format-v2-to-v3/src/validation.ts +++ b/packages/session/session-format-v2-to-v3/src/validation.ts @@ -1,7 +1,7 @@ -/** V3 validation preserves the released-v2 event model. */ +/** V3 event admission and released lifecycle validation for PTC dispatches. */ -import { SessionFormatError } from '@deepseek-ai/dsh-session-format' -import type { SessionFormatArtifact, SessionFormatHeader } 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' /** @@ -14,7 +14,7 @@ export function assertReleasedV3Header(header: SessionFormatHeader): void { } /** - * Validate v3 event admission, relationships, and inherited cut without copying events. + * Validate v3 event admission, relationships, and inherited cut without changing the artifact. * @param artifact - detached v3 artifact. * @param knownEventTypes - event types understood by the installed Session package. * @returns the same validated artifact. @@ -24,6 +24,40 @@ export function restoreReleasedV3Artifact( knownEventTypes: ReadonlySet, ): SessionFormatArtifact { assertReleasedV3Header(artifact.header) - restoreReleasedV2Artifact({ ...artifact, header: { ...artifact.header, version: 2 } }, knownEventTypes, 3) + restoreReleasedV2Artifact({ + ...artifact, + header: { ...artifact.header, version: 2 }, + events: artifact.events.map(releasedValidationEvent), + }, knownEventTypes, 3) return artifact } + +/** + * Refuse required predecessor PTC tags in a native v3 event. + * @param event - decoded event whose physical envelope was validated. + */ +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), + ) + } +} + +/** Adapt only the validator input; released codecs and returned events keep their own names. */ +function releasedValidationEvent(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) + // An obsolete ignorable event is opaque, not a released-v2 lifecycle contribution. + return { ...event, type: 'v3/opaque-released-event' } + default: + return event + } +} 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 b5351b912a..65a2862490 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 @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { SessionFormatEventCollector } from '@deepseek-ai/dsh-session-format' -import type { SessionFormatEvent, SessionFormatHeader } from '@deepseek-ai/dsh-session-format' +import type { SessionFormatEvent, SessionFormatHeader, SessionFormatJsonValue } from '@deepseek-ai/dsh-session-format' import { assertReleasedV3Header, releasedV2SessionFormatCodec, @@ -9,12 +9,32 @@ import { sessionFormatV2ToV3, } from '../src/index.ts' -const header: SessionFormatHeader = { - version: 2, id: 'identity', createdAt: 1, isSeeded: false, delegationDepth: 0, +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('v2 to v3 identity migration', () => { - it('changes only the header version and forwards events and runs without expansion', () => { +const header: SessionFormatHeader = deepFreeze({ + version: 2, id: 'tools-code-mode:session', createdAt: 1, isSeeded: false, delegationDepth: 0, +}) + +function migrateEvents(events: SessionFormatEvent[]): SessionFormatEvent[] { + deepFreeze(events) + const stage = sessionFormatV2ToV3.createStage({ + sourceHeader: header, targetHeader: sessionFormatV2ToV3.migrateHeader(header), + sourceInheritedEventCount: 0, sourceKind: 'decoded', + }) + const collector = new SessionFormatEventCollector() + for (const event of events) stage.transformEvent(event, collector) + expect(stage.finish(collector)).toBe(0) + return collector.values +} + +describe('v2 to v3 PTC migration', () => { + it('changes the header version and forwards unrelated events and runs without expansion', () => { const target = sessionFormatV2ToV3.migrateHeader(header) expect(target).toEqual({ ...header, version: 3 }) expect(header.version).toBe(2) @@ -35,6 +55,132 @@ describe('v2 to v3 identity migration', () => { expect(stage.finish(context)).toBe(0) }) + + it.each([ + ['tool/code-dispatch-start', 'tool/ptc-dispatch-start'], + ['tool/code-dispatch', 'tool/ptc-dispatch'], + ])('renames only the exact %s envelope tag', (type, targetType) => { + const event = deepFreeze({ + type, seq: 41, time: -17, ignorable: true, + data: { + rootCallId: 'tools-code-mode:root', parentCallId: 'tools-ptc:root', subCallId: 'tool/code-dispatch', + name: 'tool/code-dispatch-start', arguments: { plugin: 'tools-code-mode' }, + content: [{ type: 'text', text: 'tool/code-dispatch tools-code-mode' }], + source: { kind: 'plugin', plugin: 'tools-code-mode' }, extra: { nested: ['tools-code-mode'] }, + }, + sourceEventSeqs: [3, 7], surfaceOp: { op: 'replace', start: 3, end: 7 }, + extension: { type: 'tool/code-dispatch' }, + }) + const before = JSON.stringify(event) + const [output] = migrateEvents([event]) + expect(output).toEqual({ ...event, type: targetType }) + expect(output).not.toBe(event) + expect(output?.data).toBe(event.data) + expect(output?.['sourceEventSeqs']).toBe(event.sourceEventSeqs) + expect(output?.['surfaceOp']).toBe(event.surfaceOp) + expect(output?.['extension']).toBe(event.extension) + expect(JSON.stringify(event)).toBe(before) + }) + + it.each(['user/message', 'agent/inbox/spliced', 'session/title-llm-request'])('rewrites only owned plugin attribution in %s', (type) => { + const message = deepFreeze({ + id: 'tools-code-mode:message', role: 'user', + source: { kind: 'plugin', plugin: 'tools-code-mode', id: 'tools-code-mode:source', extra: ['tools-code-mode'] }, + content: [ + { type: 'text', text: '{"type":"tool/code-dispatch","source":{"kind":"plugin","plugin":"tools-code-mode"}}' }, + { type: 'tool-result', toolCallId: 'tools-code-mode:call', content: [{ type: 'text', text: 'tools-code-mode' }] }, + { type: 'extension', source: { kind: 'plugin', plugin: 'tools-code-mode' } }, + ], + extra: { source: { kind: 'plugin', plugin: 'tools-code-mode' } }, + }) + const untouched = { ...message, id: 'tools-ptc:message', source: { kind: 'user', plugin: 'tools-code-mode' } } + const key = type === 'agent/inbox/spliced' ? 'inserted' : 'messages' + const data = type === 'user/message' ? message : { + [key]: [untouched, message], removed: [message.id], messageSeqs: [1, 4], + source: message.source, extra: { messages: [message] }, + } + const event = deepFreeze({ type, seq: 9, time: 111, data, sourceEventSeqs: [1, 4], surfaceOp: { op: 'replace', start: 1, end: 4 } }) + const renamed = { ...message, source: { ...message.source, plugin: 'tools-ptc' } } + const before = JSON.stringify(event) + const [output] = migrateEvents([event]) + expect(output).toEqual({ ...event, data: type === 'user/message' ? renamed : { ...data, [key]: [untouched, renamed] } }) + expect(output?.['sourceEventSeqs']).toBe(event.sourceEventSeqs) + expect(output?.['surfaceOp']).toBe(event.surfaceOp) + expect(JSON.stringify(event)).toBe(before) + }) + + it('retains no-op messages, malformed extension values, and non-plugin lookalikes by reference', () => { + const messages: SessionFormatJsonValue[] = [ + null, false, 7, 'tools-code-mode', [], {}, { source: null }, { source: [] }, { source: 'tools-code-mode' }, + { source: { plugin: 'tools-code-mode' } }, { source: { kind: 'user', plugin: 'tools-code-mode' } }, + ...['tools-ptc', 'tools-code-mode-extra', '@deepseek-ai/dsh-tools-code-mode', 'Tools-code-mode'].map(plugin => ({ source: { kind: 'plugin', plugin } })), + ] + const events: SessionFormatEvent[] = messages.map((data, seq) => ({ type: 'user/message', seq, time: 1, data })) + for (const type of ['agent/inbox/spliced', 'session/title-llm-request']) { + const key = type === 'agent/inbox/spliced' ? 'inserted' : 'messages' + for (const data of [null, false, [], {}, { [key]: null }, { [key]: {} }, { [key]: [] }, { [key]: messages }]) { + events.push({ type, seq: events.length, time: 1, data }) + } + } + const outputs = migrateEvents(events) + expect(outputs).toEqual(events) + for (const [index, event] of events.entries()) expect(outputs[index]).toBe(event) + }) + + it('leaves spoofed names and message-shaped values in unrelated payloads opaque', () => { + const data = { + type: 'tool/code-dispatch', source: { kind: 'plugin', plugin: 'tools-code-mode' }, + inserted: [{ source: { kind: 'plugin', plugin: 'tools-code-mode' } }], + messages: [{ source: { kind: 'plugin', plugin: 'tools-code-mode' } }], + text: 'tool/code-dispatch-start tools-code-mode', + } + const events = [ + 'tool/code-dispatch-extra', 'tool/code-dispatch-started', 'tool/ptc-dispatch-extra', 'tool/ptc-dispatch-started', + 'external/event', 'assistant/message', 'tool/result', 'agent/inbox/spliced-extra', 'session/title', + ].map((type, seq) => ({ type, seq, time: 1, data, ignorable: true })) + const outputs = migrateEvents(events) + for (const [index, event] of events.entries()) expect(outputs[index]).toBe(event) + }) + + it.each(['decoded', 'transformed'] as const)('refuses required and ignorable V3 tag collisions from %s V2 events', (sourceKind) => { + for (const type of ['tool/ptc-dispatch-start', 'tool/ptc-dispatch']) { + for (const ignorable of [false, true]) { + const stage = sessionFormatV2ToV3.createStage({ + sourceHeader: header, targetHeader: sessionFormatV2ToV3.migrateHeader(header), + sourceInheritedEventCount: 0, sourceKind, + }) + const event = deepFreeze({ type, seq: 0, time: 1, data: null, ...(ignorable ? { ignorable: true } : {}) }) + const collector = new SessionFormatEventCollector() + expect(() => { stage.transformEvent(event, collector) }).toThrow(/format v2.*ptc-dispatch/) + expect(collector.values).toEqual([]) + } + } + }) + + it('does not collapse old-prefix and new-prefix message IDs or rewrite references', () => { + const messages = ['tools-code-mode:result', 'tools-ptc:result'].map(id => ({ + id, role: 'user', content: [{ type: 'text', text: id }], source: { kind: 'plugin', plugin: 'tools-code-mode' }, + })) + const events = [ + ...messages.map((data, seq) => ({ type: 'user/message', seq, time: seq + 1, data })), + { type: 'agent/inbox/spliced', seq: 2, time: 3, data: { inserted: messages, removed: messages.map(message => message.id) } }, + { type: 'external/references', seq: 3, time: 4, data: { messageId: 'tools-code-mode:result', callId: 'tools-code-mode:call' } }, + ] + const before = JSON.stringify(events) + expect(migrateEvents(events)).toEqual([ + ...messages.map((data, seq) => ({ type: 'user/message', seq, time: seq + 1, data: { ...data, source: { ...data.source, plugin: 'tools-ptc' } } })), + { ...events[2], data: { inserted: messages.map(data => ({ ...data, source: { ...data.source, plugin: 'tools-ptc' } })), removed: messages.map(message => message.id) } }, + events[3], + ]) + expect(JSON.stringify(events)).toBe(before) + }) + + it('keeps own and historical delivery markers unchanged', () => { + const events = [null, {}, { sessionFormatVersion: 1, sessionId: 'other' }, { sessionFormatVersion: 2, sessionId: header.id }] + .map((data, seq) => ({ type: 'session-log-deepseek/delivery-accepted', seq, time: 1, data })) + expect(migrateEvents(events)).toEqual(events) + }) + it('derives the inherited cut from scalar end-seed markers', () => { const seeded = { ...header, isSeeded: true } const stage = sessionFormatV2ToV3.createStage({ @@ -80,6 +226,29 @@ describe('v2 to v3 identity migration', () => { expect(context.values).toEqual([first]) }) + + it.each(['tool/code-dispatch-start', 'tool/code-dispatch'])('keeps %s in the frozen v2 codec but rejects it as required native v3 input', (type) => { + const row = deepFreeze({ type, seq: 0, time: 1, data: { text: 'tools-code-mode' } }) + const v2 = releasedV2SessionFormatCodec.createDecoder(releasedV2SessionFormatCodec.encodeHeader(header, 0), 'strict') + const oldEvents = new SessionFormatEventCollector() + v2.decodeRow(row, oldEvents) + expect(v2.finish(oldEvents)).toBe(0) + expect(oldEvents.values).toEqual([row]) + + const physical = releasedV3SessionFormatCodec.encodeHeader({ ...header, version: 3 }, 0) + const required = releasedV3SessionFormatCodec.createDecoder(physical, 'strict') + const rejected = new SessionFormatEventCollector() + expect(() => { required.decodeRow(row, rejected) }).toThrow(/format v3 contains unknown event type/) + expect(rejected.values).toEqual([]) + + const opaqueRow = deepFreeze({ ...row, ignorable: true }) + const ignorable = releasedV3SessionFormatCodec.createDecoder(physical, 'strict') + const accepted = new SessionFormatEventCollector() + ignorable.decodeRow(opaqueRow, accepted) + expect(ignorable.finish(accepted)).toBe(0) + expect(accepted.values).toEqual([opaqueRow]) + }) + it.each([null, [], false, { version: 2 }])('rejects a non-v3 physical header %j', (value) => { expect(() => releasedV3SessionFormatCodec.decodeHeader(value)).toThrow(/format v3 physical/) }) @@ -126,3 +295,76 @@ describe('v2 to v3 identity migration', () => { ] }, new Set())).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/tests/v2-ptc-migration.spec.ts b/packages/session/session-persistence-jsonl/tests/v2-ptc-migration.spec.ts new file mode 100644 index 0000000000..3484508cdb --- /dev/null +++ b/packages/session/session-persistence-jsonl/tests/v2-ptc-migration.spec.ts @@ -0,0 +1,189 @@ +/** 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 { mkdir, mkdtemp, readFile, readdir, rm, 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 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', '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) + 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 = events.map(event => ({ ...event })) + expectedEvents[5] = { ...events[5], type: 'tool/ptc-dispatch-start' } as SessionFormatEvent + expectedEvents[6] = { ...events[6], type: 'tool/ptc-dispatch' } as SessionFormatEvent + expectedEvents[8] = { ...events[8], data: { + target: 'next-step', start: 0, removedCount: 0, inserted: [currentImageMessage], + } } as SessionFormatEvent + expectedEvents[9] = { ...events[9], data: currentImageMessage } as SessionFormatEvent + expect(publishedEvents).toEqual(expectedEvents) + expect(await readFile(predecessor)).toEqual(source) + + 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 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/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/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 0df05a31df..a1341e3bec 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -1402,7 +1402,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"]', @@ -1442,7 +1442,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 6ea2d2b2f9..387b4555e8 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -1301,8 +1301,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 0074eca78f..b8c2f71990 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -170,7 +170,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 11 } }, @@ -338,7 +338,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 17 } }, @@ -544,7 +544,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 24 } }, @@ -651,13 +651,13 @@ } }, { - "type": "tool/code-dispatch-start", + "type": "tool/ptc-dispatch-start", "seq": 28, "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 @@ -665,13 +665,13 @@ } }, { - "type": "tool/code-dispatch", + "type": "tool/ptc-dispatch", "seq": 29, "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 @@ -743,7 +743,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 32 } }, @@ -907,7 +907,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 38 } }, @@ -1111,7 +1111,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 48 } }, @@ -1311,7 +1311,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 55 } }, @@ -1725,7 +1725,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 11 } } @@ -1929,7 +1929,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 17 } } @@ -2177,7 +2177,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 24 } } @@ -2302,13 +2302,13 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "tool/code-dispatch-start", + "type": "tool/ptc-dispatch-start", "seq": 28, "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 @@ -2322,13 +2322,13 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "tool/code-dispatch", + "type": "tool/ptc-dispatch", "seq": 29, "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 @@ -2424,7 +2424,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 32 } } @@ -2819,7 +2819,7 @@ "time": 0, "data": { "sessionId": "{{child-1}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 12 } } @@ -3051,7 +3051,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 38 } } @@ -3478,7 +3478,7 @@ "time": 0, "data": { "sessionId": "{{child-2}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 12 } } @@ -3741,7 +3741,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 48 } } @@ -3983,7 +3983,7 @@ "time": 0, "data": { "sessionId": "{{parent}}", - "sessionFormatVersion": 2, + "sessionFormatVersion": 3, "throughSeq": 55 } } diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v3.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v3.jsonl new file mode 100644 index 0000000000..6482f8e2c9 --- /dev/null +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v3.jsonl @@ -0,0 +1,18 @@ +{"type":"session","version":3,"id":"{{child-1}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","isSeeded":false,"origin":"subagent","delegationDepth":1} +{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} +{"type":"approval/policy","data":{"policy":"never","source":"delegation"}} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn","label":"Check direct child"}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"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`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"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`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","sessionFormatVersion":3,"throughSeq":12}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"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":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DIRECT_CHILD_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v3.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v3.jsonl new file mode 100644 index 0000000000..57fc6bf557 --- /dev/null +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v3.jsonl @@ -0,0 +1,18 @@ +{"type":"session","version":3,"id":"{{child-2}}","createdAt":0,"cwd":"{{cwd}}","parentSession":"{{parent}}","isSeeded":false,"origin":"subagent","delegationDepth":1} +{"type":"sandbox/mode","data":{"mode":"danger-full-access","source":"delegation"}} +{"type":"approval/policy","data":{"policy":"never","source":"delegation"}} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"subagent/descriptor","data":{"version":3,"mode":"one-shot","provider":"spawn"}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"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`).\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"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`)."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[8],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","sessionFormatVersion":3,"throughSeq":12}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"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":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["WORKFLOW_CHILD_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl new file mode 100644 index 0000000000..d370e3bccc --- /dev/null +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.v3.jsonl @@ -0,0 +1,67 @@ +{"type":"session","version":3,"id":"{{parent}}","createdAt":0,"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":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} +{"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":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"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":"{{messageId}}"},"surfaceOp":"append"} +{"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[7],"source":{"kind":"fallback"}}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":11}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"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-define","name":"cordis_define","args":["{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}},{"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":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"step/start","data":{"turn":1,"step":2}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":17}} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"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-run","name":"cordis_run","args":["{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}},{"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":2,"callId":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[20],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":2}} +{"type":"step/start","data":{"turn":1,"step":3}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"change"}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":24}} +{"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/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":[27],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":3}} +{"type":"step/start","data":{"turn":1,"step":4}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":32}} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"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-direct-child","name":"subagent","args":["{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}},{"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":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":4}} +{"type":"step/start","data":{"turn":1,"step":5}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":38}} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"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-workflow","name":"workflow","args":["{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}},{"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":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}} +{"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}} +{"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}} +{"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}} +{"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[41],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":5}} +{"type":"step/start","data":{"turn":1,"step":6}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":48}} +{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"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-undefine","name":"cordis_undefine","args":["{\"pluginId\": \"snap-1\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}},{"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":6,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}} +{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[51],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":6}} +{"type":"step/start","data":{"turn":1,"step":7}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change"}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":3,"throughSeq":55}} +{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{message:1}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["ADVANCED_EXECUTABLE_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":7}} +{"type":"command/run","data":{"commandId":"{{command:1}}","name":"feedback","source":{"kind":"user"}}} +{"type":"feedback/record","data":{"text":"The session needs a clearer explanation."}} +{"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"Feedback recorded for session {{parent}}\nAnonymous user: {{anonymous-user}}."}} +{"type":"feedback/message-put","data":{"sessionId":"{{parent}}","item":{"messageId":"{{message:1}}","rating":"negative","note":"Explain the result.","version":"{{feedback-version:1}}","createdAt":0,"updatedAt":0}}} +{"type":"feedback/message-put","data":{"sessionId":"{{parent}}","item":{"messageId":"{{message:1}}","rating":"positive","note":"The explanation is clear now.","version":"{{feedback-version:2}}","createdAt":0,"updatedAt":0}}} +{"type":"feedback/message-delete","data":{"sessionId":"{{parent}}","messageId":"{{message:1}}"}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/advanced-toolchain/session.v3.jsonl b/snapshots/session/advanced-toolchain/session.v3.jsonl index a14523e74d..2296497e1c 100644 --- a/snapshots/session/advanced-toolchain/session.v3.jsonl +++ b/snapshots/session/advanced-toolchain/session.v3.jsonl @@ -18,10 +18,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:5}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233481651,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233481651,"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":1788233481651,"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":1788233481651,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233481651,"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:6}}"}},"sourceEventSeqs":[18],"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 717608d2ac..bc20ddbb37 100644 --- a/snapshots/session/both-mode-turn/session.v3.jsonl +++ b/snapshots/session/both-mode-turn/session.v3.jsonl @@ -13,8 +13,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:3}}"},"usage":{"inputTokens":10400,"outputTokens":130,"cacheReadTokens":0,"reasoningTokens":34},"stream":[{"type":"chunk","time":1788233486359,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788233486359,"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],"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":1788233486359,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233486359,"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,1,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":1788233486360,"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":1788233486360,"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":1788233486360,"chunk":{"type":"usage","usage":{"inputTokens":10400,"outputTokens":130,"cacheReadTokens":0,"reasoningTokens":34}}},{"type":"chunk","time":1788233486360,"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:4}}"}},"sourceEventSeqs":[13],"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 77a85a6054..7f2506ac67 100644 --- a/snapshots/session/ptc-python-turn/session.v3.jsonl +++ b/snapshots/session/ptc-python-turn/session.v3.jsonl @@ -13,10 +13,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:3}}"},"usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60},"stream":[{"type":"chunk","time":1788270162450,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1788270162450,"index":0,"dt":[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],"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":1788270162451,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788270162451,"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,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,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":1788270162452,"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":1788270162452,"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":1788270162452,"chunk":{"type":"usage","usage":{"inputTokens":6152,"outputTokens":214,"cacheReadTokens":0,"reasoningTokens":60}}},{"type":"chunk","time":1788270162452,"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:4}}"}},"sourceEventSeqs":[13],"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 8432359d39..af616378ac 100644 --- a/snapshots/session/ptc-read-image-attachment-path/session.v3.jsonl +++ b/snapshots/session/ptc-read-image-attachment-path/session.v3.jsonl @@ -13,24 +13,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:3}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233512477,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788233512477,"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":1788233512477,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233512477,"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:4}}"}},"sourceEventSeqs":[13],"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:5}}"}]}} +{"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:5}}"}]}} {"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:5}}"},"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:5}}"},"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:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233512574,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788233512574,"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":1788233512574,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233512574,"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:7}}"}},"sourceEventSeqs":[23],"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:8}}"}]}} +{"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:8}}"}]}} {"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:8}}"},"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:8}}"},"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:9}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233512644,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788233512644,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788233512644,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233512644,"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 72aa2aa06f..47e0b8ee70 100644 --- a/snapshots/session/ptc-read-image/session.v3.jsonl +++ b/snapshots/session/ptc-read-image/session.v3.jsonl @@ -13,16 +13,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:3}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233511677,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1788233511677,"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":1788233511677,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233511677,"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:4}}"}},"sourceEventSeqs":[13],"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:5}}"}]}} +{"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:5}}"}]}} {"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:5}}"},"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:5}}"},"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:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788233511819,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1788233511819,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1788233511819,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788233511819,"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 cbe3a08a2e..1640306f68 100644 --- a/snapshots/session/ptc-turn/session.v3.jsonl +++ b/snapshots/session/ptc-turn/session.v3.jsonl @@ -13,10 +13,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:3}}"},"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/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:4}}"}},"sourceEventSeqs":[13],"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 3e3b83bfd2..94045def19 100644 --- a/snapshots/session/ptc-workspace-context/session.v3.jsonl +++ b/snapshots/session/ptc-workspace-context/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":"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:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1788233514219,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788233514219,"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":1788233514219,"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":1788233514219,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1788233514219,"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:5}}"}},"sourceEventSeqs":[14],"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:6}}"}]}} diff --git a/snapshots/web/ptc-round/session.v3.jsonl b/snapshots/web/ptc-round/session.v3.jsonl index 6f819934f9..868bbb3a1d 100644 --- a/snapshots/web/ptc-round/session.v3.jsonl +++ b/snapshots/web/ptc-round/session.v3.jsonl @@ -13,10 +13,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:3}}"},"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:4}}"}},"sourceEventSeqs":[13],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}}