diff --git a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.i18n.yaml b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.i18n.yaml index f00ecd5d2a..b00bff1000 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.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 -2026-07-26-code-dispatch-log-spill.md: 2668c195a43ae1f6011c09413338a23caf75401e -2026-07-26-code-dispatch-log-spill.zh.md: e084ae80d7fed864c7f296b1fd6db713acf7a2b0 +2026-07-26-code-dispatch-log-spill.md: 65af7808c493867cb13042a4f169ffdf05eb4538 +2026-07-26-code-dispatch-log-spill.zh.md: e1293e62f9de9860300428c5c0d25c5404dc76f9 diff --git a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md index 2668c195a4..65af7808c4 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md +++ b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md @@ -14,7 +14,7 @@ Since the full-content dispatch logging landed, a `run_code` program that reads **A log-shaping waterfall on the registry, and the spill policy as its first listener.** -- **Seam**: `tools/code-dispatch-log` — a scope-filtered waterfall the bridge runs (via `registry.shapeDispatchLog`, contained: a throwing listener falls back to the unshaped content) over each settled sub-dispatch before appending `tool/code-dispatch`. The payload (`CodeDispatchLog`) carries the outer execution, the hoisted `agent` routing key, the sub-call identity, and the default content. Only the durable copy is shapeable — the program already received the complete value across the worker boundary, and the model sees neither. +- **Seam**: `tools/code-dispatch-log` — a scope-filtered waterfall the bridge runs (via `registry.shapeDispatchLog`, contained: a throwing listener falls back to the unshaped content, with total error formatting so a hostile thrown value cannot escape the containment) over each settled sub-dispatch before appending `tool/code-dispatch`. The payload (`CodeDispatchLog`) carries the outer execution, the hoisted `agent` routing key, the sub-call identity, and the default content — the RENDERED result projection a native `tool/result` would carry (the program itself received the structured `value`). Only the durable copy is shapeable; the model sees neither. Shaping runs OFF the program path as tracked side work, but bounded: past `maxParallelSubCalls` pending log tasks the ordered commit lane holds, so a slow spill backend backpressures the run instead of accumulating unbounded pending I/O; run settlement still drains every task inside the open turn. - **Policy**: `dsh-spill-policy` registers a second arm on the new seam sharing the exact replacement pipeline of its model-facing arm (same `maxInlineBytes` cap, same preview + locator + within-cap invariant, same best-effort fallbacks), with the artifact labeled `dispatch` under the sub-call id. UIs and replay read the full text through the spill artifact exactly as they do for spilled native results, so the native-parity rendering story survives bounding. - **One deliberate asymmetry**: the model-facing arm skips `read` (the `read → spill → read again` loop); the dispatch-log arm bounds `read` sub-calls too — a log copy is not model context, so the loop cannot happen, and `read` is precisely the tool that produces huge logs. diff --git a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.zh.md b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.zh.md index e084ae80d7..e1293e62f9 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.zh.md +++ b/.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.zh.md @@ -14,7 +14,7 @@ Status: implemented **在注册表上增设一个日志整形 waterfall(瀑布式事件),spill 策略作为其第一个监听器。** -- **Seam**:`tools/code-dispatch-log`,一个按作用域过滤的 waterfall,由桥接层在追加 `tool/code-dispatch` 之前对每个已结算的子分发运行(经由 `registry.shapeDispatchLog`,且故障被兜住:监听器抛出异常时回退到未整形的内容)。载荷(`CodeDispatchLog`)携带外层执行、提升出来的 `agent` 路由键、子调用标识与默认内容。可整形的只有持久副本:程序已经跨 worker 边界收到了完整的值,而模型两者都看不到。 +- **Seam**:`tools/code-dispatch-log`,一个按作用域过滤的 waterfall,由桥接层在追加 `tool/code-dispatch` 之前对每个已结算的子分发运行(经由 `registry.shapeDispatchLog`,且故障被兜住:监听器抛出异常时回退到未整形的内容,并用全防御的错误格式化确保恶意抛出值无法逃出兜底)。载荷(`CodeDispatchLog`)携带外层执行、提升出来的 `agent` 路由键、子调用标识与默认内容——即原生 `tool/result` 所载的渲染后结果投影(程序本身收到的是结构化 `value`)。可整形的只有持久副本;模型两者都看不到。整形作为被跟踪的旁路工作在程序路径之外运行,但有界:待处理日志任务超过 `maxParallelSubCalls` 时有序提交车道会暂停,因此慢速 spill 后端会对整个 run 施加背压,而不是无限累积待完成 I/O;run 结算仍会在开放轮次内排空全部任务。 - **策略**:`dsh-spill-policy` 在新 seam 上注册第二个分支,与其面向模型的分支共用一模一样的替换流水线(同样的 `maxInlineBytes` 上限、同样的预览 + 定位符 + 不超上限不变式、同样的尽力而为回退),产物以 `dispatch` 为标签,记在子调用 id 名下。UI 与回放通过 spill 产物读取全文,方式与读取被 spill 的原生结果完全相同,因此与原生同等保真的渲染在施加边界之后依然成立。 - **一处有意的不对称**:面向模型的分支跳过 `read`(避免 `read → spill → read again` 循环);分发日志分支则连 `read` 子调用也施加边界:日志副本不是模型上下文,该循环因此不可能发生,而 `read` 恰恰是会产生巨大日志的那个工具。 diff --git a/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.i18n.yaml b/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.i18n.yaml index 6dd3aaf059..91685e9811 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-26-code-mode-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 -2026-07-26-code-mode-live-parallel-dispatch.md: f0d13456d63779fb89b9af4cb09bc90c37356a21 -2026-07-26-code-mode-live-parallel-dispatch.zh.md: 5554ab0456f13a2bbc6d5b18e515930f954c6682 +2026-07-26-code-mode-live-parallel-dispatch.md: b4afc21be902d8ed3e5bee2ad1a540413a864f25 +2026-07-26-code-mode-live-parallel-dispatch.zh.md: 409e4cbf9ea3d1b4d1bbe0cd86b494429ebb8a3d diff --git a/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.md b/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.md index f0d13456d6..b4afc21be9 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.md +++ b/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.md @@ -15,7 +15,7 @@ Two gaps remained after the first two PRs. Sub-call rows appeared only when each **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. -- **Bridge scheduler**: submitted calls are classified at submission via `registry.executionMode` (the SAME fail-closed `isConcurrencySafe` contract the loop uses) and start strictly in submission order. Consecutive parallel-classified calls overlap up to `maxParallelSubCalls` (a validated registry `Config` field, default 10 — the loop scheduler's own default; `1` restores serial dispatch); an exclusive call drains the pool, runs alone, and bars later calls. This is the loop's group semantics adapted to calls that arrive over time instead of in one parsed batch. Run settlement aborts in-flight dispatches and abandons queued-unstarted ones (binding rejection, no events), then drains to quiescence before the outer result closes the turn. +- **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**: `CodeSubCall` widens to `RunningToolCall | ToolResultNode` — a start event lands the running shape in the dispatch index (rows derive the running ring from the shape, exactly as for native in-flight calls), and its settle replaces the 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 code-mode snapshot. diff --git a/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.zh.md b/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.zh.md index 5554ab0456..409e4cbf9e 100644 --- a/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.zh.md +++ b/.agents/notes/implemented/feature/2026-07-26-code-mode-live-parallel-dispatch.zh.md @@ -15,7 +15,7 @@ Status: implemented **一对生命周期事件,一份调度契约,与原生共用。** - **事件对**:`tool/code-dispatch-start`(父/子 id、名称、规范化参数)在调度器真正启动某个调用时才追加,而非在提交时,因此因 run 结算而被放弃的排队调用不会留下任何日志。既有的 `tool/code-dispatch` 结算该事件对(`subCallId` 相同);每个已启动的调用恰好结算一次(中止也会作为 `isError` 结果经由流水线结算)。计时即这两个事件的 `time` 字段。两个事件都保持仅日志;模型上下文不受影响;格式保持 v0。 -- **桥接层调度器**:已提交的调用在提交那一刻就经 `registry.executionMode` 分类(与 loop 所用完全相同的 fail-closed `isConcurrencySafe` 契约),并严格按提交顺序启动。连续被分类为可并行的调用可以重叠执行,上限为 `maxParallelSubCalls`(经校验的注册表 `Config` 字段,默认值 10,即 loop 调度器自身的默认值;设为 `1` 即恢复串行分发);独占调用则先排空池、独自运行,并阻挡其后的调用。这是把 loop 的分组语义适配到另一种场景:调用随时间陆续到达,而非作为单个已解析的批次一次性到达。run 结算时会中止仍在运行的分发,并放弃已排队未启动的分发(绑定调用被拒绝,不产生事件),随后排空到完全停稳,之后外层结果才结束该轮次。 +- **桥接层调度器**:已提交的调用在启动那一刻经 `registry.executionMode` 分类(与 loop 所用完全相同的 fail-closed `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 结算时会中止仍在运行的分发,并放弃已排队未启动的分发(绑定调用被拒绝,不产生事件),随后排空到完全停稳——包括程序返回时已在途的提交——之后外层结果才结束该轮次。 - **client 侧**:`CodeSubCall` 拓宽为 `RunningToolCall | ToolResultNode`:start 事件把运行中形状写入分发索引(行组件从该形状推导出运行指示环,与原生运行中的调用处理完全一致),其结算事件则原位替换该条目,即使并行完成也保持启动顺序不变,并把 start 事件的 `time` 作为 `callTime`(时长来源)带入。未观察到对应 start 的结算事件(窗口切在事件对中间,或日志录制于 start 事件引入之前)会直接追加,因此旧日志仍能照常渲染。 - **SDK 提示词**:面向模型的「调用按顺序执行」一句替换为真实契约(相互独立的安全调用可以在 `Promise.all` 下重叠执行;相互依赖的工作以 `await` 顺序衔接);这是模型可见的变更,每一份 code-mode 快照都已重新录制。 diff --git a/apps/cli/README.md b/apps/cli/README.md index 4ff9034dd3..50571632c0 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -14,6 +14,8 @@ The TUI surface: The Web and headless surfaces boot one shared composition (`cordis.yml`): both treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless `--workspace-root ` overrides it, load applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opt into first-message model titles. Headless differs only in listening on an OS-assigned port (parallel `dsh -p` runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (`pnpm run build && pnpm run build:web`). +`DSH_TOOLS_MODE` selects the tool presentation mode for the whole Web/headless process: `native` (the schema default when unset), `code` (the `run_code`-only Code Mode wire), or `both`; any other value fails loud at boot through the `dsh-tools` config schema. It is a TEMPORARY seam — process-wide because Loader composition is static — and is removed once the web UI owns per-session tool-mode selection; the TUI surface ignores it (its config tree pins its own mode). + ## Install (developer machine) Symlink the source-running launcher onto your PATH; it resolves the checkout through its own real path, so code changes apply on the next launch with no build step: diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 058eb65eeb..cf0b45c1ff 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1718,7 +1718,7 @@ export interface Config { export type ToolPresentationMode = 'native' | 'code' | 'both' ``` -Source: [`packages/core/tools/src/index.ts:564`](../packages/core/tools/src/index.ts) +Source: [`packages/core/tools/src/index.ts:566`](../packages/core/tools/src/index.ts) ## `@deepseek-ai/dsh-tui` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 6facbbb9b3..918382c5fe 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -1859,7 +1859,7 @@ async execute(exec: ToolExecutionInput): Promise Types: [CodeDispatchLog](../core-data-structures/tools.md) · [ContentBlock](../core-data-structures/core.md) · [ScopeKey](../core-data-structures/scope.md) · [ToolDefinition](../core-data-structures/tools.md) · [ToolExecutionInput](../core-data-structures/tools.md) · [ToolExecutionMode](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolGuard](../core-data-structures/tools.md) · [ToolRestriction](../core-data-structures/tools.md) · [ToolSchema](../core-data-structures/tools.md) -Source: [`packages/core/tools/src/index.ts:677`](../../packages/core/tools/src/index.ts) +Source: [`packages/core/tools/src/index.ts:688`](../../packages/core/tools/src/index.ts) ## `ctx.tui` — `TuiExtensionService` (abstract seam) diff --git a/docs/core-data-structures/tools.i18n.yaml b/docs/core-data-structures/tools.i18n.yaml index 19c6cb4612..7c82b890b7 100644 --- a/docs/core-data-structures/tools.i18n.yaml +++ b/docs/core-data-structures/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 -tools.md: 389c54bf625f762257a4830ed915d526230090ab -tools.zh.md: fba3453fa91be2544eb3ab94ca67aaf0452958b2 +tools.md: 250e869397f8ecb128d5b644ff7506376d0657c6 +tools.zh.md: 96fc9d3eeda0240e195beb11bea088d5606d4757 diff --git a/docs/core-data-structures/tools.md b/docs/core-data-structures/tools.md index 389c54bf62..250e869397 100644 --- a/docs/core-data-structures/tools.md +++ b/docs/core-data-structures/tools.md @@ -238,8 +238,10 @@ Code Mode's bridge additionally exposes each settled sub-dispatch to the `tools/ * One settled `run_code` sub-dispatch about to be logged, as seen by the * `tools/code-dispatch-log` waterfall: the parent execution (session owner, * outer call identity), the sub-call identity, and the outcome whose durable - * copy a listener may reshape. The complete `content` is what the program - * already received; only the `tool/code-dispatch` event's copy changes. + * 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. */ interface CodeDispatchLog { /** The outer `run_code` execution. */ diff --git a/docs/core-data-structures/tools.zh.md b/docs/core-data-structures/tools.zh.md index fba3453fa9..96fc9d3eed 100644 --- a/docs/core-data-structures/tools.zh.md +++ b/docs/core-data-structures/tools.zh.md @@ -238,8 +238,10 @@ Code Mode 的桥接层还会把每个已结算的子分派暴露给 `tools/code- * One settled `run_code` sub-dispatch about to be logged, as seen by the * `tools/code-dispatch-log` waterfall: the parent execution (session owner, * outer call identity), the sub-call identity, and the outcome whose durable - * copy a listener may reshape. The complete `content` is what the program - * already received; only the `tool/code-dispatch` event's copy changes. + * 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. */ interface CodeDispatchLog { /** The outer `run_code` execution. */ diff --git a/examples/tui-agent/tests/snapshots/code-mode-dispatch-spill/session.jsonl b/examples/tui-agent/tests/snapshots/code-mode-dispatch-spill/session.jsonl new file mode 100644 index 0000000000..21b88b77ac --- /dev/null +++ b/examples/tui-agent/tests/snapshots/code-mode-dispatch-spill/session.jsonl @@ -0,0 +1,194 @@ +{"type":"session","version":0,"id":"main-session","createdAt":1785052797743,"cwd":"/tmp/dsh-tui-snapshot-code-mode-dispatch-spill-8cOdia"} +{"type":"turn/start","seq":0,"time":1785052797817,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1785052797818,"data":{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool exactly once with the command `seq 1 200 | awk '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}'`, then return ONLY the number of lines in its output. Reply with just that number and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"session/title","seq":2,"time":1785052797825,"data":{"title":"Using ONE run_code program: call","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"step/start","seq":3,"time":1785052797826,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":4,"time":1785052797827,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":5,"time":1785052798220,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":6,"time":1785052798221,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":7,"time":1785052798391,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":8,"time":1785052798421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":9,"time":1785052798421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":10,"time":1785052798421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":11,"time":1785052798421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" write"}}} +{"type":"assistant/chunk","seq":12,"time":1785052798451,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":13,"time":1785052798452,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":14,"time":1785052798452,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} +{"type":"assistant/chunk","seq":15,"time":1785052798452,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_code"}}} +{"type":"assistant/chunk","seq":16,"time":1785052798480,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" program"}}} +{"type":"assistant/chunk","seq":17,"time":1785052798480,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":18,"time":1785052798480,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" calls"}}} +{"type":"assistant/chunk","seq":19,"time":1785052798480,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" bash"}}} +{"type":"assistant/chunk","seq":20,"time":1785052798509,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":21,"time":1785052798539,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" once"}}} +{"type":"assistant/chunk","seq":22,"time":1785052798539,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":23,"time":1785052798569,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":24,"time":1785052798569,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" specific"}}} +{"type":"assistant/chunk","seq":25,"time":1785052798599,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} +{"type":"assistant/chunk","seq":26,"time":1785052798599,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":27,"time":1785052798599,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} +{"type":"assistant/chunk","seq":28,"time":1785052798599,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" returns"}}} +{"type":"assistant/chunk","seq":29,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" only"}}} +{"type":"assistant/chunk","seq":30,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":31,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" number"}}} +{"type":"assistant/chunk","seq":32,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" of"}}} +{"type":"assistant/chunk","seq":33,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" lines"}}} +{"type":"assistant/chunk","seq":34,"time":1785052798629,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} +{"type":"assistant/chunk","seq":35,"time":1785052798659,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" its"}}} +{"type":"assistant/chunk","seq":36,"time":1785052798689,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" output"}}} +{"type":"assistant/chunk","seq":37,"time":1785052798690,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":38,"time":1785052798781,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":39,"time":1785052798781,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":40,"time":1785052798781,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":41,"time":1785052798781,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":42,"time":1785052798809,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":43,"time":1785052798809,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":44,"time":1785052798809,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":45,"time":1785052798809,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":46,"time":1785052798839,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"Count"}}} +{"type":"assistant/chunk","seq":47,"time":1785052798868,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lines"}}} +{"type":"assistant/chunk","seq":48,"time":1785052798869,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" in"}}} +{"type":"assistant/chunk","seq":49,"time":1785052798869,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" seq"}}} +{"type":"assistant/chunk","seq":50,"time":1785052798899,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":51,"time":1785052798929,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"awk"}}} +{"type":"assistant/chunk","seq":52,"time":1785052798930,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" output"}}} +{"type":"assistant/chunk","seq":53,"time":1785052798930,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":54,"time":1785052798960,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":55,"time":1785052798960,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":56,"time":1785052798960,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"code"}}} +{"type":"assistant/chunk","seq":57,"time":1785052798988,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":58,"time":1785052798989,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":59,"time":1785052798989,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":60,"time":1785052798989,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"const"}}} +{"type":"assistant/chunk","seq":61,"time":1785052799019,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" result"}}} +{"type":"assistant/chunk","seq":62,"time":1785052799019,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" ="}}} +{"type":"assistant/chunk","seq":63,"time":1785052799019,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" await"}}} +{"type":"assistant/chunk","seq":64,"time":1785052799019,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" tools"}}} +{"type":"assistant/chunk","seq":65,"time":1785052799019,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":".b"}}} +{"type":"assistant/chunk","seq":66,"time":1785052799020,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"ash"}}} +{"type":"assistant/chunk","seq":67,"time":1785052799048,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"({\\n"}}} +{"type":"assistant/chunk","seq":68,"time":1785052799049,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":69,"time":1785052799049,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" command"}}} +{"type":"assistant/chunk","seq":70,"time":1785052799049,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":":"}}} +{"type":"assistant/chunk","seq":71,"time":1785052799049,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":72,"time":1785052799049,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"seq"}}} +{"type":"assistant/chunk","seq":73,"time":1785052799107,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":74,"time":1785052799108,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"1"}}} +{"type":"assistant/chunk","seq":75,"time":1785052799108,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":76,"time":1785052799123,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"200"}}} +{"type":"assistant/chunk","seq":77,"time":1785052799123,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" |"}}} +{"type":"assistant/chunk","seq":78,"time":1785052799123,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" awk"}}} +{"type":"assistant/chunk","seq":79,"time":1785052799123,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" '{"}}} +{"type":"assistant/chunk","seq":80,"time":1785052799123,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"printf"}}} +{"type":"assistant/chunk","seq":81,"time":1785052799162,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" \\\\\\\""}}} +{"type":"assistant/chunk","seq":82,"time":1785052799162,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"line"}}} +{"type":"assistant/chunk","seq":83,"time":1785052799163,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" %"}}} +{"type":"assistant/chunk","seq":84,"time":1785052799163,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"04"}}} +{"type":"assistant/chunk","seq":85,"time":1785052799191,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"d"}}} +{"type":"assistant/chunk","seq":86,"time":1785052799191,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":":"}}} +{"type":"assistant/chunk","seq":87,"time":1785052799191,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":88,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" quick"}}} +{"type":"assistant/chunk","seq":89,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" brown"}}} +{"type":"assistant/chunk","seq":90,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" fox"}}} +{"type":"assistant/chunk","seq":91,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" jumps"}}} +{"type":"assistant/chunk","seq":92,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" over"}}} +{"type":"assistant/chunk","seq":93,"time":1785052799220,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":94,"time":1785052799250,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lazy"}}} +{"type":"assistant/chunk","seq":95,"time":1785052799250,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" dog"}}} +{"type":"assistant/chunk","seq":96,"time":1785052799250,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\\\\\\"}}} +{"type":"assistant/chunk","seq":97,"time":1785052799250,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"n"}}} +{"type":"assistant/chunk","seq":98,"time":1785052799250,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\\\\\","}}} +{"type":"assistant/chunk","seq":99,"time":1785052799251,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" $"}}} +{"type":"assistant/chunk","seq":100,"time":1785052799280,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"1"}}} +{"type":"assistant/chunk","seq":101,"time":1785052799281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"}'"}}} +{"type":"assistant/chunk","seq":102,"time":1785052799281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\",\\n"}}} +{"type":"assistant/chunk","seq":103,"time":1785052799281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":104,"time":1785052799281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" description"}}} +{"type":"assistant/chunk","seq":105,"time":1785052799310,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":":"}}} +{"type":"assistant/chunk","seq":106,"time":1785052799310,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":107,"time":1785052799310,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"Generate"}}} +{"type":"assistant/chunk","seq":108,"time":1785052799350,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":109,"time":1785052799350,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"200"}}} +{"type":"assistant/chunk","seq":110,"time":1785052799350,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lines"}}} +{"type":"assistant/chunk","seq":111,"time":1785052799370,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" of"}}} +{"type":"assistant/chunk","seq":112,"time":1785052799371,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" text"}}} +{"type":"assistant/chunk","seq":113,"time":1785052799400,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\"\\n"}}} +{"type":"assistant/chunk","seq":114,"time":1785052799400,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"});\\n\\n"}}} +{"type":"assistant/chunk","seq":115,"time":1785052799430,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"//"}}} +{"type":"assistant/chunk","seq":116,"time":1785052799430,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" Count"}}} +{"type":"assistant/chunk","seq":117,"time":1785052799460,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lines"}}} +{"type":"assistant/chunk","seq":118,"time":1785052799460,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" in"}}} +{"type":"assistant/chunk","seq":119,"time":1785052799460,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" stdout"}}} +{"type":"assistant/chunk","seq":120,"time":1785052799491,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\n"}}} +{"type":"assistant/chunk","seq":121,"time":1785052799491,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"const"}}} +{"type":"assistant/chunk","seq":122,"time":1785052799491,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lines"}}} +{"type":"assistant/chunk","seq":123,"time":1785052799521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" ="}}} +{"type":"assistant/chunk","seq":124,"time":1785052799521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" result"}}} +{"type":"assistant/chunk","seq":125,"time":1785052799521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":".k"}}} +{"type":"assistant/chunk","seq":126,"time":1785052799551,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"ind"}}} +{"type":"assistant/chunk","seq":127,"time":1785052799551,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" ==="}}} +{"type":"assistant/chunk","seq":128,"time":1785052799551,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":129,"time":1785052799581,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"fore"}}} +{"type":"assistant/chunk","seq":130,"time":1785052799581,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"ground"}}} +{"type":"assistant/chunk","seq":131,"time":1785052799582,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":132,"time":1785052799582,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" ?"}}} +{"type":"assistant/chunk","seq":133,"time":1785052799611,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" result"}}} +{"type":"assistant/chunk","seq":134,"time":1785052799611,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":".stdout"}}} +{"type":"assistant/chunk","seq":135,"time":1785052799611,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":".text"}}} +{"type":"assistant/chunk","seq":136,"time":1785052799641,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":".trim"}}} +{"type":"assistant/chunk","seq":137,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"()."}}} +{"type":"assistant/chunk","seq":138,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"split"}}} +{"type":"assistant/chunk","seq":139,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"(\\\"\\\\"}}} +{"type":"assistant/chunk","seq":140,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"n"}}} +{"type":"assistant/chunk","seq":141,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\\\")."}}} +{"type":"assistant/chunk","seq":142,"time":1785052799672,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"length"}}} +{"type":"assistant/chunk","seq":143,"time":1785052799702,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" :"}}} +{"type":"assistant/chunk","seq":144,"time":1785052799702,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":145,"time":1785052799702,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"0"}}} +{"type":"assistant/chunk","seq":146,"time":1785052799702,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":";\\n"}}} +{"type":"assistant/chunk","seq":147,"time":1785052799703,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"return"}}} +{"type":"assistant/chunk","seq":148,"time":1785052799703,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":" lines"}}} +{"type":"assistant/chunk","seq":149,"time":1785052799731,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":";"}}} +{"type":"assistant/chunk","seq":150,"time":1785052799731,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":151,"time":1785052799731,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":152,"time":1785052799793,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single run_code program that calls bash exactly once with a specific command, then returns only the number of lines in its output."}}}} +{"type":"assistant/chunk","seq":153,"time":1785052799793,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","arguments":"{\"description\": \"Count lines in seq/awk output\", \"code\": \"const result = await tools.bash({\\n command: \\\"seq 1 200 | awk '{printf \\\\\\\"line %04d: the quick brown fox jumps over the lazy dog\\\\\\\\n\\\\\\\", $1}'\\\",\\n description: \\\"Generate 200 lines of text\\\"\\n});\\n\\n// Count lines in stdout\\nconst lines = result.kind === \\\"foreground\\\" ? result.stdout.text.trim().split(\\\"\\\\n\\\").length : 0;\\nreturn lines;\"}"}}}} +{"type":"assistant/chunk","seq":154,"time":1785052799794,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":90,"outputTokens":186,"cacheReadTokens":3968,"reasoningTokens":32}}}} +{"type":"assistant/chunk","seq":155,"time":1785052799794,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":156,"time":1785052799798,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to write a single run_code program that calls bash exactly once with a specific command, then returns only the number of lines in its output."},{"type":"tool-call","id":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","arguments":"{\"description\": \"Count lines in seq/awk output\", \"code\": \"const result = await tools.bash({\\n command: \\\"seq 1 200 | awk '{printf \\\\\\\"line %04d: the quick brown fox jumps over the lazy dog\\\\\\\\n\\\\\\\", $1}'\\\",\\n description: \\\"Generate 200 lines of text\\\"\\n});\\n\\n// Count lines in stdout\\nconst lines = result.kind === \\\"foreground\\\" ? result.stdout.text.trim().split(\\\"\\\\n\\\").length : 0;\\nreturn lines;\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":90,"outputTokens":186,"cacheReadTokens":3968,"reasoningTokens":32}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155],"surfaceOp":"append"} +{"type":"tool/call","seq":157,"time":1785052799799,"data":{"turn":1,"step":1,"callId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","name":"run_code","arguments":"{\"description\": \"Count lines in seq/awk output\", \"code\": \"const result = await tools.bash({\\n command: \\\"seq 1 200 | awk '{printf \\\\\\\"line %04d: the quick brown fox jumps over the lazy dog\\\\\\\\n\\\\\\\", $1}'\\\",\\n description: \\\"Generate 200 lines of text\\\"\\n});\\n\\n// Count lines in stdout\\nconst lines = result.kind === \\\"foreground\\\" ? result.stdout.text.trim().split(\\\"\\\\n\\\").length : 0;\\nreturn lines;\"}"}} +{"type":"tool/code-dispatch-start","seq":158,"time":1785052799893,"data":{"parentCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","subCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490:code:1","name":"bash","arguments":{"command":"seq 1 200 | awk '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}'","description":"Generate 200 lines of text"}}} +{"type":"tool/code-dispatch","seq":159,"time":1785052799923,"data":{"parentCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","subCallId":"call_00_R6g9Uzx4h0jeUv9g3fno7490:code:1","name":"bash","arguments":{"command":"seq 1 200 | awk '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}'","description":"Generate 200 lines of text"},"isError":false,"content":[{"type":"text","text":"line 0001: the quick brown fox jumps over the lazy dog\nline 0002: the quick brown fox jumps over the lazy dog\nline 0003: the quick brown fox jumps over the lazy dog\nline 0004: the quick s over the lazy dog\nline 0198: the quick brown fox jumps over the lazy dog\nline 0199: the quick brown fox jumps over the lazy dog\nline 0200: the quick brown fox jumps over the lazy dog\n\n\n(Omitted 10629 bytes. Full formatted result stored at: /tmp/dsh-tui-snapshot-code-mode-dispatch-spill-8cOdia/.spill/session-2d2b9e84a250/825a63550249-bash.txt. Use read with offset/limit, or grep this path to search within it.)"}]}} +{"type":"tool/result","seq":160,"time":1785052799925,"data":{"turn":1,"step":1,"callId":"call_00_R6g9Uzx4h0jeUv9g3fno7490","content":[{"type":"text","text":"200"}],"isError":false},"sourceEventSeqs":[157],"surfaceOp":"append"} +{"type":"step/end","seq":161,"time":1785052799926,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":162,"time":1785052799928,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":163,"time":1785052800414,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":164,"time":1785052800415,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":165,"time":1785052800572,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" result"}}} +{"type":"assistant/chunk","seq":166,"time":1785052800604,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} +{"type":"assistant/chunk","seq":167,"time":1785052800604,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":168,"time":1785052800604,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"200"}}} +{"type":"assistant/chunk","seq":169,"time":1785052800604,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" lines"}}} +{"type":"assistant/chunk","seq":170,"time":1785052800605,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":171,"time":1785052800635,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} +{"type":"assistant/chunk","seq":172,"time":1785052800636,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":173,"time":1785052800666,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":174,"time":1785052800666,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":175,"time":1785052800666,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":176,"time":1785052800666,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":177,"time":1785052800699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":178,"time":1785052800700,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} +{"type":"assistant/chunk","seq":179,"time":1785052800700,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":180,"time":1785052800700,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" number"}}} +{"type":"assistant/chunk","seq":181,"time":1785052800700,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":182,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} +{"type":"assistant/chunk","seq":183,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":184,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":185,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"200"}}} +{"type":"assistant/chunk","seq":186,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The result is 200 lines. The user wants me to reply with just that number and stop."}}}} +{"type":"assistant/chunk","seq":187,"time":1785052800731,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"200"}}}} +{"type":"assistant/chunk","seq":188,"time":1785052800732,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":33,"outputTokens":22,"cacheReadTokens":4224,"reasoningTokens":20}}}} +{"type":"assistant/chunk","seq":189,"time":1785052800732,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":190,"time":1785052800733,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The result is 200 lines. The user wants me to reply with just that number and stop."},{"type":"text","text":"200"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":33,"outputTokens":22,"cacheReadTokens":4224,"reasoningTokens":20}},"sourceEventSeqs":[163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189],"surfaceOp":"append"} +{"type":"step/end","seq":191,"time":1785052800733,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":192,"time":1785052800733,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/tui-agent/tests/snapshots/code-mode-dispatch-spill/terminal.expected.txt b/examples/tui-agent/tests/snapshots/code-mode-dispatch-spill/terminal.expected.txt new file mode 100644 index 0000000000..aad4b2cd50 --- /dev/null +++ b/examples/tui-agent/tests/snapshots/code-mode-dispatch-spill/terminal.expected.txt @@ -0,0 +1,65 @@ +terminal 100x36 buffer=normal length=36 base=0 viewport=0 +lifecycle started=1 stopped=0 progress=inactive +title "Using ONE run_code program: call — DSH TUI snapshot" +cursor hidden column=1 viewportRow=26 bufferRow=26 +buffer +0| " DEEPSEEK HARNESS" + style 1-8 fg=bright-blue bold + style 10-16 bold +1| " Using ONE run_code program: call" + style 1-32 fg=bright-black +2| " deepseek-v4-flash • main-session" + style 1-34 dim +3| +4| "▌ " + style 0-0 fg=bright-blue +5| "▌ You " + style 0-0 fg=bright-blue + style 2-4 fg=bright-blue bold +6| "▌ Using ONE run_code program: call the bash tool exactly once with the command seq 1 200 | awk " + style 0-0 fg=bright-blue + style 79-99 fg=cyan +7| "▌ '{printf \"line %04d: the quick brown fox jumps over the lazy dog\\n\", $1}', then return ONLY the " + style 0-0 fg=bright-blue + style 2-74 fg=cyan +8| "▌ number of lines in its output. Reply with just that number and stop. " + style 0-0 fg=bright-blue +9| "▌ " + style 0-0 fg=bright-blue +10| +11| " Reasoning " + style 1-9 fg=bright-black italic +12| " The user wants me to write a single run_code program that calls bash exactly once with a specific " + style 1-99 fg=bright-black italic +13| " command, then returns only the number of lines in its output. " + style 1-61 fg=bright-black italic +14| +15| "▌ " + style 0-0 fg=green +16| "▌ ✓ Count lines in seq/awk output " + style 0-0 fg=green + style 2-2 fg=green bold + style 3-32 bold +17| "▌ 200 " + style 0-0 fg=green +18| "▌ " + style 0-0 fg=green +19| +20| " Reasoning " + style 1-9 fg=bright-black italic +21| " The result is 200 lines. The user wants me to reply with just that number and stop. " + style 1-83 fg=bright-black italic +22| +23| " Assistant " + style 1-9 fg=bright-magenta bold +24| " 200 " +25| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +26| " " + style 1-1 inverse +27| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +28| "deepseek-v4-flash /workspace/project ↑123 ↓208 cache 99% 3% c" + style 0-93 dim + style 96-99 dim +29-35| diff --git a/examples/tui-agent/tests/tui.snapshot.ts b/examples/tui-agent/tests/tui.snapshot.ts index 26ba64f23b..1341a22291 100644 --- a/examples/tui-agent/tests/tui.snapshot.ts +++ b/examples/tui-agent/tests/tui.snapshot.ts @@ -27,6 +27,8 @@ import * as ToolTodo from '@deepseek-ai/dsh-tool-todo' import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph' import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow' import { createTuiChat, FILE_REFERENCE_PROMPT } from '@deepseek-ai/dsh-tui' +import LocalSpillStore from '@deepseek-ai/dsh-spill-local' +import * as SpillPolicy from '@deepseek-ai/dsh-spill-policy' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread' import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-terminal.ts' @@ -56,6 +58,13 @@ interface Scenario { * mounts it; the rest cover the default, todo-free composition. */ enableTodo?: boolean + /** + * Mount the spill stack (local backend + policy) with this inline cap, as the + * shipped configs do. The dispatch-spill scenario proves the durable + * `tool/code-dispatch` copy of an oversized sub-result is bounded to a + * preview + locator while the program value stays whole. + */ + spillMaxInlineBytes?: number } const SCENARIOS: Scenario[] = [ @@ -96,6 +105,14 @@ const SCENARIOS: Scenario[] = [ expectedEventCounts: { 'tool/code-dispatch': 2 }, recorded: true, }, + { + name: 'code-mode-dispatch-spill', + composition: 'code', + expectedTools: ['run_code'], + expectedEventCounts: { 'tool/code-dispatch-start': 1, 'tool/code-dispatch': 1 }, + recorded: true, + spillMaxInlineBytes: 600, + }, { name: 'dynamic-workflow', composition: 'native', @@ -225,6 +242,10 @@ async function mountScenarioContext( if (scenario.composition === 'code' || scenario.composition === 'advanced') { await ctx.plugin(WorkerCodeRuntime, {}) } + if (scenario.spillMaxInlineBytes !== undefined) { + await ctx.plugin(LocalSpillStore, { root: join(cwd, '.spill') }) + await ctx.plugin(SpillPolicy, { maxInlineBytes: scenario.spillMaxInlineBytes }) + } if (scenario.composition === 'advanced') await ctx.plugin(ToolCordis, { vmTimeoutMs: 5_000 }) if (MODE === 'record' && scenario.recorded) { await ctx.plugin(LlmDeepSeek) @@ -344,6 +365,17 @@ async function runScenario(scenario: Scenario): Promise { expect(events.filter(event => event.type === 'user/message' && event.data.source.kind === 'plugin').map(event => (event.data as { content: unknown }).content)) .toContainEqual([{ type: 'text', text: 'The user switched this session back to the default mode.' }]) } + if (scenario.spillMaxInlineBytes !== undefined) { + // The REAL pipeline ran (tools execute on replay too): the durable + // dispatch copy is bounded to a preview + locator under the run cwd, + // while the outer result still carries the program's whole value. + const dispatch = events.find(event => (event.type as string) === 'tool/code-dispatch') + const content = (dispatch?.data as { content: { type: string; text?: string }[] }).content + const text = content.filter(block => block.type === 'text').map(block => block.text ?? '').join('') + expect(Buffer.byteLength(text, 'utf8')).toBeLessThanOrEqual(scenario.spillMaxInlineBytes) + expect(text).toContain('Full formatted result stored at:') + expect(text).toContain('.spill') + } expect(events.filter(event => event.type === 'tool/result').every(event => !event.data.isError)).toBe(true) expect(events.filter(event => event.type === 'turn/end').every(event => event.data.reason.kind !== 'error')).toBe(true) if (scenario.name === 'dynamic-workflow' || scenario.name === 'cordis-dynamic-toolchain') { diff --git a/packages/client/runtime/README.md b/packages/client/runtime/README.md index 12bfaa459b..2768a8a58d 100644 --- a/packages/client/runtime/README.md +++ b/packages/client/runtime/README.md @@ -14,7 +14,7 @@ SlotsService gives the renderer separate bare observables for `useSessions` and ## Code Mode sub-dispatch index -`ConversationSnapshot.codeDispatches` groups a `run_code` call's sub-dispatches under their parent callId, in start order, using the native call-block shapes: a started-but-unsettled sub-call is a `RunningToolCall` (rows derive the running ring from the shape) and its `tool/code-dispatch` settlement replaces it in place with the `ToolResultNode` form, `callTime` carrying the paired start's time. Live mux frames and history replay build the identical index; sub-calls never join the surface `nodes` flow; per-parent array and map references are memo-stable across unrelated snapshot swaps. +`ConversationSnapshot.codeDispatches` groups a `run_code` call's sub-dispatches under their parent callId, in start order, using the native call-block shapes: a `tool/code-dispatch-start` event lands the `RunningToolCall` form (rows derive the running ring from the shape) and its `tool/code-dispatch` settlement replaces it in place with the `ToolResultNode` form, `callTime` carrying the paired start's time. A settle whose start fell outside the replay window appends directly with `callTime: null` (duration unknown — never a fabricated zero). Live mux frames and history replay build the identical index; sub-calls never join the surface `nodes` flow; per-parent array and map references are memo-stable across unrelated snapshot swaps. ## Session title projection diff --git a/packages/client/runtime/tests/session.spec.ts b/packages/client/runtime/tests/session.spec.ts index 43745a8324..f5cf9a2138 100644 --- a/packages/client/runtime/tests/session.spec.ts +++ b/packages/client/runtime/tests/session.spec.ts @@ -685,6 +685,9 @@ describe('run_code sub-dispatch indexing', () => { expect(subs?.[0]).toMatchObject({ kind: 'tool-result', callId: 'p1:code:1', call: { name: 'bash', argsRaw: '{"command":"ls","description":"列目录"}' }, + // The settle event carries no start time: callTime stays null (never a + // fabricated zero-duration). + callTime: null, isError: false, content: [{ type: 'text', text: 'demo.txt' }], }) expect(subs?.[1]).toMatchObject({ callId: 'p1:code:2', isError: true }) diff --git a/packages/core/tools/src/code-mode.ts b/packages/core/tools/src/code-mode.ts index f8d7857dc6..f45bea489c 100644 --- a/packages/core/tools/src/code-mode.ts +++ b/packages/core/tools/src/code-mode.ts @@ -249,108 +249,119 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => let dispatches = 0 // The per-run scheduler, reusing the NATIVE concurrency contract through - // the registry's staged view (the loop scheduler's own seam): submitted - // calls START strictly in submission order; only the around-dispatch/body - // stage overlaps — ordered pre-execute runs at start time and ordered - // post-execute/context commitment runs in submission order through the - // commit cursor below, so stateful policy listeners observe submission - // order exactly as they do under the native loop. Consecutive - // parallel-classified calls overlap up to maxParallel; an exclusive call - // waits for the pool to drain, runs alone, and bars later calls. - // Classification is re-read via executionMode() immediately before each - // start (a registry mutation while queued can flip a call exclusive), - // matching the native scheduler's lazy reclassification. + // the registry's staged view (the loop scheduler's own seam) — and the + // native loop's SEQUENCING: every ordered stage (the dispatch-start + // append, prepare = pre-execute/guards, finalize/finish = post-execute, + // context deferral, the settle append) runs inside ONE driver lane, so + // ordered policy stages never overlap each other and only the + // around-dispatch/body stage runs concurrently. Starts are strictly + // submission-ordered; results commit in submission order through the + // head-of-line cursor. Consecutive parallel-classified calls overlap up + // to maxParallel; an exclusive call waits for the pool to drain, runs + // alone, and holds its barrier until its COMMIT (post-execute included) + // completes, exactly like a native exclusive group. Classification is + // re-read via executionMode() immediately before each start (a registry + // mutation while queued can flip a call exclusive), matching the native + // scheduler's lazy reclassification. interface PendingDispatch { - /** Ordered stage: append the start event, prepare, dispatch (body overlaps), park for commit. */ + /** Ordered stage: append the start event, await prepare (pre-execute/guards), launch the body into `flight`. */ start(): Promise classify(): 'parallel' | 'exclusive' abandon(): void /** Ordered stage: post-execute + context deferral + settle event, in submission order. */ commit(): Promise - /** Set once the dispatch stage settles; commit() runs after this resolves. */ - dispatched?: Promise + /** The launched around-dispatch/body stage; resolved until start() replaces it. */ + flight: Promise + /** True once the dispatch stage parked its outcome; the commit cursor waits on it. */ + settled: boolean + /** The classification this entry started under; an exclusive holds its barrier through commit(). */ + mode?: 'parallel' | 'exclusive' } const pendingQueue: PendingDispatch[] = [] const inFlight = new Set>() /** Tracked settle-event side work (log shaping + append), drained at run settlement. */ const logWork = new Set>() const commitQueue: PendingDispatch[] = [] - let committing = false let exclusiveActive = false - let pumping = false - /** Ordered commit cursor: drain the head-of-line settled dispatches one at a time. */ - const commitReady = async (): Promise => { - if (committing) return - committing = true - try { - while (commitQueue.length > 0) { - const head = commitQueue[0] - /* v8 ignore next -- the loop condition bounds the index. */ - if (head === undefined) break - /* v8 ignore next -- entries join commitQueue only after start() set dispatched (see pump). */ - if (head.dispatched === undefined) break - await head.dispatched - commitQueue.shift() - await head.commit() - } - } finally { - committing = false - } + let driving = false + let driverRun: Promise = Promise.resolve() + let wake: (() => void) | undefined + const wakeup = (): void => { + const release = wake + wake = undefined + release?.() } - const pump = (): void => { - // Defensive re-entry guard: today every caller (binding submission, - // flight.finally, drain) runs off promise callbacks, never while pump - // is on the stack, so this cannot fire — kept against a future - // synchronous caller. - /* v8 ignore next -- see the re-entry note above. */ - if (pumping) return - pumping = true - try { - for (;;) { - const head = pendingQueue[0] - if (head === undefined) return - if (runController.signal.aborted) { - pendingQueue.shift() - head.abandon() - continue + /** + * The single ordered lane. Each pass commits the head-of-line settled + * dispatch (ordered post-execute), then starts the next queued entry if + * its slot is free (ordered pre-execute), and otherwise sleeps until a + * body settles or a new submission arrives. One run reaching the + * empty-queues/empty-pool state is quiescence. + */ + const drive = (): Promise => { + if (driving) return driverRun + driving = true + driverRun = (async () => { + try { + for (;;) { + // Arm before inspecting state so a settle or submission landing + // between the checks and the await below cannot be lost. + const signal = new Promise((resolve) => { wake = resolve }) + const commitHead = commitQueue[0] + if (commitHead !== undefined && commitHead.settled) { + commitQueue.shift() + await commitHead.commit() + // The barrier covers post-execute: later starts wait for the + // exclusive call's full pipeline, as under the native loop. + if (commitHead.mode === 'exclusive') exclusiveActive = false + continue + } + const head = pendingQueue[0] + if (head !== undefined) { + if (runController.signal.aborted) { + pendingQueue.shift() + head.abandon() + continue + } + // Reclassify at start time (fail-closed on registry changes). + const mode = head.classify() + const capacity = !exclusiveActive + && (mode === 'exclusive' ? inFlight.size === 0 : inFlight.size < maxParallel) + if (capacity) { + if (mode === 'exclusive') exclusiveActive = true + head.mode = mode + pendingQueue.shift() + // Joined before start() so the commit cursor sees submission + // order; nothing commits it until `settled` flips. + commitQueue.push(head) + await head.start() + const flight: Promise = head.flight.finally(() => { + inFlight.delete(flight) + wakeup() + }) + inFlight.add(flight) + continue + } + } + if (pendingQueue.length === 0 && commitQueue.length === 0 && inFlight.size === 0) return + await signal } - // Reclassify at start time (fail-closed on registry changes). - const mode = head.classify() - if (exclusiveActive || inFlight.size >= (mode === 'exclusive' ? 1 : maxParallel)) return - // The guard above already returned for an exclusive head with any - // in-flight sibling, so claiming the barrier here is race-free. - if (mode === 'exclusive') exclusiveActive = true - pendingQueue.shift() - const flight = head.start().finally(() => { - inFlight.delete(flight) - if (mode === 'exclusive') exclusiveActive = false - // Commit ordering and slot refill are independent: the cursor - // may wait head-of-line on an earlier dispatch while later - // slots keep starting. - void commitReady() - pump() - }) - // Joined AFTER start() ran synchronously, so every commitQueue - // entry already carries its `dispatched` promise. - commitQueue.push(head) - inFlight.add(flight) + } finally { + driving = false + wake = undefined } - } finally { - pumping = false - } + })() + return driverRun } - /** Every in-flight dispatch settled AND committed; nothing can start (the run is aborted at call time). */ + /** Every dispatch settled AND committed; nothing can start (the run is aborted at call time). */ const drainDispatches = async (): Promise => { - // Abandon queued-unstarted tasks first, then await the live set until quiescent. - pump() - while (inFlight.size > 0) await Promise.allSettled([...inFlight]) - await commitReady() - // Every settle's shaped append lands inside the open run_code turn. - while (logWork.size > 0) { - const pending = [...logWork] - await Promise.allSettled(pending) - for (const done of pending) logWork.delete(done) - } + // The abort already fired: the driver abandons queued-unstarted + // entries, awaits the live pool, and drains the ordered commit lane — + // including a commit already in progress when the program returned. + await drive() + // Every settle's shaped append lands inside the open run_code turn + // (tasks self-remove on settlement). + while (logWork.size > 0) await Promise.allSettled([...logWork]) } // Read through a call, not a bare property: the abort state genuinely @@ -376,7 +387,7 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => type DispatchOutcome = { isError: true; message: string } | { isError: false; value: JsonValue } const scheduler = registry[TOOL_REGISTRY_SCHEDULER] const outcome = await new Promise((resolve, reject) => { - // Set by start(): what commit() finalizes in submission order. + // Set by the dispatch stage (or start() for a pre-settled result): what commit() finalizes in submission order. let parked: | { kind: 'post-result' | 'final-result'; exec: ToolRunContext; result: ToolExecutionResult } | undefined @@ -392,7 +403,7 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => : { isError: false, value: result.value }) const agent = exec.agent if (agent === undefined) return - logWork.add((async () => { + const task: Promise = (async () => { // The durable copy may be reshaped (e.g. spilled to a preview + // locator) by the log-shaping waterfall; the program's value // and the model contract are untouched. @@ -414,37 +425,41 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => isError: result.isError, content: logged, }) - })()) + })().finally(() => { logWork.delete(task) }) + logWork.add(task) } pendingQueue.push({ - // Re-read per pump pass against the same agent view the SDK + flight: Promise.resolve(), + settled: false, + // Re-read per driver pass against the same agent view the SDK // declared; fail-closed exclusive when undeclared/invalid. classify: () => registry.executionMode(input).kind, abandon: () => { reject(new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`)) }, - start(): Promise { + async start(): Promise { exec.agent?.session.append('tool/code-dispatch-start', { parentCallId: exec.callId, subCallId, name, arguments: normalized.logged, }) - // Ordered prepare (pre-execute/guards) runs here — starts are - // strictly submission-ordered; only dispatch overlaps. - this.dispatched = (async () => { - const prepared = await scheduler.prepare(input) - if (prepared.kind === 'dispatch') { - const dispatchOutcome = await scheduler.dispatch(prepared.exec) + // Ordered prepare runs INSIDE the driver lane: the next entry's + // pre-execute waits for this resolution, as under the native + // scheduler. Only the launched body below overlaps. + const prepared = await scheduler.prepare(input) + if (prepared.kind === 'dispatch') { + this.flight = scheduler.dispatch(prepared.exec).then((dispatchOutcome) => { parked = { kind: dispatchOutcome.kind, exec: prepared.exec, result: dispatchOutcome.result } - return - } - parked = { kind: prepared.kind, exec: prepared.exec, result: prepared.result } - })() - return this.dispatched + this.settled = true + }) + return + } + parked = { kind: prepared.kind, exec: prepared.exec, result: prepared.result } + this.settled = true }, async commit(): Promise { - /* v8 ignore next -- commit() runs only after this.dispatched resolved, which set parked. */ + /* v8 ignore next -- commit() runs only after `settled` flipped, which set parked. */ if (parked === undefined) return const result = parked.kind === 'post-result' ? await scheduler.finalize(parked.exec, parked.result) @@ -453,9 +468,16 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => exec.deferContext(context) } settle(result) + // Backpressure on the shaped-append side channel: pending log + // tasks (each retaining a full result while a slow backend + // stores it) are bounded by the pool cap — beyond it the + // ordered lane waits, so later sub-calls cannot start and + // pending I/O/memory cannot grow without bound. + while (logWork.size > maxParallel) await Promise.race(logWork) }, }) - pump() + wakeup() + void drive() }) // A budget expiry or outer cancel that lands while this call was in // flight already aborted the dispatch; stop the program now rather diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 0593e6ec51..5536cc4753 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -289,8 +289,10 @@ export type ToolExecutionMode = * One settled `run_code` sub-dispatch about to be logged, as seen by the * `tools/code-dispatch-log` waterfall: the parent execution (session owner, * outer call identity), the sub-call identity, and the outcome whose durable - * copy a listener may reshape. The complete `content` is what the program - * already received; only the `tool/code-dispatch` event's copy changes. + * 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. */ export interface CodeDispatchLog { /** The outer `run_code` execution. */ @@ -670,6 +672,15 @@ interface FusedToolSignal { dispose(): void } +/** Resolve the run_code overlap cap at the owning config boundary (direct construction bypasses the Loader schema). */ +function resolveMaxParallelSubCalls(value: number | undefined): number { + const maxParallelSubCalls = value ?? 10 + if (!Number.isInteger(maxParallelSubCalls) || maxParallelSubCalls < 1) { + throw new Error('maxParallelSubCalls must be a positive integer') + } + return maxParallelSubCalls +} + /** * Tool registry and execution pipeline. Scoped registrations shadow globals; * one visibility resolver feeds presentation, lookup, and dispatch. @@ -716,7 +727,7 @@ export class ToolRegistry extends Service { // the filterable global/scoped capability layers. this.codeTransport = this.mode === 'native' ? undefined - : createRunCodeTool(this, () => this.requireCodeRuntime(), config.maxParallelSubCalls ?? 10) + : createRunCodeTool(this, () => this.requireCodeRuntime(), resolveMaxParallelSubCalls(config.maxParallelSubCalls)) ctx.systemPrompt.tools(context => this.wireSchemas(context.scope)) if (this.mode !== 'native') { ctx.systemPrompt.section({ @@ -982,7 +993,7 @@ export class ToolRegistry extends Service { () => Promise.resolve(dispatch.content), ) } catch (error: unknown) { - this.ctx.logger.warn(`tools: code-dispatch-log listener failed for ${dispatch.name}: ${String(error)}; logging the unshaped content`) + this.ctx.logger.warn(`tools: code-dispatch-log listener failed for ${dispatch.name}: ${errorMessage(error)}; logging the unshaped content`) return dispatch.content } } diff --git a/packages/core/tools/tests/code-mode.spec.ts b/packages/core/tools/tests/code-mode.spec.ts index b77bef0fec..343f542222 100644 --- a/packages/core/tools/tests/code-mode.spec.ts +++ b/packages/core/tools/tests/code-mode.spec.ts @@ -510,6 +510,113 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => { expect(calls).toEqual([]) }) + it('ordered pre-execute never overlaps: a slow policy on one call delays the next start', async () => { + const { ctx, runtime } = await setup({ mode: 'code' }) + const gated = registerGated(ctx, 'safe_read', true) + const stages: string[] = [] + let releaseGate: (() => void) | undefined + ctx.on('tools/pre-execute', async (preExec, next) => { + if (preExec.name !== 'safe_read') return next() + stages.push(`pre-enter:${String(preExec.callId)}`) + if (releaseGate === undefined) { + // The FIRST call's policy awaits an asynchronous decision. + await new Promise((resolve) => { releaseGate = resolve }) + } + stages.push(`pre-exit:${String(preExec.callId)}`) + return next() + }) + runtime.behavior = async (request) => { + const tools = request.bindings[0]!.functions + const all = Promise.all([tools.safe_read!({ id: 'a' }), tools.safe_read!({ id: 'b' })]) + // 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']) + releaseGate!() + await expect.poll(() => gated.pending()).toBe(2) + gated.releaseAll() + await all + return { logs: [], value: 'ordered-prepare' } + } + 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', + ]) + }) + + it('an exclusive call holds its barrier through post-execute: the next start waits for the commit', async () => { + const { ctx, runtime } = await setup({ mode: 'code' }) + const writer = registerGated(ctx, 'writer', false) + const reader = registerGated(ctx, 'safe_read', true) + const stages: string[] = [] + let releasePost: (() => void) | undefined + ctx.on('tools/post-execute', async (postExec, _result, next): Promise => { + if (postExec.name === 'writer') { + stages.push('post-enter:writer') + await new Promise((resolve) => { releasePost = resolve }) + stages.push('post-exit:writer') + } + return next() + }) + runtime.behavior = async (request) => { + const tools = request.bindings[0]!.functions + const w = tools.writer!({ id: 'w' }) + const r = tools.safe_read!({ id: 'r' }) + await expect.poll(() => writer.pending()).toBe(1) + writer.release() + // The writer's body is done and its async post-execute is running; the + // parallel read must not have STARTED (no pre/body) while the exclusive + // call's pipeline is still open. + await expect.poll(() => stages).toContain('post-enter:writer') + expect(reader.pending()).toBe(0) + releasePost!() + await w + await expect.poll(() => reader.pending()).toBe(1) + reader.releaseAll() + await r + return { logs: [], value: 'barrier-through-commit' } + } + const result = await runCode(ctx, 'program') + expect(result.isError).toBe(false) + expect(stages).toEqual(['post-enter:writer', 'post-exit:writer']) + }) + + it('run settlement drains a commit already in progress: the settle event lands inside the turn', async () => { + const { ctx, runtime } = await setup({ mode: 'code' }) + const gated = registerGated(ctx, 'safe_read', true) + const { agent, events } = fakeAgent() + let releasePost: (() => void) | undefined + ctx.on('tools/post-execute', async (postExec, _result, next): Promise => { + if (postExec.name === 'safe_read') { + await new Promise((resolve) => { releasePost = resolve }) + } + return next() + }) + runtime.behavior = async (request) => { + // Fire-and-forget: the program returns while the sub-call's async + // post-execute commit is mid-flight. + request.bindings[0]!.functions.safe_read!({ id: 'a' }).catch(() => 'run-over') + await expect.poll(() => gated.pending()).toBe(1) + gated.release() + await expect.poll(() => releasePost !== undefined).toBe(true) + queueMicrotask(() => { releasePost!() }) + return { logs: [], value: 'returned-early' } + } + const result = await runCode(ctx, 'program', { agent }) + expect(result.isError).toBe(false) + // The drain awaited the in-progress commit: the settle event exists and + // preceded the run_code turn closing (all appends happen inside + // execute()). The run's settlement aborted the sub-call's signal while + // 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') + expect(settles).toHaveLength(1) + expect(settles[0]?.data).toMatchObject({ name: 'safe_read', isError: true }) + }) + it('post-execute and context commitment stay in submission order under out-of-order completion', async () => { const { ctx, runtime } = await setup({ mode: 'code' }) const gated = registerGated(ctx, 'safe_read', true) @@ -1308,6 +1415,13 @@ describe('the run_code dispatch bridge', () => { expect(derived[0]?.role).toBe('user') }) + it('direct construction rejects a non-positive parallel sub-call cap at load', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt, {}) + expect(() => new ToolRegistry(ctx, { mode: 'code', maxParallelSubCalls: 0 })) + .toThrow('maxParallelSubCalls must be a positive integer') + }) + it('direct construction in code mode defaults the parallel sub-call cap', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt, {}) diff --git a/packages/spill/spill-policy/tests/spill-policy.spec.ts b/packages/spill/spill-policy/tests/spill-policy.spec.ts index f132c0f98a..32b9483dca 100644 --- a/packages/spill/spill-policy/tests/spill-policy.spec.ts +++ b/packages/spill/spill-policy/tests/spill-policy.spec.ts @@ -29,9 +29,12 @@ const testToolSignal = new AbortController().signal class StubStore extends SpillStore { saves: SaveTextSpill[] = [] fail = false + /** Per-save hang hook: each call awaits the returned promise before completing. */ + gate: (() => Promise) | undefined async saveText(input: SaveTextSpill): Promise { if (this.fail) throw new Error('disk full') + await this.gate?.() this.saves.push(input) return { locator: SpillLocator(`/spill/${input.suggestedName}`), @@ -367,6 +370,63 @@ describe('the durable dispatch-log arm', () => { expect(smallAfterHuge).toBe(true) }) + it('a sustained slow backend backpressures the run instead of accumulating unbounded log tasks', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + // Cap 1: once the hung shaped-append backlog exceeds the cap, the ordered + // lane holds inside the second commit, so the THIRD dispatch cannot start + // until a pending save drains — the bound is observable as its missing + // start event. + await ctx.plugin(ToolRegistry, { mode: 'code', maxParallelSubCalls: 1 }) + await ctx.plugin(StubStore) + await ctx.plugin(SpillPolicy, { maxInlineBytes: 100 }) + await ctx.plugin(WorkerCodeRuntime, {}) + const store = ctx.spillStore as StubStore + const releases: (() => void)[] = [] + store.gate = () => new Promise((resolve) => { releases.push(resolve) }) + const events: { type: string; data: unknown }[] = [] + const agent = { + session: { + header: { id: SessionId('dispatch-spill-bound'), cwd: '/workspace' }, + append: (type: string, data: unknown) => { events.push({ type, data }) }, + }, + } + 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 runPromise = ctx.tools.execute({ + signal: testToolSignal, + callId: CallId('parent-bound'), + name: 'run_code', + arguments: { + code: 'await tools.huge_read({}); await tools.huge_read({}); await tools.huge_read({}); return "done"', + description: 'Three oversized reads against a hung backend', + }, + agent: agent as never, + }) + // Two hung saves = backlog above the cap: the lane must hold before + // starting dispatch 3. + await vi.waitFor(() => { + if (releases.length < 2) throw new Error('second hung save not reached yet') + }) + expect(started(2)).toBe(true) + expect(started(3)).toBe(false) + releases.shift()!() + // Draining one pending save releases the lane; dispatch 3 starts. + await vi.waitFor(() => { + if (!started(3)) throw new Error('third dispatch not started yet') + }) + while (releases.length > 0) releases.shift()!() + const result = await runPromise + 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) { + throw new Error('settle events still pending') + } + }) + }) + it('a saveText failure keeps the complete content in the durable log (best-effort)', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt)