From e68496fd7981d5da6a3dd44d9ca5c9fc18bd8398 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 22 Jun 2026 08:39:36 +0800 Subject: [PATCH 1/3] Add per-session snapshot replay for nested agents (PR2.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The snapshot tier was built single-session: dsh-llm-replay served calls from one global positional cursor, and the harness harvested one session log. A subagent runs as a second agent with its own session, so a parent→child scenario could neither replay deterministically nor harvest the child's log. This resolves the TODO(subagent-snapshots) deferral from the subagent RFC. - Stamp the calling session id onto the model request: GenerateOptions.sessionId (typed Branded<'SessionId'> to avoid the dsh-llm↔dsh-session cycle), set by the agent loop from agent.session.id. Adapters ignore it; an llm/stream listener routes by it. - Key replay per session: dsh-llm-replay loads the parent log plus one per child (childFiles / $DSH_SNAPSHOT_CHILD_FILES), derives a script per recorded session, and binds each live (freshly-random) session to a recorded script by first-call order — parent first (earliest createdAt, first to stream). Keys by WHO calls, so it survives a future concurrent/backgrounded subagent; a global cursor would not. An unrecorded extra session fails loud. - Harvest every log: the harness collects all .jsonl across cwd buckets, ordered primary-first (top-level, then children by createdAt), and RunResult exposes the plural sessionLogs. The spec writes each back on record (session.jsonl + session..jsonl) and diffs each against its fixture on replay. - Wire the subagent seam + spawn + fork + tool into the acp-agent example (both cordis configs) and add two nested scenarios recorded against the real API: subagent-spawn (parent + 1 child) and subagent-multi (parent + 2 children, 3 sessions). Both replay keyless in the default gate. A new RFC documents the design (docs/rfc/implemented/testing/). Single-session replay is unchanged (a call with no sessionId is one anonymous primary session). TODO follow-up: a dedicated branded-ids package could own the SessionId brand and dissolve the cross-package cycle note; out of scope for this testing PR. --- docs/core-data-structures/core.md | 14 + docs/rfc/README.md | 1 + .../2026-06-22-subagent-snapshot-replay.md | 52 ++++ .../2026-06-21-subagent-capability-seam.md | 2 +- examples/acp-agent/cordis.snapshot.yml | 36 ++- examples/acp-agent/cordis.yml | 38 ++- examples/acp-agent/tests/acp.snapshot.ts | 77 ++++-- examples/acp-agent/tests/snapshot-harness.ts | 87 +++++-- .../tests/snapshots/subagent-multi/input.json | 7 + .../snapshots/subagent-multi/session.1.jsonl | 35 +++ .../snapshots/subagent-multi/session.2.jsonl | 33 +++ .../snapshots/subagent-multi/session.jsonl | 213 ++++++++++++++++ .../subagent-multi/stdout.golden.jsonl | 115 +++++++++ .../tests/snapshots/subagent-spawn/input.json | 7 + .../snapshots/subagent-spawn/session.1.jsonl | 35 +++ .../snapshots/subagent-spawn/session.jsonl | 142 +++++++++++ .../subagent-spawn/stdout.golden.jsonl | 89 +++++++ packages/core/agent-loop/src/loop.ts | 1 + packages/llm/llm/src/types.ts | 15 ++ packages/support/llm-replay/README.md | 23 +- packages/support/llm-replay/src/index.ts | 219 +++++++++++++--- .../llm-replay/tests/llm-replay.spec.ts | 239 +++++++++++++++++- 22 files changed, 1392 insertions(+), 88 deletions(-) create mode 100644 docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md create mode 100644 examples/acp-agent/tests/snapshots/subagent-multi/input.json create mode 100644 examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl create mode 100644 examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl create mode 100644 examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl create mode 100644 examples/acp-agent/tests/snapshots/subagent-multi/stdout.golden.jsonl create mode 100644 examples/acp-agent/tests/snapshots/subagent-spawn/input.json create mode 100644 examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl create mode 100644 examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl create mode 100644 examples/acp-agent/tests/snapshots/subagent-spawn/stdout.golden.jsonl diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 49c1559c40..ad884aeff6 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -140,6 +140,20 @@ interface GenerateOptions { */ stop?: string[] signal?: AbortSignal + /** + * The id of the session this request belongs to — stamped by the agent loop + * from `agent.session.id`. Adapters ignore it; it lets an `llm/stream` listener + * route a call by WHICH session issued it (the replay adapter keys its per-call + * cursor by session, so a parent and its in-process subagent — each with its + * own session on one context — replay from their own recorded scripts). + * + * Typed as `Branded<'SessionId'>` rather than importing `SessionId` from + * `dsh-session`: that package imports `Message` from here, so importing its + * `SessionId` back would cycle. `SessionId` IS `Branded<'SessionId'>`, so a + * real session id assigns with no cast. (A future ids package could own the + * brand and dissolve this note.) + */ + sessionId?: Branded<'SessionId'> } ``` diff --git a/docs/rfc/README.md b/docs/rfc/README.md index 41bb1c537f..87b6456ab8 100644 --- a/docs/rfc/README.md +++ b/docs/rfc/README.md @@ -139,6 +139,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r | [ACP snapshot tests — record-once / replay-deterministic](implemented/testing/2026-06-19-acp-snapshot-tests.md) | 2026-06-19 | | [Real-API e2e in CI against the external DeepSeek API](implemented/testing/2026-06-19-real-api-e2e-ci.md) | 2026-06-19 | | [Use `session.jsonl` as the only snapshot session-log artifact](implemented/testing/2026-06-20-remove-redundant-snapshot-log-goldens.md) | 2026-06-20 | +| [Per-session snapshot replay for nested agents](implemented/testing/2026-06-22-subagent-snapshot-replay.md) | 2026-06-22 | ## Rejected diff --git a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md b/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md new file mode 100644 index 0000000000..92a324104e --- /dev/null +++ b/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md @@ -0,0 +1,52 @@ +# RFC: Per-session snapshot replay for nested agents + +Status: implemented + +## Problem + +The snapshot tier (`pnpm run test:snapshot`) boots the real `acp-agent` subprocess, replays a recorded session through [`dsh-llm-replay`](../../../../packages/support/llm-replay), and diffs the normalized stdout transcript + re-persisted session log against committed goldens. It is the only tier that exercises the full editor-facing transcript end to end. + +It was built for ONE session per process, and that assumption is wired into two places: + +- **`dsh-llm-replay` keyed nothing.** It served the Nth `llm/stream` call the Nth recorded entry from a single global cursor. With a parent agent AND an in-process subagent both streaming on one context, the calls interleave and the single cursor hands the child the parent's script (and vice versa). +- **The harness harvested one log.** `findSessionLog` walked the sessions root and returned the FIRST `.jsonl` it found. A subagent runs as a second `Session` with its own log in the same cwd bucket, so the child's transcript was silently dropped. + +This was the `TODO(subagent-snapshots)` deferral recorded in the [subagent seam RFC](../../proposed/feature/2026-06-21-subagent-capability-seam.md): the in-process backends (PR2) shipped with unit + e2e coverage, but the full-transcript snapshot tier could not express a nested-agent shape until this infrastructure landed. This RFC is that stacked follow-up. + +## Decision + +Replay is keyed **per calling session**, and the harness harvests **every** session log. + +### 1. The calling session id rides on the model request + +`GenerateOptions` gains an optional `sessionId`, stamped by the agent loop from `agent.session.id` at request-assembly time (where the session is already in scope). Adapters ignore it; it exists so an `llm/stream` listener can route a call by WHICH session issued it. It is typed `Branded<'SessionId'>` (from `dsh-brand`) rather than importing `SessionId` from `dsh-session` — that package imports `Message` from `dsh-llm`, so importing its id back would cycle. `SessionId` IS `Branded<'SessionId'>`, so a real id assigns with no cast. (A future dedicated ids package could own the brand and dissolve the note; tracked separately — it touches every id import and does not belong in this testing PR.) + +### 2. Replay binds live sessions to recorded scripts by first-call order + +A nested scenario records more than one log: the parent (`session.jsonl`) plus one per subagent child (`session.1.jsonl`, …). `dsh-llm-replay` loads them all, derives one script per recorded session, and orders the scripts by header `createdAt` (the parent is created before its children). + +Live session ids are freshly random every run and never equal the recorded ones, so a live session cannot bind to a script by id equality. Instead it binds by **first-call order**: the first live session to make any model call claims the first ordered script (the parent — earliest `createdAt`, and necessarily the first to stream, because it must run a turn before it can delegate), the next new live session claims the next script, and so on. Each session then advances its own cursor independently. + +This keys by WHO calls, not by global call order — so it stays correct even if subagents ever run concurrently or in the background (a global cursor would interleave them). A call carrying no `sessionId` (a direct unit-test `stream()`) is treated as one anonymous session bound to the primary script, so the single-session path is byte-for-byte the old behavior. More distinct live sessions than recorded scripts is a fail-loud error (an unrecorded subagent appeared), never a silent mis-route. + +The alternative considered and rejected was a **call-ordered merge of the parent and child logs** into one global script (sound only because in-process subagent execution is strictly nested — the parent blocks on the child). It is simpler for today's synchronous cut but bakes in the parent-blocks-on-child invariant that a future backgrounded/concurrent subagent would break; per-session keying does not. + +### 3. The harness harvests every log, primary-first + +`harvestSessionLogs` collects every `.jsonl` across every cwd bucket under the sessions root (the JSONL backend puts a parent and its same-cwd child in the same bucket), parses each header, and orders them primary-first: the top-level session (no `parentSession`) leads, then each child by ascending `createdAt`. `RunResult.sessionLogs` is the plural result; the spec writes each back to its fixture on record (`session.jsonl` + `session..jsonl`) and diffs each harvested log against its fixture on replay. The normalizer already accepted plural session ids and collapses any stray UUID, so no normalizer change was needed. + +### 4. Scenarios + +Two nested scenarios were added and recorded against the real API: + +- **`subagent-spawn`** — the parent delegates one subtask via the `subagent` tool to a fresh spawn child (2 sessions). +- **`subagent-multi`** — the parent delegates two subtasks, each to its own spawn child (3 sessions), stressing the per-session keying with three concurrent scripts and the `createdAt` ordering of two children under one parent. + +Both replay keyless in the default gate. + +## Consequences + +- The `TODO(subagent-snapshots)` deferral is resolved: nested-agent transcripts are now a first-class snapshot shape. +- `GenerateOptions.sessionId` is a small, honest core-seam addition useful beyond replay (telemetry, request routing). +- The `subagent` tool is bound to a single provider, so both children in `subagent-multi` are spawn (fresh). The fork backend is loaded in the example and exercised by PR2's unit tests; a mixed spawn+fork snapshot would need a second tool instance bound to `fork` (pure config) and is a trivial future addition, not a gap in the keying — the keying routes by session, not by backend. +- Out-of-process (ACP) subagents are a different replay shape entirely (each child is its own PROCESS with its own replay), tracked as `TODO(acp-subagent-replay)` in the PR3 plan. diff --git a/docs/rfc/proposed/feature/2026-06-21-subagent-capability-seam.md b/docs/rfc/proposed/feature/2026-06-21-subagent-capability-seam.md index d99637b550..71a277ff5b 100644 --- a/docs/rfc/proposed/feature/2026-06-21-subagent-capability-seam.md +++ b/docs/rfc/proposed/feature/2026-06-21-subagent-capability-seam.md @@ -70,4 +70,4 @@ The `dsh-tool-subagent` consumer awaits `run.result` and returns the child's fin - **Blocking the parent turn.** Synchronous collect holds the parent's `runStep` open for the child's full duration. This is acceptable for the first cut; **background / poll / spill semantics are deferred to a future redesign that unifies long-running-tool handling across subagents AND bash** (a sub-agent and a long `bash` background task pose the same "the model started something slow, how does it collect later" problem, and should share one mechanism rather than each inventing its own). - **Live progress.** This cut surfaces only lifecycle + final result; a per-chunk child→parent update stream is deferred with the background redesign. - **ACP client surface.** Proxying `fs`/`terminal` from the ACP child back to the parent (a shared-workspace mode) is future work; the first cut advertises neither, so the child self-serves in its own process. -- **Snapshot coverage of nested agents.** The snapshot tier (`pnpm run test:snapshot`) replays a recorded session through `dsh-llm-replay`, whose dispatch is a single GLOBAL positional cursor (the Nth `llm/stream` call serves the Nth recorded entry) and whose harness harvests a single session log file. A subagent runs as a *second* agent with its own session log, so a parent→child scenario needs per-session-keyed replay (or a call-ordered merge of both logs, sound because subagent execution is strictly nested/non-concurrent — the parent blocks on the child) plus harvest-all-logs and plural-session-id plumbing in the harness. This is self-contained infrastructure orthogonal to the backends, so it lands as a **dedicated stacked follow-up** rather than in the in-process-backends PR. Until it lands, in-process subagents are covered by real-loop unit tests (a parent driving a fork AND a spawn child) and a with-key e2e (a parent delegating to a child that writes a file), not by the snapshot transcript tier. Tracked by `TODO(subagent-snapshots)`. +- **Snapshot coverage of nested agents.** The snapshot tier (`pnpm run test:snapshot`) replays a recorded session through `dsh-llm-replay`. It was built single-session: a single GLOBAL positional cursor (the Nth `llm/stream` call serves the Nth recorded entry) and a harness that harvested a single session log file. A subagent runs as a *second* agent with its own session log, so a parent→child scenario needed per-session-keyed replay plus harvest-all-logs and plural-session-id plumbing — self-contained infrastructure orthogonal to the backends, scheduled as a dedicated stacked follow-up rather than folded into the in-process-backends PR. That follow-up has **landed**: see [Per-session snapshot replay for nested agents](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md). Replay now keys each call by its calling session (`GenerateOptions.sessionId`) and binds live sessions to recorded scripts by first-call order; the harness harvests every log; and two nested scenarios (`subagent-spawn`, `subagent-multi`) replay keyless in the default gate. In-process subagents remain covered by real-loop unit tests and a with-key e2e in addition to the snapshot tier. diff --git a/examples/acp-agent/cordis.snapshot.yml b/examples/acp-agent/cordis.snapshot.yml index b57920668a..5bee10f2a7 100644 --- a/examples/acp-agent/cordis.snapshot.yml +++ b/examples/acp-agent/cordis.snapshot.yml @@ -31,9 +31,33 @@ systemPrompt: | You are a coding assistant driven over the Agent Client Protocol. - Your only tools are bash (plus bash_output/bash_kill for background - tasks). Do ALL file operations through bash: read with cat/sed/head, - search with grep, write with heredocs (cat <<'EOF' > file), edit with - sed or a rewrite. Each bash call runs in a fresh shell — pass workdir - instead of cd. Check the [exit code: N] marker; verify your work. Keep - answers brief and factual. + Your tools are bash (plus bash_output/bash_kill for background tasks) + and subagent. Do ALL file operations through bash: read with + cat/sed/head, search with grep, write with heredocs (cat <<'EOF' > + file), edit with sed or a rewrite. Each bash call runs in a fresh + shell — pass workdir instead of cd. Check the [exit code: N] marker; + verify your work. Keep answers brief and factual. + + Use the subagent tool to delegate a focused, self-contained subtask to + a fresh child agent (it works in its own context and returns only its + final result) — give it a complete, standalone instruction. + +# The subagent seam + both in-process backends + the model-facing `subagent` +# tool — identical to cordis.yml's wiring (only the LLM backend differs above). +- id: subagent + name: '@deepseek-ai/dsh-subagent' + +- id: subagent-spawn + name: '@deepseek-ai/dsh-subagent-spawn' + config: + providerName: spawn + +- id: subagent-fork + name: '@deepseek-ai/dsh-subagent-fork' + config: + providerName: fork + +- id: tool-subagent + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml index e456e8ff05..e00e868dce 100644 --- a/examples/acp-agent/cordis.yml +++ b/examples/acp-agent/cordis.yml @@ -40,9 +40,35 @@ systemPrompt: | You are a coding assistant driven over the Agent Client Protocol. - Your only tools are bash (plus bash_output/bash_kill for background - tasks). Do ALL file operations through bash: read with cat/sed/head, - search with grep, write with heredocs (cat <<'EOF' > file), edit with - sed or a rewrite. Each bash call runs in a fresh shell — pass workdir - instead of cd. Check the [exit code: N] marker; verify your work. Keep - answers brief and factual. + Your tools are bash (plus bash_output/bash_kill for background tasks) + and subagent. Do ALL file operations through bash: read with + cat/sed/head, search with grep, write with heredocs (cat <<'EOF' > + file), edit with sed or a rewrite. Each bash call runs in a fresh + shell — pass workdir instead of cd. Check the [exit code: N] marker; + verify your work. Keep answers brief and factual. + + Use the subagent tool to delegate a focused, self-contained subtask to + a fresh child agent (it works in its own context and returns only its + final result) — give it a complete, standalone instruction. + +# The subagent seam + both in-process backends + the model-facing `subagent` +# tool, as leaf entries after the app (which provides ctx.agents/ctx.tools). The +# tool is bound to the `spawn` backend (a fresh child); the `fork` backend is +# loaded too so a multi-child scenario can exercise both transports. +- id: subagent + name: '@deepseek-ai/dsh-subagent' + +- id: subagent-spawn + name: '@deepseek-ai/dsh-subagent-spawn' + config: + providerName: spawn + +- id: subagent-fork + name: '@deepseek-ai/dsh-subagent-fork' + config: + providerName: fork + +- id: tool-subagent + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts index be6aabada0..ffdea0f94e 100644 --- a/examples/acp-agent/tests/acp.snapshot.ts +++ b/examples/acp-agent/tests/acp.snapshot.ts @@ -3,7 +3,7 @@ import { existsSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' import { describe, expect, it } from 'vitest' -import { type InputScript, runScenario } from './snapshot-harness.ts' +import { type HarvestedLog, type InputScript, runScenario } from './snapshot-harness.ts' import { type NormalizeContext, normalizeSessionLog, normalizeStdout } from './snapshot-normalize.ts' /** @@ -37,6 +37,14 @@ interface Scenario { * coaxed into deterministically) are NEVER re-recorded. */ recorded: boolean + /** + * How many SUBAGENT child sessions this scenario records beyond the top-level + * one (0 for a single-session scenario). Each child rides in a sibling fixture + * `session..jsonl` (1-based); replay forwards them to `dsh-llm-replay` so + * each child session replays from its own script, and record mode writes the + * harvested child logs back to those files. Defaults to 0. + */ + childSessions?: number } const SCENARIOS: Scenario[] = [ @@ -48,8 +56,15 @@ const SCENARIOS: Scenario[] = [ { name: 'multi-turn', hasModelTurn: true, recorded: true }, { name: 'error-finish', hasModelTurn: true, recorded: false }, { name: 'cancel', hasModelTurn: true, recorded: false }, + { name: 'subagent-spawn', hasModelTurn: true, recorded: true, childSessions: 1 }, + { name: 'subagent-multi', hasModelTurn: true, recorded: true, childSessions: 2 }, ] +/** The sibling child-fixture paths for a scenario (`session.1.jsonl` …). */ +function childFixturePaths(dir: string, childSessions: number): string[] { + return Array.from({ length: childSessions }, (_, i) => join(dir, `session.${i + 1}.jsonl`)) +} + /** * Derive the {@link NormalizeContext} for a `session.jsonl` fixture from its own * header line (`{ type: 'session', id, cwd }`). A committed fixture carries the @@ -81,42 +96,59 @@ for (const scenario of SCENARIOS) { const input = JSON.parse(await readFile(join(dir, 'input.json'), 'utf8')) as InputScript const overrideFile = join(dir, 'replay.override.json') const workspaceDir = join(dir, 'workspace') + const childSessions = scenario.childSessions ?? 0 const result = await runScenario(input, { mode: RECORDING ? 'record' : 'replay', fixtureFile: join(dir, 'session.jsonl'), ...existsSync(overrideFile) ? { overrideFile } : {}, + // In REPLAY, forward the recorded child fixtures so each subagent session + // replays from its own script. In RECORD they are harvested, not read. + ...!RECORDING && childSessions > 0 ? { childFiles: childFixturePaths(dir, childSessions) } : {}, ...existsSync(workspaceDir) ? { workspaceDir } : {}, }) + // Scrub every volatile id the run produced: the ACP server-issued session + // id plus every harvested log's recorded id (a subagent child id never + // surfaces over ACP, but it appears in the child's own log header). The + // normalizer's UUID catch-all covers any we don't enumerate. const ctx: NormalizeContext = { - sessionIds: result.sessionId !== undefined ? [result.sessionId] : [], + sessionIds: [ + ...result.sessionId !== undefined ? [result.sessionId] : [], + ...result.sessionLogs.map(l => l.id), + ], cwd: result.cwd, } - // RECORD mode (recorded scenarios only): persist the freshly-harvested log - // back to the scenario's session.jsonl fixture. `--update` refreshes the - // Vitest goldens but NOT this fixture, so write it here. + // RECORD mode (recorded model scenarios only): persist the freshly-harvested + // logs back to their fixtures — the primary to session.jsonl, each child to + // session..jsonl in harvest order. `--update` refreshes the Vitest + // goldens but NOT these fixtures, so write them here. if (RECORDING && scenario.recorded && scenario.hasModelTurn) { - expect(result.sessionLog, 'record produced no session log to harvest').toBeDefined() - await writeFile(join(dir, 'session.jsonl'), result.sessionLog as string) + expect(result.sessionLogs.length, 'record produced no session log to harvest').toBeGreaterThan(0) + expect(result.sessionLogs.length, `expected ${childSessions + 1} session logs (parent + children)`) + .toBe(childSessions + 1) + await writeFile(join(dir, 'session.jsonl'), (result.sessionLogs[0] as HarvestedLog).content) + for (let i = 1; i < result.sessionLogs.length; i++) { + await writeFile(join(dir, `session.${i}.jsonl`), (result.sessionLogs[i] as HarvestedLog).content) + } } await expect(normalizeStdout(result.rawStdout, ctx)) .toMatchFileSnapshot(join(dir, 'stdout.golden.jsonl')) if (scenario.hasModelTurn) { - expect(result.sessionLog, 'a model scenario must persist a session log').toBeDefined() - // Compare the replay run's persisted log against the `session.jsonl` - // fixture — there is no separate session golden. Both sides pass through - // normalizeSessionLog so the comparison is on normalized form: the - // fixture is raw-harvested (its own real session id / cwd / timestamps), - // the replay output has fresh ones, and each is scrubbed against ITS OWN - // volatile values. The fixture's are read from its header line (a - // committed file cannot share the live run's ctx), so the stale recorded - // cwd/id are scrubbed too, not left to leak past the run's `ctx`. - const fixture = await readFile(join(dir, 'session.jsonl'), 'utf8') - expect(normalizeSessionLog(result.sessionLog as string, ctx)) - .toEqual(normalizeSessionLog(fixture, fixtureContext(fixture))) + // The harvested logs (primary-first) must match their committed fixtures + // 1:1. Each side passes through normalizeSessionLog, scrubbed against ITS + // OWN volatile values — the live run's via `ctx`, the committed fixture's + // via its own header (a committed file cannot share the live run's ids). + expect(result.sessionLogs.length, 'a model scenario must persist a session log').toBe(childSessions + 1) + const fixtureFiles = ['session.jsonl', ...Array.from({ length: childSessions }, (_, i) => `session.${i + 1}.jsonl`)] + for (let i = 0; i < fixtureFiles.length; i++) { + const harvested = (result.sessionLogs[i] as HarvestedLog).content + const fixture = await readFile(join(dir, fixtureFiles[i] as string), 'utf8') + expect(normalizeSessionLog(harvested, ctx), `${fixtureFiles[i]} mismatch`) + .toEqual(normalizeSessionLog(fixture, fixtureContext(fixture))) + } } }) }) @@ -144,7 +176,7 @@ describe('snapshot fixtures', () => { // doubles as the expected-log artifact the run is diffed against. An authored // (non-`recorded`) model scenario additionally ships a `replay.override.json` // sidecar for the throw/hang cases a derived script cannot express. - for (const { name, hasModelTurn, recorded } of SCENARIOS) { + for (const { name, hasModelTurn, recorded, childSessions } of SCENARIOS) { const dir = join(SNAPSHOTS_DIR, name) expect(existsSync(join(dir, 'input.json')), `${name}/input.json`).toBe(true) expect(existsSync(join(dir, 'stdout.golden.jsonl')), `${name}/stdout.golden.jsonl`).toBe(true) @@ -152,6 +184,11 @@ describe('snapshot fixtures', () => { if (hasModelTurn && !recorded) { expect(existsSync(join(dir, 'replay.override.json')), `${name}/replay.override.json`).toBe(true) } + // A nested-agent scenario ships one child fixture per recorded subagent + // session (`session.1.jsonl` …), the replay source for that child session. + for (const childFixture of childFixturePaths(dir, childSessions ?? 0)) { + expect(existsSync(childFixture), childFixture).toBe(true) + } } }) }) diff --git a/examples/acp-agent/tests/snapshot-harness.ts b/examples/acp-agent/tests/snapshot-harness.ts index 7545768b1d..80847f08dd 100644 --- a/examples/acp-agent/tests/snapshot-harness.ts +++ b/examples/acp-agent/tests/snapshot-harness.ts @@ -17,7 +17,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process' import { cp, mkdtemp, readFile, readdir, rm } from 'node:fs/promises' import { existsSync } from 'node:fs' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { join, delimiter } from 'node:path' import { fileURLToPath } from 'node:url' import { Readable, Writable } from 'node:stream' import { @@ -71,7 +71,19 @@ export interface InputScript { steps: InputStep[] } -/** The result of running a scenario: raw stdout + the harvested session log. */ +/** One harvested session log plus the identifying facts off its header line. */ +export interface HarvestedLog { + /** The recorded session id (header `id`). */ + id: string + /** Session creation time (header `createdAt`) — the child-ordering key. */ + createdAt: number + /** The parent session id, if this log is a subagent child (header `parentSession`). */ + parentSession?: string + /** The full `.jsonl` file content. */ + content: string +} + +/** The result of running a scenario: raw stdout + the harvested session log(s). */ export interface RunResult { /** Raw stdout bytes (decoded utf8), every newline-delimited JSON-RPC frame. */ rawStdout: string @@ -81,8 +93,13 @@ export interface RunResult { sessionId?: string /** The temp cwd the session ran in (the bash workspace). */ cwd: string - /** The persisted session log's content, if one was produced. */ - sessionLog?: string + /** + * Every persisted session log harvested after the run, ordered primary-first: + * the top-level (parent) session — the one with no `parentSession` — then each + * subagent child by ascending `createdAt`. A single-session scenario harvests + * exactly one; a nested-agent scenario harvests the parent plus one per child. + */ + sessionLogs: HarvestedLog[] } interface RunOptions { @@ -92,6 +109,14 @@ interface RunOptions { fixtureFile: string /** Optional sidecar override path (replay). */ overrideFile?: string + /** + * Recorded SUBAGENT child-session fixture paths (replay). A nested-agent + * scenario ships one per child (`session.1.jsonl`, …); the harness forwards + * them to `dsh-llm-replay` via `$DSH_SNAPSHOT_CHILD_FILES` so each child + * session replays from its own recorded script. Empty for single-session + * scenarios. Ignored in record mode (children are harvested, not replayed). + */ + childFiles?: string[] /** * Optional `/workspace/` directory whose contents are copied into * the temp cwd BEFORE the run — the standard way to seed files the agent @@ -114,7 +139,7 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise // never leaks them (the "e2e tests own their resources" rule). let child: ChildProcessWithoutNullStreams | undefined let sessionId: string | undefined - let sessionLog: string | undefined + let sessionLogs: HarvestedLog[] = [] const rawBuffers: Buffer[] = [] const stderrChunks: string[] = [] try { @@ -131,6 +156,9 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise DSH_SNAPSHOT_FILE: opts.fixtureFile, DSH_SNAPSHOT_SESSIONS_ROOT: sessionsRoot, ...opts.overrideFile !== undefined ? { DSH_SNAPSHOT_OVERRIDE: opts.overrideFile } : {}, + ...opts.childFiles !== undefined && opts.childFiles.length > 0 + ? { DSH_SNAPSHOT_CHILD_FILES: opts.childFiles.join(delimiter) } + : {}, } child = spawn( @@ -189,9 +217,9 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise // persistence) and exits. Then await exit so the harvested log is complete. child.stdin.end() await waitForExit(child) - // Harvest the persisted log (if any) while the temp dirs still exist. - const sessionLogPath = await findSessionLog(sessionsRoot) - if (sessionLogPath !== undefined) sessionLog = await readFile(sessionLogPath, 'utf8') + // Harvest EVERY persisted log (parent + any subagent children) while the + // temp dirs still exist, ordered primary-first. + sessionLogs = await harvestSessionLogs(sessionsRoot) } finally { // Failure-safe teardown: kill a still-running child and drop the temp dirs // even if seeding/spawn/a step/harvest threw, so a flaky run never leaks a @@ -209,7 +237,7 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise stderr: stderrChunks.join(''), cwd, ...sessionId !== undefined ? { sessionId } : {}, - ...sessionLog !== undefined ? { sessionLog } : {}, + sessionLogs, } } @@ -300,14 +328,25 @@ function waitForExit(child: ChildProcessWithoutNullStreams): Promise { return new Promise(resolve => child.once('exit', () => { resolve() })) } -/** Find the single produced `.jsonl` session log under a sessions root, if any. */ -async function findSessionLog(root: string): Promise { +/** + * Harvest EVERY persisted `.jsonl` session log under a sessions root, parse each + * header line, and return them ordered primary-first: the top-level session (no + * `parentSession`) leads, then each subagent child by ascending `createdAt`. + * + * The JSONL backend lays sessions out as `//.jsonl` + * (one bucket per cwd), so a parent and its same-cwd in-process child land in + * the SAME bucket — collecting all files across all buckets catches both (the + * old first-match short-circuit silently dropped the child). Returns `[]` if no + * log was produced (a no-session scenario). + */ +async function harvestSessionLogs(root: string): Promise { let cwdDirs: string[] try { cwdDirs = await readdir(root) } catch { - return undefined + return [] } + const logs: HarvestedLog[] = [] for (const dir of cwdDirs) { const sub = join(root, dir) let files: string[] @@ -316,8 +355,26 @@ async function findSessionLog(root: string): Promise { } catch { continue } - const jsonl = files.find(f => f.endsWith('.jsonl')) - if (jsonl !== undefined) return join(sub, jsonl) + for (const f of files) { + if (!f.endsWith('.jsonl')) continue + const content = await readFile(join(sub, f), 'utf8') + const firstLine = content.split('\n').find(line => line.trim().length > 0) ?? '{}' + const header = JSON.parse(firstLine) as { id?: unknown; createdAt?: unknown; parentSession?: unknown } + logs.push({ + id: typeof header.id === 'string' ? header.id : '', + createdAt: typeof header.createdAt === 'number' ? header.createdAt : 0, + ...typeof header.parentSession === 'string' ? { parentSession: header.parentSession } : {}, + content, + }) + } } - return undefined + // Primary (no parentSession) first, then children by ascending createdAt. A + // scenario has exactly one top-level session; ties among children fall back to + // recorded id for a stable order. + logs.sort((a, b) => { + const ap = a.parentSession === undefined ? 0 : 1 + const bp = b.parentSession === undefined ? 0 : 1 + return ap - bp || a.createdAt - b.createdAt || a.id.localeCompare(b.id) + }) + return logs } diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/input.json b/examples/acp-agent/tests/snapshots/subagent-multi/input.json new file mode 100644 index 0000000000..d497fd737a --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-multi/input.json @@ -0,0 +1,7 @@ +{ + "steps": [ + { "op": "initialize" }, + { "op": "newSession" }, + { "op": "prompt", "text": "Use the subagent tool TWICE, once at a time, to delegate two subtasks to child agents. First subtask: 'Reply with exactly the word ALPHA and nothing else.' Second subtask (after the first returns): 'Reply with exactly the word BETA and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool." } + ] +} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl new file mode 100644 index 0000000000..13ff222aae --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl @@ -0,0 +1,35 @@ +{"type":"session","version":0,"id":"dba897b9-c416-4b56-928c-75d12c3e6b32","createdAt":1782087750369,"cwd":"/tmp/acp-snap-cwd-v6PaeC","parentSession":"2a50c62e-1d77-4b0e-bfab-3a9285e3aa32"} +{"type":"turn/start","seq":0,"time":1782087750369,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1782087750369,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"}}} +{"type":"step/start","seq":2,"time":1782087750370,"data":{"turn":1,"step":1}} +{"type":"assistant/chunk","seq":3,"time":1782087750967,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":4,"time":1782087750967,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":5,"time":1782087751058,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":6,"time":1782087751092,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} +{"type":"assistant/chunk","seq":7,"time":1782087751092,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":8,"time":1782087751092,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":9,"time":1782087751092,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":10,"time":1782087751092,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":11,"time":1782087751117,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":12,"time":1782087751117,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":13,"time":1782087751117,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":14,"time":1782087751117,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":15,"time":1782087751117,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"AL"}}} +{"type":"assistant/chunk","seq":16,"time":1782087751117,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"P"}}} +{"type":"assistant/chunk","seq":17,"time":1782087751166,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"HA"}}} +{"type":"assistant/chunk","seq":18,"time":1782087751166,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":19,"time":1782087751166,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":20,"time":1782087751166,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" nothing"}}} +{"type":"assistant/chunk","seq":21,"time":1782087751166,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" else"}}} +{"type":"assistant/chunk","seq":22,"time":1782087751166,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":23,"time":1782087751197,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":24,"time":1782087751197,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"AL"}}} +{"type":"assistant/chunk","seq":25,"time":1782087751197,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"P"}}} +{"type":"assistant/chunk","seq":26,"time":1782087751197,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"HA"}}} +{"type":"assistant/chunk","seq":27,"time":1782087751198,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked me to reply with exactly the word \"ALPHA\" and nothing else."}}}} +{"type":"assistant/chunk","seq":28,"time":1782087751198,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"ALPHA"}}}} +{"type":"assistant/chunk","seq":29,"time":1782087751198,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":68,"outputTokens":23,"cacheReadTokens":896,"reasoningTokens":19}}}} +{"type":"assistant/chunk","seq":30,"time":1782087751198,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":31,"time":1782087751198,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user asked me to reply with exactly the word \"ALPHA\" and nothing else."},{"type":"text","text":"ALPHA"}],"usage":{"inputTokens":68,"outputTokens":23,"cacheReadTokens":896,"reasoningTokens":19}}} +{"type":"step/end","seq":32,"time":1782087751198,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":33,"time":1782087751198,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl new file mode 100644 index 0000000000..a239b67cc3 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl @@ -0,0 +1,33 @@ +{"type":"session","version":0,"id":"52bb4e53-1cf4-4680-b954-4ad941a9e986","createdAt":1782087752261,"cwd":"/tmp/acp-snap-cwd-v6PaeC","parentSession":"2a50c62e-1d77-4b0e-bfab-3a9285e3aa32"} +{"type":"turn/start","seq":0,"time":1782087752262,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1782087752262,"data":{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"}}} +{"type":"step/start","seq":2,"time":1782087752262,"data":{"turn":1,"step":1}} +{"type":"assistant/chunk","seq":3,"time":1782087752714,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":4,"time":1782087752714,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":5,"time":1782087752857,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":6,"time":1782087752875,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} +{"type":"assistant/chunk","seq":7,"time":1782087752909,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":8,"time":1782087752909,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":9,"time":1782087752910,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":10,"time":1782087752910,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":11,"time":1782087752910,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":12,"time":1782087752941,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":13,"time":1782087752941,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":14,"time":1782087752942,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":15,"time":1782087752942,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"B"}}} +{"type":"assistant/chunk","seq":16,"time":1782087752942,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ETA"}}} +{"type":"assistant/chunk","seq":17,"time":1782087752942,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":18,"time":1782087752974,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":19,"time":1782087752975,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" nothing"}}} +{"type":"assistant/chunk","seq":20,"time":1782087752975,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" else"}}} +{"type":"assistant/chunk","seq":21,"time":1782087752975,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":22,"time":1782087753004,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":23,"time":1782087753004,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"B"}}} +{"type":"assistant/chunk","seq":24,"time":1782087753004,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ETA"}}} +{"type":"assistant/chunk","seq":25,"time":1782087753004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked me to reply with exactly the word \"BETA\" and nothing else."}}}} +{"type":"assistant/chunk","seq":26,"time":1782087753004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"BETA"}}}} +{"type":"assistant/chunk","seq":27,"time":1782087753005,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":67,"outputTokens":21,"cacheReadTokens":896,"reasoningTokens":18}}}} +{"type":"assistant/chunk","seq":28,"time":1782087753005,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":29,"time":1782087753005,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user asked me to reply with exactly the word \"BETA\" and nothing else."},{"type":"text","text":"BETA"}],"usage":{"inputTokens":67,"outputTokens":21,"cacheReadTokens":896,"reasoningTokens":18}}} +{"type":"step/end","seq":30,"time":1782087753005,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":31,"time":1782087753005,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl new file mode 100644 index 0000000000..e42c624a97 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl @@ -0,0 +1,213 @@ +{"type":"session","version":0,"id":"2a50c62e-1d77-4b0e-bfab-3a9285e3aa32","createdAt":1782087748790,"cwd":"/tmp/acp-snap-cwd-v6PaeC"} +{"type":"turn/start","seq":0,"time":1782087748793,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1782087748794,"data":{"content":[{"type":"text","text":"Use the subagent tool TWICE, once at a time, to delegate two subtasks to child agents. First subtask: 'Reply with exactly the word ALPHA and nothing else.' Second subtask (after the first returns): 'Reply with exactly the word BETA and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"}}} +{"type":"step/start","seq":2,"time":1782087748794,"data":{"turn":1,"step":1}} +{"type":"assistant/chunk","seq":3,"time":1782087749465,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":4,"time":1782087749465,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":5,"time":1782087749560,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":6,"time":1782087749588,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":7,"time":1782087749590,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":8,"time":1782087749590,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":9,"time":1782087749590,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" use"}}} +{"type":"assistant/chunk","seq":10,"time":1782087749590,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":11,"time":1782087749617,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sub"}}} +{"type":"assistant/chunk","seq":12,"time":1782087749618,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"agent"}}} +{"type":"assistant/chunk","seq":13,"time":1782087749618,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tool"}}} +{"type":"assistant/chunk","seq":14,"time":1782087749618,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" twice"}}} +{"type":"assistant/chunk","seq":15,"time":1782087749618,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":16,"time":1782087749643,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" once"}}} +{"type":"assistant/chunk","seq":17,"time":1782087749644,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" at"}}} +{"type":"assistant/chunk","seq":18,"time":1782087749644,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":19,"time":1782087749672,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" time"}}} +{"type":"assistant/chunk","seq":20,"time":1782087749673,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":21,"time":1782087749673,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" First"}}} +{"type":"assistant/chunk","seq":22,"time":1782087749702,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" subt"}}} +{"type":"assistant/chunk","seq":23,"time":1782087749703,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ask"}}} +{"type":"assistant/chunk","seq":24,"time":1782087749703,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} +{"type":"assistant/chunk","seq":25,"time":1782087749731,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":26,"time":1782087749731,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":27,"time":1782087749732,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":28,"time":1782087749758,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"AL"}}} +{"type":"assistant/chunk","seq":29,"time":1782087749758,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"P"}}} +{"type":"assistant/chunk","seq":30,"time":1782087749758,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"HA"}}} +{"type":"assistant/chunk","seq":31,"time":1782087749758,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":32,"time":1782087749759,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" After"}}} +{"type":"assistant/chunk","seq":33,"time":1782087749786,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":34,"time":1782087749787,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" returns"}}} +{"type":"assistant/chunk","seq":35,"time":1782087749814,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":36,"time":1782087749814,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" second"}}} +{"type":"assistant/chunk","seq":37,"time":1782087749843,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" subt"}}} +{"type":"assistant/chunk","seq":38,"time":1782087749844,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ask"}}} +{"type":"assistant/chunk","seq":39,"time":1782087749844,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} +{"type":"assistant/chunk","seq":40,"time":1782087749844,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":41,"time":1782087749844,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":42,"time":1782087749844,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":43,"time":1782087749872,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"B"}}} +{"type":"assistant/chunk","seq":44,"time":1782087749873,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ETA"}}} +{"type":"assistant/chunk","seq":45,"time":1782087749873,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":46,"time":1782087749873,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Then"}}} +{"type":"assistant/chunk","seq":47,"time":1782087749873,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":48,"time":1782087749901,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":49,"time":1782087749901,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":50,"time":1782087749901,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":51,"time":1782087749901,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"PAR"}}} +{"type":"assistant/chunk","seq":52,"time":1782087749902,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ENT"}}} +{"type":"assistant/chunk","seq":53,"time":1782087749902,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_D"}}} +{"type":"assistant/chunk","seq":54,"time":1782087749930,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":55,"time":1782087749930,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":56,"time":1782087749930,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":57,"time":1782087749931,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":58,"time":1782087749931,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" start"}}} +{"type":"assistant/chunk","seq":59,"time":1782087749959,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":60,"time":1782087749959,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":61,"time":1782087749959,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" first"}}} +{"type":"assistant/chunk","seq":62,"time":1782087749959,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sub"}}} +{"type":"assistant/chunk","seq":63,"time":1782087749959,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"agent"}}} +{"type":"assistant/chunk","seq":64,"time":1782087749959,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" call"}}} +{"type":"assistant/chunk","seq":65,"time":1782087749986,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":66,"time":1782087750072,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":67,"time":1782087750072,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":68,"time":1782087750075,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":69,"time":1782087750075,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":70,"time":1782087750075,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":71,"time":1782087750102,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":72,"time":1782087750102,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":73,"time":1782087750103,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":74,"time":1782087750103,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"First"}}} +{"type":"assistant/chunk","seq":75,"time":1782087750129,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":" subt"}}} +{"type":"assistant/chunk","seq":76,"time":1782087750129,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"ask"}}} +{"type":"assistant/chunk","seq":77,"time":1782087750130,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":":"}}} +{"type":"assistant/chunk","seq":78,"time":1782087750159,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":" AL"}}} +{"type":"assistant/chunk","seq":79,"time":1782087750159,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"P"}}} +{"type":"assistant/chunk","seq":80,"time":1782087750159,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"HA"}}} +{"type":"assistant/chunk","seq":81,"time":1782087750159,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":82,"time":1782087750189,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":83,"time":1782087750190,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":84,"time":1782087750216,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"prom"}}} +{"type":"assistant/chunk","seq":85,"time":1782087750216,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"pt"}}} +{"type":"assistant/chunk","seq":86,"time":1782087750216,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":87,"time":1782087750216,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":88,"time":1782087750247,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":89,"time":1782087750248,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"Reply"}}} +{"type":"assistant/chunk","seq":90,"time":1782087750248,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":" with"}}} +{"type":"assistant/chunk","seq":91,"time":1782087750248,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":" exactly"}}} +{"type":"assistant/chunk","seq":92,"time":1782087750248,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":93,"time":1782087750248,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":" word"}}} +{"type":"assistant/chunk","seq":94,"time":1782087750275,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":" AL"}}} +{"type":"assistant/chunk","seq":95,"time":1782087750275,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"P"}}} +{"type":"assistant/chunk","seq":96,"time":1782087750275,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"HA"}}} +{"type":"assistant/chunk","seq":97,"time":1782087750276,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":" and"}}} +{"type":"assistant/chunk","seq":98,"time":1782087750276,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":" nothing"}}} +{"type":"assistant/chunk","seq":99,"time":1782087750276,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":" else"}}} +{"type":"assistant/chunk","seq":100,"time":1782087750304,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"."}}} +{"type":"assistant/chunk","seq":101,"time":1782087750305,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":102,"time":1782087750305,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":103,"time":1782087750365,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the subagent tool twice, once at a time. First subtask: reply with \"ALPHA\". After that returns, second subtask: reply with \"BETA\". Then I reply with \"PARENT_DONE\". Let me start with the first subagent call."}}}} +{"type":"assistant/chunk","seq":104,"time":1782087750366,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","arguments":"{\"description\": \"First subtask: ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\"}"}}}} +{"type":"assistant/chunk","seq":105,"time":1782087750366,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1189,"outputTokens":139,"cacheReadTokens":0,"reasoningTokens":62}}}} +{"type":"assistant/chunk","seq":106,"time":1782087750366,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":107,"time":1782087750368,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to use the subagent tool twice, once at a time. First subtask: reply with \"ALPHA\". After that returns, second subtask: reply with \"BETA\". Then I reply with \"PARENT_DONE\". Let me start with the first subagent call."},{"type":"tool-call","id":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","arguments":"{\"description\": \"First subtask: ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\"}"}],"usage":{"inputTokens":1189,"outputTokens":139,"cacheReadTokens":0,"reasoningTokens":62}}} +{"type":"tool/call","seq":108,"time":1782087750368,"data":{"turn":1,"step":1,"callId":"call_00_T91HrbiohZjyqZ7biX4s4408","name":"subagent","arguments":"{\"description\": \"First subtask: ALPHA\", \"prompt\": \"Reply with exactly the word ALPHA and nothing else.\"}"}} +{"type":"tool/result","seq":109,"time":1782087751204,"data":{"turn":1,"step":1,"callId":"call_00_T91HrbiohZjyqZ7biX4s4408","content":[{"type":"text","text":"ALPHA"}],"isError":false}} +{"type":"step/end","seq":110,"time":1782087751204,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":111,"time":1782087751204,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":112,"time":1782087751674,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":113,"time":1782087751674,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"First"}}} +{"type":"assistant/chunk","seq":114,"time":1782087751762,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" sub"}}} +{"type":"assistant/chunk","seq":115,"time":1782087751793,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"agent"}}} +{"type":"assistant/chunk","seq":116,"time":1782087751793,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" returned"}}} +{"type":"assistant/chunk","seq":117,"time":1782087751793,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":118,"time":1782087751793,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"AL"}}} +{"type":"assistant/chunk","seq":119,"time":1782087751819,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"P"}}} +{"type":"assistant/chunk","seq":120,"time":1782087751819,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"HA"}}} +{"type":"assistant/chunk","seq":121,"time":1782087751819,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":122,"time":1782087751819,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Now"}}} +{"type":"assistant/chunk","seq":123,"time":1782087751819,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":124,"time":1782087751819,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} +{"type":"assistant/chunk","seq":125,"time":1782087751848,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":126,"time":1782087751849,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" call"}}} +{"type":"assistant/chunk","seq":127,"time":1782087751849,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":128,"time":1782087751849,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" second"}}} +{"type":"assistant/chunk","seq":129,"time":1782087751849,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" sub"}}} +{"type":"assistant/chunk","seq":130,"time":1782087751849,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"agent"}}} +{"type":"assistant/chunk","seq":131,"time":1782087751877,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":132,"time":1782087751966,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":133,"time":1782087751966,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":134,"time":1782087751995,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":135,"time":1782087751995,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":136,"time":1782087751995,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":137,"time":1782087751995,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":138,"time":1782087751995,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":139,"time":1782087752025,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":140,"time":1782087752025,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"Second"}}} +{"type":"assistant/chunk","seq":141,"time":1782087752025,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":" subt"}}} +{"type":"assistant/chunk","seq":142,"time":1782087752025,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"ask"}}} +{"type":"assistant/chunk","seq":143,"time":1782087752025,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":":"}}} +{"type":"assistant/chunk","seq":144,"time":1782087752025,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":" B"}}} +{"type":"assistant/chunk","seq":145,"time":1782087752053,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"ETA"}}} +{"type":"assistant/chunk","seq":146,"time":1782087752053,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":147,"time":1782087752082,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":148,"time":1782087752083,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":149,"time":1782087752083,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"prom"}}} +{"type":"assistant/chunk","seq":150,"time":1782087752083,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"pt"}}} +{"type":"assistant/chunk","seq":151,"time":1782087752112,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":152,"time":1782087752113,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":153,"time":1782087752113,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":154,"time":1782087752113,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"Reply"}}} +{"type":"assistant/chunk","seq":155,"time":1782087752140,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":" with"}}} +{"type":"assistant/chunk","seq":156,"time":1782087752141,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":" exactly"}}} +{"type":"assistant/chunk","seq":157,"time":1782087752141,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":158,"time":1782087752141,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":" word"}}} +{"type":"assistant/chunk","seq":159,"time":1782087752141,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":" B"}}} +{"type":"assistant/chunk","seq":160,"time":1782087752141,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"ETA"}}} +{"type":"assistant/chunk","seq":161,"time":1782087752170,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":" and"}}} +{"type":"assistant/chunk","seq":162,"time":1782087752170,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":" nothing"}}} +{"type":"assistant/chunk","seq":163,"time":1782087752170,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":" else"}}} +{"type":"assistant/chunk","seq":164,"time":1782087752170,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"."}}} +{"type":"assistant/chunk","seq":165,"time":1782087752170,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":166,"time":1782087752199,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":167,"time":1782087752260,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"First subagent returned \"ALPHA\". Now I need to call the second subagent."}}}} +{"type":"assistant/chunk","seq":168,"time":1782087752260,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","arguments":"{\"description\": \"Second subtask: BETA\", \"prompt\": \"Reply with exactly the word BETA and nothing else.\"}"}}}} +{"type":"assistant/chunk","seq":169,"time":1782087752260,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":63,"outputTokens":94,"cacheReadTokens":1280,"reasoningTokens":19}}}} +{"type":"assistant/chunk","seq":170,"time":1782087752260,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":171,"time":1782087752261,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"First subagent returned \"ALPHA\". Now I need to call the second subagent."},{"type":"tool-call","id":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","arguments":"{\"description\": \"Second subtask: BETA\", \"prompt\": \"Reply with exactly the word BETA and nothing else.\"}"}],"usage":{"inputTokens":63,"outputTokens":94,"cacheReadTokens":1280,"reasoningTokens":19}}} +{"type":"tool/call","seq":172,"time":1782087752261,"data":{"turn":1,"step":2,"callId":"call_00_G13lPXMKGT1h6ms60n411588","name":"subagent","arguments":"{\"description\": \"Second subtask: BETA\", \"prompt\": \"Reply with exactly the word BETA and nothing else.\"}"}} +{"type":"tool/result","seq":173,"time":1782087753008,"data":{"turn":1,"step":2,"callId":"call_00_G13lPXMKGT1h6ms60n411588","content":[{"type":"text","text":"BETA"}],"isError":false}} +{"type":"step/end","seq":174,"time":1782087753008,"data":{"turn":1,"step":2}} +{"type":"step/start","seq":175,"time":1782087753008,"data":{"turn":1,"step":3}} +{"type":"assistant/chunk","seq":176,"time":1782087753643,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":177,"time":1782087753643,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"Both"}}} +{"type":"assistant/chunk","seq":178,"time":1782087753776,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" sub"}}} +{"type":"assistant/chunk","seq":179,"time":1782087753806,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"agents"}}} +{"type":"assistant/chunk","seq":180,"time":1782087753807,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" have"}}} +{"type":"assistant/chunk","seq":181,"time":1782087753807,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" returned"}}} +{"type":"assistant/chunk","seq":182,"time":1782087753807,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":183,"time":1782087753850,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" Now"}}} +{"type":"assistant/chunk","seq":184,"time":1782087753851,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":185,"time":1782087753851,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} +{"type":"assistant/chunk","seq":186,"time":1782087753870,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":187,"time":1782087753870,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":188,"time":1782087753871,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":189,"time":1782087753871,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":190,"time":1782087753871,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":191,"time":1782087753901,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"PAR"}}} +{"type":"assistant/chunk","seq":192,"time":1782087753901,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"ENT"}}} +{"type":"assistant/chunk","seq":193,"time":1782087753902,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"_D"}}} +{"type":"assistant/chunk","seq":194,"time":1782087753902,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":195,"time":1782087753902,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":196,"time":1782087753935,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":197,"time":1782087753935,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" nothing"}}} +{"type":"assistant/chunk","seq":198,"time":1782087753935,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" else"}}} +{"type":"assistant/chunk","seq":199,"time":1782087753935,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":200,"time":1782087753936,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":201,"time":1782087753936,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"PAR"}}} +{"type":"assistant/chunk","seq":202,"time":1782087753966,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"ENT"}}} +{"type":"assistant/chunk","seq":203,"time":1782087753966,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"_D"}}} +{"type":"assistant/chunk","seq":204,"time":1782087753966,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":205,"time":1782087753967,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Both subagents have returned. Now I need to reply with exactly \"PARENT_DONE\" and nothing else."}}}} +{"type":"assistant/chunk","seq":206,"time":1782087753967,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}}} +{"type":"assistant/chunk","seq":207,"time":1782087753967,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":43,"outputTokens":28,"cacheReadTokens":1408,"reasoningTokens":23}}}} +{"type":"assistant/chunk","seq":208,"time":1782087753967,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":209,"time":1782087753967,"data":{"turn":1,"step":3,"content":[{"type":"reasoning","text":"Both subagents have returned. Now I need to reply with exactly \"PARENT_DONE\" and nothing else."},{"type":"text","text":"PARENT_DONE"}],"usage":{"inputTokens":43,"outputTokens":28,"cacheReadTokens":1408,"reasoningTokens":23}}} +{"type":"step/end","seq":210,"time":1782087753967,"data":{"turn":1,"step":3}} +{"type":"turn/end","seq":211,"time":1782087753967,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/stdout.golden.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/stdout.golden.jsonl new file mode 100644 index 0000000000..1cfce85dc3 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-multi/stdout.golden.jsonl @@ -0,0 +1,115 @@ +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" wants"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" use"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" sub"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"agent"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" tool"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" twice"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":","}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" once"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" at"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" a"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" time"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" First"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" subt"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ask"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":":"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"AL"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"P"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"HA"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" After"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" that"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" returns"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":","}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" second"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" subt"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ask"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":":"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"B"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ETA"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Then"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"PAR"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ENT"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_D"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ONE"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Let"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" start"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" first"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" sub"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"agent"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" call"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_T91HrbiohZjyqZ7biX4s4408","title":"subagent","kind":"other","status":"in_progress","rawInput":{"description":"First subtask: ALPHA","prompt":"Reply with exactly the word ALPHA and nothing else."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_T91HrbiohZjyqZ7biX4s4408","status":"completed","content":[{"type":"content","content":{"type":"text","text":"ALPHA"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"First"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" sub"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"agent"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" returned"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"AL"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"P"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"HA"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Now"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" need"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" call"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" second"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" sub"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"agent"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_G13lPXMKGT1h6ms60n411588","title":"subagent","kind":"other","status":"in_progress","rawInput":{"description":"Second subtask: BETA","prompt":"Reply with exactly the word BETA and nothing else."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_G13lPXMKGT1h6ms60n411588","status":"completed","content":[{"type":"content","content":{"type":"text","text":"BETA"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"Both"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" sub"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"agents"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" have"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" returned"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Now"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" need"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" exactly"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"PAR"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ENT"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_D"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ONE"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" nothing"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" else"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"PAR"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ENT"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"_D"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ONE"}}}} +{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/input.json b/examples/acp-agent/tests/snapshots/subagent-spawn/input.json new file mode 100644 index 0000000000..3cd6f5350d --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-spawn/input.json @@ -0,0 +1,7 @@ +{ + "steps": [ + { "op": "initialize" }, + { "op": "newSession" }, + { "op": "prompt", "text": "Use the subagent tool exactly once to delegate this subtask to a child agent: 'Reply with exactly the word CHILD_OK and nothing else.' After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool." } + ] +} diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl new file mode 100644 index 0000000000..d32cf4b836 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl @@ -0,0 +1,35 @@ +{"type":"session","version":0,"id":"4d76c4bd-1fca-418f-b2fe-b938d7398666","createdAt":1782087699201,"cwd":"/tmp/acp-snap-cwd-s06Syv","parentSession":"9b045576-92f1-48ca-b854-9ba160449992"} +{"type":"turn/start","seq":0,"time":1782087699202,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1782087699202,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"}}} +{"type":"step/start","seq":2,"time":1782087699202,"data":{"turn":1,"step":1}} +{"type":"assistant/chunk","seq":3,"time":1782087699839,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":4,"time":1782087699839,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":5,"time":1782087699947,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":6,"time":1782087699977,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":7,"time":1782087699977,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":8,"time":1782087699977,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":9,"time":1782087699977,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":10,"time":1782087699977,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":11,"time":1782087699977,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":12,"time":1782087700016,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":13,"time":1782087700016,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":14,"time":1782087700017,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":15,"time":1782087700017,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"CH"}}} +{"type":"assistant/chunk","seq":16,"time":1782087700034,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ILD"}}} +{"type":"assistant/chunk","seq":17,"time":1782087700035,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_OK"}}} +{"type":"assistant/chunk","seq":18,"time":1782087700035,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":19,"time":1782087700035,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":20,"time":1782087700035,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" nothing"}}} +{"type":"assistant/chunk","seq":21,"time":1782087700035,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" else"}}} +{"type":"assistant/chunk","seq":22,"time":1782087700064,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":23,"time":1782087700064,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":24,"time":1782087700064,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"CH"}}} +{"type":"assistant/chunk","seq":25,"time":1782087700064,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ILD"}}} +{"type":"assistant/chunk","seq":26,"time":1782087700107,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"_OK"}}} +{"type":"assistant/chunk","seq":27,"time":1782087700108,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly the word \"CHILD_OK\" and nothing else."}}}} +{"type":"assistant/chunk","seq":28,"time":1782087700108,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"CHILD_OK"}}}} +{"type":"assistant/chunk","seq":29,"time":1782087700108,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":964,"outputTokens":23,"cacheReadTokens":0,"reasoningTokens":19}}}} +{"type":"assistant/chunk","seq":30,"time":1782087700108,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":31,"time":1782087700108,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with exactly the word \"CHILD_OK\" and nothing else."},{"type":"text","text":"CHILD_OK"}],"usage":{"inputTokens":964,"outputTokens":23,"cacheReadTokens":0,"reasoningTokens":19}}} +{"type":"step/end","seq":32,"time":1782087700108,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":33,"time":1782087700108,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl new file mode 100644 index 0000000000..af1b1856b6 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl @@ -0,0 +1,142 @@ +{"type":"session","version":0,"id":"9b045576-92f1-48ca-b854-9ba160449992","createdAt":1782087697853,"cwd":"/tmp/acp-snap-cwd-s06Syv"} +{"type":"turn/start","seq":0,"time":1782087697856,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1782087697856,"data":{"content":[{"type":"text","text":"Use the subagent tool exactly once to delegate this subtask to a child agent: 'Reply with exactly the word CHILD_OK and nothing else.' After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"}}} +{"type":"step/start","seq":2,"time":1782087697857,"data":{"turn":1,"step":1}} +{"type":"assistant/chunk","seq":3,"time":1782087698282,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":4,"time":1782087698283,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":5,"time":1782087698376,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":6,"time":1782087698406,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":7,"time":1782087698406,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":8,"time":1782087698406,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":9,"time":1782087698406,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" use"}}} +{"type":"assistant/chunk","seq":10,"time":1782087698407,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":11,"time":1782087698435,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sub"}}} +{"type":"assistant/chunk","seq":12,"time":1782087698435,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"agent"}}} +{"type":"assistant/chunk","seq":13,"time":1782087698436,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tool"}}} +{"type":"assistant/chunk","seq":14,"time":1782087698436,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" once"}}} +{"type":"assistant/chunk","seq":15,"time":1782087698464,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":16,"time":1782087698464,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":17,"time":1782087698464,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" specific"}}} +{"type":"assistant/chunk","seq":18,"time":1782087698492,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" prompt"}}} +{"type":"assistant/chunk","seq":19,"time":1782087698492,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":20,"time":1782087698521,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Reply"}}} +{"type":"assistant/chunk","seq":21,"time":1782087698521,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":22,"time":1782087698521,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":23,"time":1782087698521,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":24,"time":1782087698522,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":25,"time":1782087698522,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" CH"}}} +{"type":"assistant/chunk","seq":26,"time":1782087698549,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ILD"}}} +{"type":"assistant/chunk","seq":27,"time":1782087698550,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_OK"}}} +{"type":"assistant/chunk","seq":28,"time":1782087698550,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":29,"time":1782087698550,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" nothing"}}} +{"type":"assistant/chunk","seq":30,"time":1782087698550,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" else"}}} +{"type":"assistant/chunk","seq":31,"time":1782087698550,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".\""}}} +{"type":"assistant/chunk","seq":32,"time":1782087698578,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Then"}}} +{"type":"assistant/chunk","seq":33,"time":1782087698579,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" after"}}} +{"type":"assistant/chunk","seq":34,"time":1782087698607,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":35,"time":1782087698608,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sub"}}} +{"type":"assistant/chunk","seq":36,"time":1782087698608,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"agent"}}} +{"type":"assistant/chunk","seq":37,"time":1782087698608,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" returns"}}} +{"type":"assistant/chunk","seq":38,"time":1782087698608,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":39,"time":1782087698608,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":40,"time":1782087698636,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" should"}}} +{"type":"assistant/chunk","seq":41,"time":1782087698665,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":42,"time":1782087698666,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":43,"time":1782087698666,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":44,"time":1782087698694,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"PAR"}}} +{"type":"assistant/chunk","seq":45,"time":1782087698695,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ENT"}}} +{"type":"assistant/chunk","seq":46,"time":1782087698695,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_D"}}} +{"type":"assistant/chunk","seq":47,"time":1782087698695,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":48,"time":1782087698695,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":49,"time":1782087698695,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":50,"time":1782087698726,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} +{"type":"assistant/chunk","seq":51,"time":1782087698727,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":52,"time":1782087698727,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" No"}}} +{"type":"assistant/chunk","seq":53,"time":1782087698756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" bash"}}} +{"type":"assistant/chunk","seq":54,"time":1782087698756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tool"}}} +{"type":"assistant/chunk","seq":55,"time":1782087698785,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" usage"}}} +{"type":"assistant/chunk","seq":56,"time":1782087698818,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":57,"time":1782087698875,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":58,"time":1782087698875,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":59,"time":1782087698921,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":60,"time":1782087698921,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":61,"time":1782087698922,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":62,"time":1782087698933,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":63,"time":1782087698933,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":64,"time":1782087698934,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":65,"time":1782087698934,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"Reply"}}} +{"type":"assistant/chunk","seq":66,"time":1782087698959,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":" with"}}} +{"type":"assistant/chunk","seq":67,"time":1782087698960,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":" CH"}}} +{"type":"assistant/chunk","seq":68,"time":1782087698988,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"ILD"}}} +{"type":"assistant/chunk","seq":69,"time":1782087698988,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"_OK"}}} +{"type":"assistant/chunk","seq":70,"time":1782087698988,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":71,"time":1782087699021,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":72,"time":1782087699021,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":73,"time":1782087699021,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"prom"}}} +{"type":"assistant/chunk","seq":74,"time":1782087699050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"pt"}}} +{"type":"assistant/chunk","seq":75,"time":1782087699050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":76,"time":1782087699050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":77,"time":1782087699050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":78,"time":1782087699079,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"Reply"}}} +{"type":"assistant/chunk","seq":79,"time":1782087699079,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":" with"}}} +{"type":"assistant/chunk","seq":80,"time":1782087699079,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":" exactly"}}} +{"type":"assistant/chunk","seq":81,"time":1782087699079,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":82,"time":1782087699079,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":" word"}}} +{"type":"assistant/chunk","seq":83,"time":1782087699079,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":" CH"}}} +{"type":"assistant/chunk","seq":84,"time":1782087699107,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"ILD"}}} +{"type":"assistant/chunk","seq":85,"time":1782087699108,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"_OK"}}} +{"type":"assistant/chunk","seq":86,"time":1782087699108,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":" and"}}} +{"type":"assistant/chunk","seq":87,"time":1782087699108,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":" nothing"}}} +{"type":"assistant/chunk","seq":88,"time":1782087699108,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":" else"}}} +{"type":"assistant/chunk","seq":89,"time":1782087699137,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"."}}} +{"type":"assistant/chunk","seq":90,"time":1782087699137,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":91,"time":1782087699137,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":92,"time":1782087699198,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to use the subagent tool once with the specific prompt \"Reply with exactly the word CHILD_OK and nothing else.\" Then after the subagent returns, I should reply with \"PARENT_DONE\" and stop. No bash tool usage."}}}} +{"type":"assistant/chunk","seq":93,"time":1782087699198,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\"}"}}}} +{"type":"assistant/chunk","seq":94,"time":1782087699198,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1159,"outputTokens":128,"cacheReadTokens":0,"reasoningTokens":53}}}} +{"type":"assistant/chunk","seq":95,"time":1782087699198,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":96,"time":1782087699200,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to use the subagent tool once with the specific prompt \"Reply with exactly the word CHILD_OK and nothing else.\" Then after the subagent returns, I should reply with \"PARENT_DONE\" and stop. No bash tool usage."},{"type":"tool-call","id":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\"}"}],"usage":{"inputTokens":1159,"outputTokens":128,"cacheReadTokens":0,"reasoningTokens":53}}} +{"type":"tool/call","seq":97,"time":1782087699200,"data":{"turn":1,"step":1,"callId":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","name":"subagent","arguments":"{\"description\": \"Reply with CHILD_OK\", \"prompt\": \"Reply with exactly the word CHILD_OK and nothing else.\"}"}} +{"type":"tool/result","seq":98,"time":1782087700114,"data":{"turn":1,"step":1,"callId":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","content":[{"type":"text","text":"CHILD_OK"}],"isError":false}} +{"type":"step/end","seq":99,"time":1782087700114,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":100,"time":1782087700114,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":101,"time":1782087700497,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":102,"time":1782087700497,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":103,"time":1782087700630,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" sub"}}} +{"type":"assistant/chunk","seq":104,"time":1782087700660,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"agent"}}} +{"type":"assistant/chunk","seq":105,"time":1782087700660,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" returned"}}} +{"type":"assistant/chunk","seq":106,"time":1782087700661,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":107,"time":1782087700661,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"CH"}}} +{"type":"assistant/chunk","seq":108,"time":1782087700661,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ILD"}}} +{"type":"assistant/chunk","seq":109,"time":1782087700661,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_OK"}}} +{"type":"assistant/chunk","seq":110,"time":1782087700687,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":111,"time":1782087700716,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" as"}}} +{"type":"assistant/chunk","seq":112,"time":1782087700717,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" expected"}}} +{"type":"assistant/chunk","seq":113,"time":1782087700746,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":114,"time":1782087700746,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Now"}}} +{"type":"assistant/chunk","seq":115,"time":1782087700746,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":116,"time":1782087700746,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} +{"type":"assistant/chunk","seq":117,"time":1782087700746,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":118,"time":1782087700746,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":119,"time":1782087700774,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":120,"time":1782087700775,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":121,"time":1782087700775,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"PAR"}}} +{"type":"assistant/chunk","seq":122,"time":1782087700775,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ENT"}}} +{"type":"assistant/chunk","seq":123,"time":1782087700775,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_D"}}} +{"type":"assistant/chunk","seq":124,"time":1782087700775,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":125,"time":1782087700804,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":126,"time":1782087700804,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":127,"time":1782087700804,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} +{"type":"assistant/chunk","seq":128,"time":1782087700804,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":129,"time":1782087700804,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":130,"time":1782087700805,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"PAR"}}} +{"type":"assistant/chunk","seq":131,"time":1782087700833,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ENT"}}} +{"type":"assistant/chunk","seq":132,"time":1782087700833,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_D"}}} +{"type":"assistant/chunk","seq":133,"time":1782087700834,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":134,"time":1782087700834,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with \"PARENT_DONE\" and stop."}}}} +{"type":"assistant/chunk","seq":135,"time":1782087700834,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"PARENT_DONE"}}}} +{"type":"assistant/chunk","seq":136,"time":1782087700834,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":22,"outputTokens":32,"cacheReadTokens":1280,"reasoningTokens":27}}}} +{"type":"assistant/chunk","seq":137,"time":1782087700834,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":138,"time":1782087700834,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The subagent returned \"CHILD_OK\" as expected. Now I need to reply with \"PARENT_DONE\" and stop."},{"type":"text","text":"PARENT_DONE"}],"usage":{"inputTokens":22,"outputTokens":32,"cacheReadTokens":1280,"reasoningTokens":27}}} +{"type":"step/end","seq":139,"time":1782087700835,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":140,"time":1782087700835,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/stdout.golden.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn/stdout.golden.jsonl new file mode 100644 index 0000000000..3c78183324 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-spawn/stdout.golden.jsonl @@ -0,0 +1,89 @@ +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}"}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" wants"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" use"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" sub"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"agent"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" tool"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" once"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" specific"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" prompt"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"Reply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" exactly"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" word"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" CH"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ILD"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_OK"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" nothing"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" else"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":".\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Then"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" after"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" sub"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"agent"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" returns"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":","}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" should"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"PAR"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ENT"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_D"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ONE"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" stop"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" No"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" bash"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" tool"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" usage"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","title":"subagent","kind":"other","status":"in_progress","rawInput":{"description":"Reply with CHILD_OK","prompt":"Reply with exactly the word CHILD_OK and nothing else."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_3R6FQL0Zk3D122Jpbu0Q9888","status":"completed","content":[{"type":"content","content":{"type":"text","text":"CHILD_OK"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" sub"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"agent"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" returned"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"CH"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ILD"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_OK"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" as"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" expected"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Now"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" need"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"PAR"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ENT"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_D"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ONE"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" stop"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"PAR"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ENT"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"_D"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ONE"}}}} +{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}} diff --git a/packages/core/agent-loop/src/loop.ts b/packages/core/agent-loop/src/loop.ts index 8d19c464fd..54a9387518 100644 --- a/packages/core/agent-loop/src/loop.ts +++ b/packages/core/agent-loop/src/loop.ts @@ -570,6 +570,7 @@ async function runStep( messages: session.deriveMessages(), ...system ? { system } : {}, ...assembly.tools.length > 0 ? { tools: assembly.tools } : {}, + sessionId: session.id, signal, } request = await ctx.waterfall('agent/request', agent, turn, step, request, () => Promise.resolve(request)) diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts index 63fc0f5b0c..7b1b9bdc47 100644 --- a/packages/llm/llm/src/types.ts +++ b/packages/llm/llm/src/types.ts @@ -19,6 +19,7 @@ * ``` */ +import type { Branded } from '@deepseek-ai/dsh-brand' import type { CallId } from './brand.ts' /** Cache hint attached to a content block (provider-interpreted). */ @@ -192,4 +193,18 @@ export interface GenerateOptions { */ stop?: string[] signal?: AbortSignal + /** + * The id of the session this request belongs to — stamped by the agent loop + * from `agent.session.id`. Adapters ignore it; it lets an `llm/stream` listener + * route a call by WHICH session issued it (the replay adapter keys its per-call + * cursor by session, so a parent and its in-process subagent — each with its + * own session on one context — replay from their own recorded scripts). + * + * Typed as `Branded<'SessionId'>` rather than importing `SessionId` from + * `dsh-session`: that package imports `Message` from here, so importing its + * `SessionId` back would cycle. `SessionId` IS `Branded<'SessionId'>`, so a + * real session id assigns with no cast. (A future ids package could own the + * brand and dissolve this note.) + */ + sessionId?: Branded<'SessionId'> } diff --git a/packages/support/llm-replay/README.md b/packages/support/llm-replay/README.md index 91230cc113..06803eaf0e 100644 --- a/packages/support/llm-replay/README.md +++ b/packages/support/llm-replay/README.md @@ -10,26 +10,35 @@ The fixture IS the persisted session log (`/session.jsonl`). Its `assi Two failure modes are not reconstructable from `assistant/chunk` alone — a pure throw before any chunk (e.g. an HTTP 401, where the log holds only a `turn/end {error}` and no chunks) and a cancel/hang (timing, not chunk content). A scenario that needs those supplies an optional sidecar (`/replay.override.json`: a `ReplayEntry[]`) that REPLACES the derived script. +## Nested agents: per-session keying + +A scenario where a parent agent delegates to in-process subagents records more than one log: the parent (`session.jsonl`) plus one per child (`session.1.jsonl`, …). Each agent runs as its own `Session` on the same context, so replay must serve each one its own script. + +Replay keys every call by its calling session id (`GenerateOptions.sessionId`, stamped by the agent loop). Live session ids are freshly random each run and never equal the recorded ones, so a live session binds to a recorded script by **first-call order**: scripts are ordered by header `createdAt` (parent first — it streams before it can delegate), and the first live session to make any call claims the first script, the next new session the next, and so on. Each session then advances its own cursor. A call with no `sessionId` is one anonymous session bound to the primary script, so single-session scenarios behave exactly as before. More distinct live sessions than recorded scripts fails loud. + ## Config | Key | Type | Default | Notes | |---|---|---|---| -| `file` | string | `$DSH_SNAPSHOT_FILE` | Path to the per-scenario `session.jsonl` fixture. Required (config or env). | -| `overrideFile` | string | `$DSH_SNAPSHOT_OVERRIDE` | Optional path to a `ReplayEntry[]` sidecar that replaces the derived script. | +| `file` | string | `$DSH_SNAPSHOT_FILE` | Path to the primary (parent) `session.jsonl` fixture. Required (config or env). | +| `overrideFile` | string | `$DSH_SNAPSHOT_OVERRIDE` | Optional path to a `ReplayEntry[]` sidecar that replaces the PRIMARY session's derived script. | +| `childFiles` | string[] | `$DSH_SNAPSHOT_CHILD_FILES` (path-delimited) | Recorded subagent child-session logs for a nested scenario; empty for a single-session scenario. | ```yaml - id: llm-replay name: '@deepseek-ai/dsh-llm-replay' - # file/overrideFile default to $DSH_SNAPSHOT_FILE / $DSH_SNAPSHOT_OVERRIDE, - # set by the snapshot harness per scenario. + # file/overrideFile/childFiles default to $DSH_SNAPSHOT_FILE / + # $DSH_SNAPSHOT_OVERRIDE / $DSH_SNAPSHOT_CHILD_FILES, set by the snapshot + # harness per scenario. ``` ## Exports - `installLlmReplay(ctx, config)` — install the `llm/stream` listener; returns the disposer (HMR safety). Use this in tests to drive replay without the Loader or env vars. -- `loadReplayScript(config)` — resolve the `ReplayEntry[]` for a scenario (sidecar override if present, else derived from the JSONL; fail-loud if the fixture is missing). -- `deriveReplayScript(events)` / `parseSessionLog(text)` — the pure helpers that turn a recorded session log into a script. A derived group must end in a `finish` chunk; a group without one is the fingerprint of a thrown `stream()` and must instead be expressed via an override sidecar. -- Types `ReplayEntry` / `ReplayConfig` / `Config`. +- `loadSessionScripts(config)` — resolve the ordered `SessionScript[]` (primary + children) for a scenario, ready to bind to live sessions in first-call order. +- `loadReplayScript(config)` — resolve the `ReplayEntry[]` for the PRIMARY session only (sidecar override if present, else derived from the JSONL; fail-loud if the fixture is missing). +- `deriveReplayScript(events)` / `parseSessionLog(text)` / `parseSessionHeader(text)` — the pure helpers that turn a recorded session log into a script and read its header `id`/`createdAt`. A derived group must end in a `finish` chunk; a group without one is the fingerprint of a thrown `stream()` and must instead be expressed via an override sidecar. +- Types `ReplayEntry` / `SessionScript` / `ReplayConfig` / `Config`. ## Plugin export shape diff --git a/packages/support/llm-replay/src/index.ts b/packages/support/llm-replay/src/index.ts index fe24213768..ce761e6f13 100644 --- a/packages/support/llm-replay/src/index.ts +++ b/packages/support/llm-replay/src/index.ts @@ -14,6 +14,15 @@ * therefore "run the real agent once and harvest the `.jsonl`", done by the * snapshot harness — this plugin does not record. * + * A NESTED-agent scenario records more than one log: the parent plus one per + * in-process subagent (each subagent runs as its own {@link Session} on the same + * context). Replay loads them all ({@link loadSessionScripts}), derives a script + * per recorded session, and keys each live call by its calling session id + * (`GenerateOptions.sessionId`, stamped by the loop). Live session ids are fresh + * random values, so a live session binds to a recorded script by FIRST-CALL + * order (parent first — it streams before it delegates); see + * {@link installLlmReplay}. + * * Two failure modes are NOT reconstructable from `assistant/chunk` alone — a * pure throw before any chunk (e.g. an HTTP 401: the log holds only a * `turn/end {error}`, no chunks) and a cancel/hang (timing, not chunk content). @@ -36,6 +45,7 @@ */ import { existsSync, readFileSync } from 'node:fs' +import { delimiter as pathDelimiter } from 'node:path' import type { Context } from 'cordis' import type { SessionEvent } from '@deepseek-ai/dsh-session' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' @@ -65,14 +75,49 @@ export type ReplayEntry = /** Resolved plugin configuration. */ export interface ReplayConfig { - /** Path to the per-scenario `session.jsonl` fixture (the recorded log). */ + /** + * Path to the PRIMARY (parent) `session.jsonl` fixture. For a single-session + * scenario this is the only log; for a nested-agent scenario it is the parent, + * and the child logs ride in {@link childFiles}. + */ file: string /** - * Optional path to a `ReplayEntry[]` sidecar that REPLACES the derived - * script. Used by the two scenarios not expressible as `assistant/chunk` - * (pure throw-before-chunk, cancel/hang). Absent for normal scenarios. + * Optional `ReplayEntry[]` sidecar that REPLACES the derived script for the + * PRIMARY session. Used by the two single-session scenarios not expressible as + * `assistant/chunk` (pure throw-before-chunk, cancel/hang). Absent for normal + * and nested scenarios. */ overrideFile?: string + /** + * Additional recorded child-session logs (a nested-agent scenario's subagent + * sessions). Each is derived independently; the full set is ordered by + * `createdAt` so the parent (earliest) binds to the first live session. Empty + * for a single-session scenario. + */ + childFiles?: string[] +} + +/** + * One recorded session's replay script: the per-call entries plus the header + * facts needed to ORDER and key it. Live session ids are freshly random at + * replay time and never equal the recorded `id`, so the recorded id is only a + * diagnostic; `createdAt` is the load-bearing field — scripts are ordered by it + * (a parent is created before its children) and each newly-seen live session is + * bound to the next script in that order (= first-call order in the synchronous + * nested cut, where the parent streams before it delegates). + */ +export interface SessionScript { + /** The recorded session id (diagnostics only — the live id differs). */ + recordedId: string + /** Session creation time; the deterministic ordering key (parent < child). */ + createdAt: number + /** The per-`stream()`-call replay entries, in recorded call order. */ + entries: ReplayEntry[] + /** + * Whether this is the PRIMARY (parent) session. Breaks a `createdAt` tie in + * favor of the parent, which always issues the first model call. + */ + primary: boolean } /** @@ -93,6 +138,23 @@ export function parseSessionLog(text: string): SessionEvent[] { return events } +/** + * Read the identifying facts off a session log's header line (line 0): the + * recorded session `id` (diagnostics) and `createdAt` (the deterministic + * ordering key that binds a recorded script to a live session — see + * {@link SessionScript}). A header missing either field falls back to a stable + * default (`''` / `0`) rather than throwing: a no-model fixture is header-only + * and still orders fine as the single (primary) script. + */ +export function parseSessionHeader(text: string): { id: string; createdAt: number } { + const firstLine = text.split('\n').find(line => line.trim().length > 0) ?? '{}' + const parsed = JSON.parse(firstLine) as { id?: unknown; createdAt?: unknown } + return { + id: typeof parsed.id === 'string' ? parsed.id : '', + createdAt: typeof parsed.createdAt === 'number' ? parsed.createdAt : 0, + } +} + /** * Reconstruct the per-`stream()` replay script from a recorded session log. * @@ -144,11 +206,11 @@ export function deriveReplayScript(events: SessionEvent[]): ReplayEntry[] { } /** - * Build the replay script for a scenario: the sidecar override if present, - * otherwise the script derived from the recorded session JSONL. Fail-loud if - * the JSONL fixture is missing (the scenario was never recorded) — never - * silently returns an empty script, so a coverage hole can't masquerade as a - * passing replay. + * Build the replay script for the PRIMARY session: the sidecar override if + * present, otherwise the script derived from the recorded session JSONL. + * Fail-loud if the JSONL fixture is missing (the scenario was never recorded) — + * never silently returns an empty script, so a coverage hole can't masquerade + * as a passing replay. */ export function loadReplayScript(config: ReplayConfig): ReplayEntry[] { if (config.overrideFile !== undefined && existsSync(config.overrideFile)) { @@ -164,6 +226,54 @@ export function loadReplayScript(config: ReplayConfig): ReplayEntry[] { return deriveReplayScript(parseSessionLog(readFileSync(config.file, 'utf8'))) } +/** + * Load every recorded session's script for a scenario, ordered by `createdAt` + * (earliest first), ready to bind to live sessions in first-call order. + * + * The PRIMARY session (`config.file`, with its optional `overrideFile`) is the + * parent; each `config.childFiles` entry is a recorded subagent session. A + * single-session scenario has no `childFiles`, so this returns one script and + * behaves exactly like the old single-cursor replay. The primary always sorts + * first when ties occur (a sub-millisecond parent/child `createdAt` collision): + * the parent issues the FIRST model call (it must stream before it can delegate + * in the synchronous nested cut), so binding it to the first live session is + * correct regardless of a timestamp tie. + */ +export function loadSessionScripts(config: ReplayConfig): SessionScript[] { + const primaryEntries = loadReplayScript(config) + // The override path replaces the derived script but carries no header; read + // the header off the JSONL when it exists, else use a stable default so an + // override-only fixture (header-less) still orders first as the primary. + const primaryHeader = existsSync(config.file) + ? parseSessionHeader(readFileSync(config.file, 'utf8')) + : { id: '', createdAt: 0 } + const primary: SessionScript = { + recordedId: primaryHeader.id, createdAt: primaryHeader.createdAt, entries: primaryEntries, primary: true, + } + const children: SessionScript[] = [] + for (const childFile of config.childFiles ?? []) { + if (!existsSync(childFile)) { + throw new Error(`llm-replay: child fixture not found: ${childFile} — re-record the scenario`) + } + const text = readFileSync(childFile, 'utf8') + const header = parseSessionHeader(text) + children.push({ + recordedId: header.id, + createdAt: header.createdAt, + entries: deriveReplayScript(parseSessionLog(text)), + primary: false, + }) + } + // The primary (parent) always binds first — it issues the first model call, + // because it must run a turn before it can delegate. Children follow in + // createdAt order (the order they were spawned in the synchronous nested cut), + // ties broken by recorded id for determinism. Keeping the primary at the head + // rather than sorting it among the children means a sub-millisecond + // parent/child createdAt collision can never reorder it behind a child. + children.sort((a, b) => a.createdAt - b.createdAt || a.recordedId.localeCompare(b.recordedId)) + return [primary, ...children] +} + /** Yield a recorded stream back, honoring abort like a real adapter. */ async function* replayEntry(entry: ReplayEntry, signal: AbortSignal | undefined): AsyncIterable { switch (entry.kind) { @@ -206,32 +316,70 @@ async function* replayEntry(entry: ReplayEntry, signal: AbortSignal | undefined) * disposer (so a fiber dispose removes it — HMR safety). Exported separately * from {@link apply} so unit tests can drive it without the Loader or env vars. * - * Replay is POSITIONAL: the Nth `stream()` call serves the Nth script entry. - * This is deterministic only with at most one model stream in flight at a time; - * the snapshot harness runs one ACP session per scenario to guarantee that. The - * cursor is advanced synchronously at listener-invocation time (not lazily - * inside the generator) so call ORDER, not iteration order, fixes the mapping. + * Replay is PER-SESSION POSITIONAL: each recorded session has its own script + * (parent + any subagent children, loaded by {@link loadSessionScripts} ordered + * by `createdAt`), and the Nth `stream()` call FROM A GIVEN SESSION serves that + * session's Nth entry. The calling session is read off `options.sessionId` (the + * agent loop stamps it from `agent.session.id`). * - * TODO(subagent-snapshots): this single global cursor cannot route calls to the - * right agent when a parent and an in-process subagent both stream on one ctx. - * Snapshot coverage of nested agents needs either per-session-keyed replay (a - * `Map` fed by the calling agent on the `agent/request` - * waterfall, which carries the agent) or a call-ordered merge of the parent and - * child session logs (sound because subagent execution is strictly nested — - * the parent blocks on the child). Tracked as a stacked follow-up to the - * in-process subagent backends; see the subagent RFC's "Snapshot coverage of - * nested agents" deferral. + * Live session ids are freshly random and never equal the recorded ones, so a + * live session binds to a recorded script by FIRST-CALL ORDER: the first live + * session to make any call takes the first ordered script (the parent — earliest + * `createdAt`, and the first to stream because it must run before it delegates), + * the next new live session takes the next script, and so on. This keys by WHO + * calls rather than global call order, so it stays correct even if subagents + * ever run concurrently/backgrounded (a global cursor would interleave them). + * + * A call with no `sessionId` (a direct unit-test `ctx.llm.stream` that omits it) + * is treated as one anonymous session — it binds to the first script, so the + * single-session path behaves exactly as the old global cursor did. + * + * Each per-session cursor advances synchronously at listener-invocation time + * (not lazily inside the generator) so call ORDER within a session, not + * iteration order, fixes the mapping. */ export function installLlmReplay(ctx: Context, config: ReplayConfig): () => void { - const entries = loadReplayScript(config) - let cursor = 0 + const scripts = loadSessionScripts(config) + // Live-session → its bound script + cursor. A new live session id claims the + // next not-yet-bound script (scripts are in bind order); `nextScript` is the + // index of the next unclaimed one. + const bound = new Map() + let nextScript = 0 + const ANON = '\0anon\0' // the key for a call that carries no sessionId return ctx.on('llm/stream', (options: GenerateOptions, _next) => { - const index = cursor++ - const entry: ReplayEntry | undefined = entries[index] + const key = options.sessionId ?? ANON + let state = bound.get(key) + let unrecorded = false + if (state === undefined) { + const script = scripts[nextScript] + if (script === undefined) { + // More distinct live sessions made calls than the scenario recorded — + // an unrecorded subagent appeared. Defer the throw into the returned + // generator (the listener must return an AsyncIterable, not throw). + unrecorded = true + state = { entries: [], cursor: 0 } + } else { + nextScript++ + state = { entries: script.entries, cursor: 0 } + bound.set(key, state) + } + } + const boundState = state + const seenSessions = nextScript + const totalScripts = scripts.length + const index = boundState.cursor++ + const entry: ReplayEntry | undefined = boundState.entries[index] return (async function* () { + if (unrecorded) { + throw new Error( + `llm-replay: a model call arrived from an unrecorded session (#${seenSessions + 1}); ` + + `the scenario recorded only ${totalScripts} session(s) — re-record it`, + ) + } if (entry === undefined) { throw new Error( - `llm-replay: script exhausted — requested model call #${index + 1} but the fixture has only ${entries.length}; re-record the scenario`, + `llm-replay: script exhausted — session requested model call #${index + 1} ` + + `but its script has only ${boundState.entries.length}; re-record the scenario`, ) } yield* replayEntry(entry, options.signal) @@ -247,6 +395,12 @@ export interface Config { file?: string /** Override the sidecar path; defaults to `$DSH_SNAPSHOT_OVERRIDE`. */ overrideFile?: string + /** + * Override the child-log paths; defaults to `$DSH_SNAPSHOT_CHILD_FILES` (a + * path-separator-delimited list). Each is a recorded subagent session log for + * a nested-agent scenario; absent/empty for a single-session scenario. + */ + childFiles?: string[] } export function apply(ctx: Context, config: Config = {}): void { @@ -255,5 +409,12 @@ export function apply(ctx: Context, config: Config = {}): void { throw new Error('llm-replay: a fixture path is required (Config.file or $DSH_SNAPSHOT_FILE)') } const overrideFile = config.overrideFile ?? process.env.DSH_SNAPSHOT_OVERRIDE - installLlmReplay(ctx, overrideFile === undefined || overrideFile.length === 0 ? { file } : { file, overrideFile }) + const childEnv = process.env.DSH_SNAPSHOT_CHILD_FILES + const childFiles = config.childFiles + ?? (childEnv !== undefined && childEnv.length > 0 ? childEnv.split(pathDelimiter) : []) + installLlmReplay(ctx, { + file, + ...overrideFile !== undefined && overrideFile.length > 0 ? { overrideFile } : {}, + ...childFiles.length > 0 ? { childFiles } : {}, + }) } diff --git a/packages/support/llm-replay/tests/llm-replay.spec.ts b/packages/support/llm-replay/tests/llm-replay.spec.ts index 925881273a..a0c6268eff 100644 --- a/packages/support/llm-replay/tests/llm-replay.spec.ts +++ b/packages/support/llm-replay/tests/llm-replay.spec.ts @@ -7,12 +7,15 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session' import LlmService, { GenerateOptions, LlmAdapter, StreamChunk } from '@deepseek-ai/dsh-llm' import { type ReplayEntry, + type SessionScript, apply, deriveReplayScript, inject, installLlmReplay, loadReplayScript, + loadSessionScripts, name, + parseSessionHeader, parseSessionLog, } from '../src/index.ts' @@ -32,9 +35,14 @@ const TEXT_CHUNKS: StreamChunk[] = [ ] /** Build a minimal session-JSONL string: a header line + the given events. */ -function sessionJsonl(events: SessionEvent[]): string { - const header = JSON.stringify({ type: 'session', version: 0, id: 's1', createdAt: 0 }) - return [header, ...events.map(e => JSON.stringify(e))].join('\n') + '\n' +function sessionJsonl(events: SessionEvent[], header?: { id?: string; createdAt?: number }): string { + const headerLine = JSON.stringify({ + type: 'session', + version: 0, + id: header?.id ?? 's1', + createdAt: header?.createdAt ?? 0, + }) + return [headerLine, ...events.map(e => JSON.stringify(e))].join('\n') + '\n' } /** A SessionEvent of type assistant/chunk for (turn, step). */ @@ -361,13 +369,188 @@ describe('installLlmReplay (through the real waterfall)', () => { }) }) +describe('parseSessionHeader', () => { + it('reads id and createdAt off the header line', () => { + expect(parseSessionHeader(sessionJsonl([], { id: 'abc', createdAt: 42 }))) + .toEqual({ id: 'abc', createdAt: 42 }) + }) + + it('falls back to id="" / createdAt=0 when the header lacks them', () => { + expect(parseSessionHeader('{"type":"session","version":0}\n')).toEqual({ id: '', createdAt: 0 }) + }) + + it('falls back on an empty buffer (no header line)', () => { + expect(parseSessionHeader('')).toEqual({ id: '', createdAt: 0 }) + }) +}) + +describe('loadSessionScripts', () => { + /** Write a session log file and return its path. */ + function writeSession(filename: string, header: { id: string; createdAt: number }, calls: StreamChunk[][]): string { + let seq = 1 + const events: SessionEvent[] = [] + calls.forEach((chunks, step) => { for (const c of chunks) events.push(chunkEvent(seq++, 1, step + 1, c)) }) + const path = join(dir, filename) + writeFileSync(path, sessionJsonl(events, header), 'utf8') + return path + } + + it('returns one primary script for a single-session scenario', () => { + const f = writeSession('session.jsonl', { id: 'p', createdAt: 100 }, [TEXT_CHUNKS]) + const scripts: SessionScript[] = loadSessionScripts({ file: f }) + expect(scripts).toHaveLength(1) + expect(scripts[0]).toMatchObject({ recordedId: 'p', createdAt: 100, primary: true }) + expect(scripts[0]?.entries).toEqual([{ kind: 'chunks', chunks: TEXT_CHUNKS }]) + }) + + it('orders parent + children by createdAt with the primary first on a tie', () => { + const f = writeSession('session.jsonl', { id: 'parent', createdAt: 100 }, [TEXT_CHUNKS]) + // One child created LATER, one child sharing the parent's createdAt (tie). + const later = writeSession('session.1.jsonl', { id: 'late', createdAt: 200 }, [TEXT_CHUNKS]) + const tie = writeSession('session.2.jsonl', { id: 'tie', createdAt: 100 }, [TEXT_CHUNKS]) + const scripts = loadSessionScripts({ file: f, childFiles: [later, tie] }) + // parent (100, primary) → tie (100, non-primary) → late (200). + expect(scripts.map(s => s.recordedId)).toEqual(['parent', 'tie', 'late']) + expect(scripts[0]?.primary).toBe(true) + }) + + it('throws when a declared child fixture is missing', () => { + const f = writeSession('session.jsonl', { id: 'p', createdAt: 1 }, [TEXT_CHUNKS]) + expect(() => loadSessionScripts({ file: f, childFiles: [join(dir, 'absent.jsonl')] })) + .toThrow(/child fixture not found/) + }) + + it('uses the override for the primary and still derives children', () => { + writeFileSync(file, sessionJsonl([], { id: 'p', createdAt: 1 }), 'utf8') + const overrideFile = join(dir, 'replay.override.json') + const override: ReplayEntry[] = [{ kind: 'hang' }] + writeFileSync(overrideFile, JSON.stringify(override), 'utf8') + const child = writeSession('session.1.jsonl', { id: 'c', createdAt: 2 }, [TEXT_CHUNKS]) + const scripts = loadSessionScripts({ file, overrideFile, childFiles: [child] }) + expect(scripts[0]?.entries).toEqual(override) + expect(scripts[1]?.entries).toEqual([{ kind: 'chunks', chunks: TEXT_CHUNKS }]) + }) + + it('defaults the primary header to id="" / createdAt=0 when only an override (no JSONL) exists', () => { + // An override-only fixture: config.file does NOT exist, the override drives + // the primary script, so the header default branch applies. + const overrideFile = join(dir, 'replay.override.json') + writeFileSync(overrideFile, JSON.stringify([{ kind: 'hang' }]), 'utf8') + const scripts = loadSessionScripts({ file: join(dir, 'absent.jsonl'), overrideFile }) + expect(scripts).toHaveLength(1) + expect(scripts[0]).toMatchObject({ recordedId: '', createdAt: 0, primary: true }) + }) + + it('orders two same-createdAt children deterministically after the primary', () => { + // Two children sharing a createdAt (both non-primary): exercises the sort + // tie-break\'s "both same primary-ness" arm and a non-primary-vs-primary arm. + const f = writeSession('session.jsonl', { id: 'parent', createdAt: 100 }, [TEXT_CHUNKS]) + const c1 = writeSession('session.1.jsonl', { id: 'c1', createdAt: 100 }, [TEXT_CHUNKS]) + const c2 = writeSession('session.2.jsonl', { id: 'c2', createdAt: 100 }, [TEXT_CHUNKS]) + const scripts = loadSessionScripts({ file: f, childFiles: [c1, c2] }) + // Primary first (its createdAt ties the children but primary wins); the two + // children keep a stable relative order. + expect(scripts[0]?.recordedId).toBe('parent') + expect(scripts.every(s => s.createdAt === 100)).toBe(true) + expect(scripts.map(s => s.primary)).toEqual([true, false, false]) + }) + + it('keeps the primary first even when a child sorts BEFORE it in input order', () => { + // The primary is appended first internally but the child has an EARLIER + // createdAt — the primary must still win on the tie-break against a + // later-but-equal child, and lose only to a genuinely earlier child via + // createdAt (here the child is earlier, so order is child-then-primary only + // if createdAt strictly less; equal createdAt keeps primary first). + const f = writeSession('session.jsonl', { id: 'parent', createdAt: 100 }, [TEXT_CHUNKS]) + const earlier = writeSession('session.1.jsonl', { id: 'early', createdAt: 100 }, [TEXT_CHUNKS]) + const scripts = loadSessionScripts({ file: f, childFiles: [earlier] }) + // Equal createdAt → primary first. + expect(scripts.map(s => s.recordedId)).toEqual(['parent', 'early']) + }) +}) + +describe('installLlmReplay (per-session keying)', () => { + const second: StreamChunk[] = [ + { type: 'block-start', index: 0, blockType: 'text' }, + { type: 'text-delta', index: 0, text: 'child' }, + { type: 'finish', reason: { kind: 'stop' } }, + ] + + /** Write a session log file and return its path. */ + function writeSession(filename: string, header: { id: string; createdAt: number }, calls: StreamChunk[][]): string { + let seq = 1 + const events: SessionEvent[] = [] + calls.forEach((chunks, step) => { for (const c of chunks) events.push(chunkEvent(seq++, 1, step + 1, c)) }) + const path = join(dir, filename) + writeFileSync(path, sessionJsonl(events, header), 'utf8') + return path + } + + const live = (id: string): GenerateOptions => + ({ model: 'm', messages: [], sessionId: id as NonNullable }) + + it('routes each live session to its own script by FIRST-CALL order', async () => { + const parentFile = writeSession('session.jsonl', { id: 'rec-parent', createdAt: 100 }, [TEXT_CHUNKS]) + const childFile = writeSession('session.1.jsonl', { id: 'rec-child', createdAt: 200 }, [second]) + const ctx = new Context() + await ctx.plugin(LlmService) + installLlmReplay(ctx, { file: parentFile, childFiles: [childFile] }) + // The first live session to call binds to the parent script; a different + // live session id binds to the child script — regardless of recorded ids. + expect(await drain(ctx.llm.stream(live('live-A')))).toEqual(TEXT_CHUNKS) + expect(await drain(ctx.llm.stream(live('live-B')))).toEqual(second) + // The first session's SECOND call would exhaust its 1-entry script. + await expect(drain(ctx.llm.stream(live('live-A')))).rejects.toThrow(/exhausted/) + }) + + it('keeps each session\'s cursor independent (interleaved calls)', async () => { + const a2: StreamChunk[] = [{ type: 'text-delta', index: 0, text: 'a2' }, { type: 'finish', reason: { kind: 'stop' } }] + const b2: StreamChunk[] = [{ type: 'text-delta', index: 0, text: 'b2' }, { type: 'finish', reason: { kind: 'stop' } }] + const parentFile = writeSession('session.jsonl', { id: 'p', createdAt: 1 }, [TEXT_CHUNKS, a2]) + const childFile = writeSession('session.1.jsonl', { id: 'c', createdAt: 2 }, [second, b2]) + const ctx = new Context() + await ctx.plugin(LlmService) + installLlmReplay(ctx, { file: parentFile, childFiles: [childFile] }) + // Interleave: A#1, B#1, A#2, B#2 — each cursor advances per-session. + expect(await drain(ctx.llm.stream(live('A')))).toEqual(TEXT_CHUNKS) + expect(await drain(ctx.llm.stream(live('B')))).toEqual(second) + expect(await drain(ctx.llm.stream(live('A')))).toEqual(a2) + expect(await drain(ctx.llm.stream(live('B')))).toEqual(b2) + }) + + it('treats a call with no sessionId as the single anonymous (primary) session', async () => { + const parentFile = writeSession('session.jsonl', { id: 'p', createdAt: 1 }, [TEXT_CHUNKS]) + const ctx = new Context() + await ctx.plugin(LlmService) + installLlmReplay(ctx, { file: parentFile }) + // No sessionId at all — the legacy single-session path. + expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS) + }) + + it('fails loud when more distinct live sessions call than were recorded', async () => { + const parentFile = writeSession('session.jsonl', { id: 'p', createdAt: 1 }, [TEXT_CHUNKS]) + const ctx = new Context() + await ctx.plugin(LlmService) + installLlmReplay(ctx, { file: parentFile }) // only ONE recorded session + expect(await drain(ctx.llm.stream(live('first')))).toEqual(TEXT_CHUNKS) + // A SECOND distinct live session has no script to bind to. + await expect(drain(ctx.llm.stream(live('second')))).rejects.toThrow(/unrecorded session/) + }) +}) + describe('apply (the plugin entry)', () => { - const ORIG = { file: process.env.DSH_SNAPSHOT_FILE, override: process.env.DSH_SNAPSHOT_OVERRIDE } + const ORIG = { + file: process.env.DSH_SNAPSHOT_FILE, + override: process.env.DSH_SNAPSHOT_OVERRIDE, + children: process.env.DSH_SNAPSHOT_CHILD_FILES, + } afterEach(() => { if (ORIG.file === undefined) delete process.env.DSH_SNAPSHOT_FILE else process.env.DSH_SNAPSHOT_FILE = ORIG.file if (ORIG.override === undefined) delete process.env.DSH_SNAPSHOT_OVERRIDE else process.env.DSH_SNAPSHOT_OVERRIDE = ORIG.override + if (ORIG.children === undefined) delete process.env.DSH_SNAPSHOT_CHILD_FILES + else process.env.DSH_SNAPSHOT_CHILD_FILES = ORIG.children }) it('exposes the namespace plugin shape (name/inject, no default export)', () => { @@ -418,4 +601,52 @@ describe('apply (the plugin entry)', () => { await ctx.plugin(LlmService) expect(() => { apply(ctx, { file: '' }) }).toThrow(/a fixture path is required/) }) + + it('loads child fixtures from config.childFiles (per-session routing)', async () => { + const childSecond: StreamChunk[] = [ + { type: 'block-start', index: 0, blockType: 'text' }, + { type: 'text-delta', index: 0, text: 'kid' }, + { type: 'finish', reason: { kind: 'stop' } }, + ] + writeFileSync(file, sessionJsonl(TEXT_CHUNKS.map((c, i) => chunkEvent(i + 1, 1, 1, c)), { id: 'p', createdAt: 1 }), 'utf8') + const childFile = join(dir, 'session.1.jsonl') + writeFileSync(childFile, sessionJsonl(childSecond.map((c, i) => chunkEvent(i + 1, 1, 1, c)), { id: 'c', createdAt: 2 }), 'utf8') + const ctx = new Context() + await ctx.plugin(LlmService) + apply(ctx, { file, childFiles: [childFile] }) + const live = (id: string): GenerateOptions => + ({ model: 'm', messages: [], sessionId: id as NonNullable }) + expect(await drain(ctx.llm.stream(live('A')))).toEqual(TEXT_CHUNKS) + expect(await drain(ctx.llm.stream(live('B')))).toEqual(childSecond) + }) + + it('falls back to $DSH_SNAPSHOT_CHILD_FILES (path-delimited) when config omits childFiles', async () => { + const childChunks: StreamChunk[] = [ + { type: 'block-start', index: 0, blockType: 'text' }, + { type: 'text-delta', index: 0, text: 'env-kid' }, + { type: 'finish', reason: { kind: 'stop' } }, + ] + writeFileSync(file, sessionJsonl(TEXT_CHUNKS.map((c, i) => chunkEvent(i + 1, 1, 1, c)), { id: 'p', createdAt: 1 }), 'utf8') + const childFile = join(dir, 'session.1.jsonl') + writeFileSync(childFile, sessionJsonl(childChunks.map((c, i) => chunkEvent(i + 1, 1, 1, c)), { id: 'c', createdAt: 2 }), 'utf8') + process.env.DSH_SNAPSHOT_FILE = file + process.env.DSH_SNAPSHOT_CHILD_FILES = childFile // single entry, no delimiter needed + const ctx = new Context() + await ctx.plugin(LlmService) + apply(ctx) + const live = (id: string): GenerateOptions => + ({ model: 'm', messages: [], sessionId: id as NonNullable }) + expect(await drain(ctx.llm.stream(live('A')))).toEqual(TEXT_CHUNKS) + expect(await drain(ctx.llm.stream(live('B')))).toEqual(childChunks) + }) + + it('ignores an empty $DSH_SNAPSHOT_CHILD_FILES (single-session)', async () => { + writeFileSync(file, sessionJsonl(TEXT_CHUNKS.map((c, i) => chunkEvent(i + 1, 1, 1, c)), { id: 'p', createdAt: 1 }), 'utf8') + process.env.DSH_SNAPSHOT_FILE = file + process.env.DSH_SNAPSHOT_CHILD_FILES = '' + const ctx = new Context() + await ctx.plugin(LlmService) + apply(ctx) + expect(await drain(ctx.llm.stream({ model: 'm', messages: [] }))).toEqual(TEXT_CHUNKS) + }) }) From 413db680088eb7cebf95122e786e894519f6fc8d Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:01:09 +0800 Subject: [PATCH 2/3] Clarify the child-ordering invariant (Codex review follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The createdAt+recordedId child sort comment over-claimed "tie-safe". Codex flagged that a same-millisecond sibling tie would be broken by random session id, which does not recover first-call order. In the current synchronous cut that tie is unreachable — the subagent tool awaits one child's result and disposes it before the parent starts the next, so siblings' createdAt values are strictly ordered and match first-call order. Restate the comment to that real invariant (at both the replay sort and the harvest sort), note that the id tiebreak only makes a degenerate collision deterministic, and flag the concurrent-subagent cut that would need a real first-call ordinal with XXX(concurrent-subagents). The RFC records the same limitation. Comment/doc only — no behavior change. --- .../2026-06-22-subagent-snapshot-replay.md | 2 ++ examples/acp-agent/tests/snapshot-harness.ts | 9 +++++++-- packages/support/llm-replay/src/index.ts | 16 ++++++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md b/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md index 92a324104e..422c8d0301 100644 --- a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md +++ b/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md @@ -29,6 +29,8 @@ Live session ids are freshly random every run and never equal the recorded ones, This keys by WHO calls, not by global call order — so it stays correct even if subagents ever run concurrently or in the background (a global cursor would interleave them). A call carrying no `sessionId` (a direct unit-test `stream()`) is treated as one anonymous session bound to the primary script, so the single-session path is byte-for-byte the old behavior. More distinct live sessions than recorded scripts is a fail-loud error (an unrecorded subagent appeared), never a silent mis-route. +The ordering key is the session header `createdAt`. In the current synchronous cut this is sound because sibling children are created **strictly sequentially** — the subagent tool awaits one child's result and disposes it before the parent's next tool call starts the next child — so their `createdAt` values are strictly ordered and match first-call order exactly. A same-millisecond sibling tie is therefore unreachable; the `recordedId` tiebreak only keeps such a degenerate collision deterministic, it does not recover first-call order. A future cut that runs siblings concurrently/backgrounded WOULD be able to create two children in the same millisecond, and must then thread a real first-call ordinal (the order live sessions first stream) rather than leaning on `createdAt` — flagged with `XXX(concurrent-subagents)` at the sort site. + The alternative considered and rejected was a **call-ordered merge of the parent and child logs** into one global script (sound only because in-process subagent execution is strictly nested — the parent blocks on the child). It is simpler for today's synchronous cut but bakes in the parent-blocks-on-child invariant that a future backgrounded/concurrent subagent would break; per-session keying does not. ### 3. The harness harvests every log, primary-first diff --git a/examples/acp-agent/tests/snapshot-harness.ts b/examples/acp-agent/tests/snapshot-harness.ts index 80847f08dd..8285b870bf 100644 --- a/examples/acp-agent/tests/snapshot-harness.ts +++ b/examples/acp-agent/tests/snapshot-harness.ts @@ -369,8 +369,13 @@ async function harvestSessionLogs(root: string): Promise { } } // Primary (no parentSession) first, then children by ascending createdAt. A - // scenario has exactly one top-level session; ties among children fall back to - // recorded id for a stable order. + // scenario has exactly one top-level session. In the synchronous cut sibling + // children are created strictly sequentially, so their createdAt values are + // strictly ordered; the recordedId tiebreak only keeps a degenerate + // same-millisecond collision (unreachable here) deterministic. This harvest + // order must match the replay load order in dsh-llm-replay's loadSessionScripts + // so session..jsonl maps to the same child on record and replay — replay + // re-sorts childFiles by the same key, so the two stay consistent. logs.sort((a, b) => { const ap = a.parentSession === undefined ? 0 : 1 const bp = b.parentSession === undefined ? 0 : 1 diff --git a/packages/support/llm-replay/src/index.ts b/packages/support/llm-replay/src/index.ts index ce761e6f13..b804c8ebd9 100644 --- a/packages/support/llm-replay/src/index.ts +++ b/packages/support/llm-replay/src/index.ts @@ -266,10 +266,18 @@ export function loadSessionScripts(config: ReplayConfig): SessionScript[] { } // The primary (parent) always binds first — it issues the first model call, // because it must run a turn before it can delegate. Children follow in - // createdAt order (the order they were spawned in the synchronous nested cut), - // ties broken by recorded id for determinism. Keeping the primary at the head - // rather than sorting it among the children means a sub-millisecond - // parent/child createdAt collision can never reorder it behind a child. + // createdAt order. In the current synchronous cut sibling children are created + // STRICTLY SEQUENTIALLY — the subagent tool awaits one child's result and + // disposes it before the parent's next tool call can start the next — so their + // createdAt values are strictly ordered and match first-call order exactly. + // The recordedId tiebreak only makes a degenerate same-millisecond collision + // (unreachable in this cut) deterministic; it does NOT recover first-call + // order, so it is arbitrary if such a tie ever occurs. + // XXX(concurrent-subagents): a future cut that runs siblings concurrently or + // backgrounded could create two children in the same millisecond, where this + // createdAt+id order may diverge from first-call order. That cut must thread a + // real first-call ordinal (the order live sessions first stream) instead of + // leaning on createdAt — see the per-session-replay RFC. children.sort((a, b) => a.createdAt - b.createdAt || a.recordedId.localeCompare(b.recordedId)) return [primary, ...children] } From 34f6f28716eb307cabc23a9e8bc0d8b0a94be9c7 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:00:59 +0800 Subject: [PATCH 3/3] Make fork reachable by the model in the acp-agent demo (review feedback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The acp-agent cordis configs loaded the fork backend but bound only one dsh-tool-subagent (to spawn), so the comment's claim that a multi-child scenario could exercise both transports was false — fork was loaded but unreachable by the model. Register a second dsh-tool-subagent bound to fork with a distinct toolName (subagent_fork), matching the coding-agent demo, in both cordis.yml (record/demo) and cordis.snapshot.yml (replay). Snapshot goldens are unchanged (the transcript does not capture the available-tool list). --- examples/acp-agent/cordis.snapshot.yml | 13 +++++++++++-- examples/acp-agent/cordis.yml | 17 +++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/examples/acp-agent/cordis.snapshot.yml b/examples/acp-agent/cordis.snapshot.yml index 5bee10f2a7..5f36a11efa 100644 --- a/examples/acp-agent/cordis.snapshot.yml +++ b/examples/acp-agent/cordis.snapshot.yml @@ -42,8 +42,10 @@ a fresh child agent (it works in its own context and returns only its final result) — give it a complete, standalone instruction. -# The subagent seam + both in-process backends + the model-facing `subagent` -# tool — identical to cordis.yml's wiring (only the LLM backend differs above). +# The subagent seam + both in-process backends + two model-facing tools — +# identical to cordis.yml's wiring (only the LLM backend differs above): spawn +# and fork are each reachable via a dsh-tool-subagent bound to it with a distinct +# toolName (subagent → spawn, subagent_fork → fork). - id: subagent name: '@deepseek-ai/dsh-subagent' @@ -61,3 +63,10 @@ name: '@deepseek-ai/dsh-tool-subagent' config: provider: spawn + toolName: subagent + +- id: tool-subagent-fork + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: fork + toolName: subagent_fork diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml index e00e868dce..a00d0e6036 100644 --- a/examples/acp-agent/cordis.yml +++ b/examples/acp-agent/cordis.yml @@ -51,10 +51,12 @@ a fresh child agent (it works in its own context and returns only its final result) — give it a complete, standalone instruction. -# The subagent seam + both in-process backends + the model-facing `subagent` -# tool, as leaf entries after the app (which provides ctx.agents/ctx.tools). The -# tool is bound to the `spawn` backend (a fresh child); the `fork` backend is -# loaded too so a multi-child scenario can exercise both transports. +# The subagent seam + both in-process backends + two model-facing tools, as leaf +# entries after the app (which provides ctx.agents/ctx.tools). spawn (a fresh +# child) and fork (a child seeded with the parent's completed-turn prefix) are +# both reachable by the model: dsh-tool-subagent is loaded once per backend with +# a distinct toolName (subagent → spawn, subagent_fork → fork), so a multi-child +# scenario can exercise both transports. - id: subagent name: '@deepseek-ai/dsh-subagent' @@ -72,3 +74,10 @@ name: '@deepseek-ai/dsh-tool-subagent' config: provider: spawn + toolName: subagent + +- id: tool-subagent-fork + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: fork + toolName: subagent_fork