From 992cf894af8033050714c09d172b5df78e60ac8e Mon Sep 17 00:00:00 2001 From: _Kerman Date: Fri, 24 Jul 2026 22:38:50 +0800 Subject: [PATCH] refactor(agent): unify sourced message delivery --- ...send-and-coalesced-user-messages.i18n.yaml | 4 +- ...nified-send-and-coalesced-user-messages.md | 16 +- ...ied-send-and-coalesced-user-messages.zh.md | 16 +- ...ction-summary-prefix-cache-reuse.i18n.yaml | 4 +- ...1-compaction-summary-prefix-cache-reuse.md | 6 +- ...ompaction-summary-prefix-cache-reuse.zh.md | 6 +- .../2026-06-14-acp-agent-client-protocol.md | 2 +- .../feature/2026-06-24-workspace-context.md | 26 +- .../feature/2026-06-30-hook-bridges.md | 12 +- .../feature/2026-06-30-interception-seams.md | 16 +- .../implemented/feature/2026-07-06-sandbox.md | 4 +- .../feature/2026-07-07-session-prefix.md | 2 + ...26-07-19-model-facing-goal-tools.i18n.yaml | 4 +- .../2026-07-19-model-facing-goal-tools.md | 4 +- .../2026-07-19-model-facing-goal-tools.zh.md | 4 +- ...9-same-session-goal-round-driver.i18n.yaml | 4 +- ...26-07-19-same-session-goal-round-driver.md | 3 +- ...07-19-same-session-goal-round-driver.zh.md | 3 +- ...-prune-producerless-vocabulary-variants.md | 2 +- .../2026-07-17-one-send-one-turn.i18n.yaml | 4 +- .../2026-07-17-one-send-one-turn.md | 4 +- .../2026-07-17-one-send-one-turn.zh.md | 4 +- ...xt-injection-from-turn-execution.i18n.yaml | 4 +- ...e-context-injection-from-turn-execution.md | 4 +- ...ontext-injection-from-turn-execution.zh.md | 4 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 4 +- docs/architecture.zh.md | 4 +- docs/config-catalog.md | 10 +- docs/cookbook/adding-a-tool.i18n.yaml | 4 +- docs/cookbook/adding-a-tool.md | 2 +- docs/cookbook/adding-a-tool.zh.md | 2 +- docs/cookbook/extension-cookbook.i18n.yaml | 4 +- docs/cookbook/extension-cookbook.md | 11 +- docs/cookbook/extension-cookbook.zh.md | 11 +- docs/cordis-catalog/events.md | 37 +- docs/core-data-structures/core.i18n.yaml | 4 +- docs/core-data-structures/core.md | 63 +-- docs/core-data-structures/core.zh.md | 63 +-- docs/core-data-structures/llm-streaming.md | 2 +- .../core-data-structures/session-reference.md | 2 +- docs/core-data-structures/session.i18n.yaml | 4 +- docs/core-data-structures/session.md | 32 +- docs/core-data-structures/session.zh.md | 32 +- docs/core-data-structures/skills.md | 2 +- docs/core-data-structures/tools.md | 12 +- docs/event-producer-consumer.md | 30 +- docs/persistence-catalog.md | 42 +- docs/user/develop/framework/events.i18n.yaml | 4 +- docs/user/develop/framework/events.md | 2 +- docs/user/develop/framework/events.zh.md | 2 +- examples/acp-agent/tests/acp.snapshot.ts | 8 +- .../goal-session/session.expected.jsonl | 8 +- .../tests/snapshots/bash-spill/session.jsonl | 2 +- .../code-mode-workspace-context/session.jsonl | 373 +++++++++--------- .../cordis-inspect-jsdoc/session.jsonl | 2 +- .../stdout.expected.jsonl | 2 +- .../escalation-approved/session.jsonl | 239 +++++------ .../escalation-rejected/session.jsonl | 263 ++++++------ .../tests/snapshots/fs-edit/session.jsonl | 2 +- .../fs-escalation-approved/session.jsonl | 153 +++---- .../snapshots/fs-policy-reject/session.jsonl | 4 +- .../fs-write-overwrite/session.jsonl | 2 +- .../tests/snapshots/fs-write/session.jsonl | 2 +- .../hook-cc-pretool-ask/session.jsonl | 4 +- .../hook-cc-promptsubmit-block/session.jsonl | 5 - .../session.jsonl | 72 ++-- .../session.jsonl | 5 - .../session.jsonl | 110 +++--- .../permission-switching/session.jsonl | 365 ++++++++--------- .../snapshots/plan-mode-reject/session.jsonl | 6 +- .../tests/snapshots/plan-mode/session.jsonl | 10 +- .../session-sandbox-root/session.jsonl | 43 +- .../tests/snapshots/skill-load/session.jsonl | 53 +-- .../snapshots/subagent-fork/session.1.jsonl | 95 +++-- .../snapshots/subagent-mixed/session.2.jsonl | 87 ++-- .../snapshots/workspace-context/session.jsonl | 43 +- .../cordis-agent/tests/cordis-tools.e2e.ts | 16 +- .../headless-agent/tests/code-mode.e2e.ts | 8 +- .../headless-agent/tests/coding-task.e2e.ts | 4 +- .../headless-agent/tests/compaction.e2e.ts | 4 +- .../headless-agent/tests/full-loop.e2e.ts | 2 +- examples/headless-agent/tests/resume.e2e.ts | 4 +- .../tests/snapshots/pty-tools/session.jsonl | 2 +- .../headless-agent/tests/todo-write.e2e.ts | 4 +- .../terminal.expected.txt | 32 +- .../bash/tool-bash/tests/integration.spec.ts | 10 +- packages/compact/compact-basic/src/region.ts | 8 +- .../compact/compact-basic/src/summarizer.ts | 2 +- .../compact-basic/tests/compact-basic.spec.ts | 17 +- .../tests/compact-loop-repro.spec.ts | 10 +- packages/context/README.md | 2 +- packages/context/session-reference/README.md | 2 +- .../context/session-reference/src/index.ts | 6 +- .../context/session-reference/src/types.ts | 5 +- packages/context/time-context/README.md | 2 +- packages/context/time-context/src/index.ts | 5 +- .../time-context/tests/time-context.spec.ts | 11 +- packages/context/workspace-context/README.md | 24 +- .../context/workspace-context/src/index.ts | 6 +- .../context/workspace-context/src/state.ts | 13 +- .../tests/workspace-context.e2e.ts | 8 +- .../tests/workspace-context.spec.ts | 27 +- .../cordis/tool-cordis/src/api-catalog.ts | 30 +- .../tool-cordis/tests/integration.spec.ts | 2 +- packages/core/agent-loop/src/agent.ts | 25 +- packages/core/agent-loop/src/tool-calls.ts | 7 +- .../agent-loop/tests/agent-initiator.spec.ts | 2 +- packages/core/agent-loop/tests/agent.spec.ts | 18 +- packages/core/agent-loop/tests/cancel.spec.ts | 24 +- .../tests/config-session-id.spec.ts | 18 +- .../tests/contract-regressions.spec.ts | 18 +- .../agent-loop/tests/coverage-edges.spec.ts | 2 +- .../agent-loop/tests/interception.spec.ts | 11 +- packages/core/agent-loop/tests/loop.spec.ts | 41 +- .../core/agent-loop/tests/properties.spec.ts | 6 +- .../agent-loop/tests/request-cache.e2e.ts | 4 +- .../agent-loop/tests/request-error.spec.ts | 8 +- .../tests/request-reconstruction.spec.ts | 4 +- packages/core/agent-loop/tests/resume.spec.ts | 12 +- .../agent-loop/tests/scope-lifecycle.spec.ts | 6 +- .../core/agent-loop/tests/tool-calls.spec.ts | 32 +- .../core/agent-loop/tests/tool-order.spec.ts | 4 +- packages/core/agent/README.md | 12 +- packages/core/agent/src/types.ts | 49 +-- packages/core/session/README.md | 2 +- packages/core/tools/README.md | 2 +- packages/core/tools/src/index.ts | 22 +- .../agent-spine-demo/tests/agent-core.spec.ts | 8 +- packages/examples/cli-demo/src/cli.ts | 2 +- packages/examples/cli-demo/tests/cli.spec.ts | 4 +- packages/fs/tool-fs/tests/fs-tools.e2e.ts | 8 +- .../command-goal/tests/command-goal.spec.ts | 30 +- packages/goal/goal-session/src/index.ts | 11 +- .../goal-session/tests/goal-session.spec.ts | 32 +- packages/goal/goal/src/index.ts | 3 +- packages/goal/goal/tests/goal.spec.ts | 57 +-- packages/goal/tool-goal/README.md | 2 +- .../goal/tool-goal/tests/tool-goal.spec.ts | 12 +- packages/guard/repeat-tool-guard/src/index.ts | 7 +- .../tests/repeat-tool-guard.spec.ts | 36 +- packages/hooks/hook-protocol/README.md | 2 +- packages/hooks/hooks-claude/README.md | 2 +- packages/hooks/hooks-claude/src/index.ts | 33 +- .../hooks/hooks-claude/tests/bridge.spec.ts | 27 +- .../hooks-claude/tests/coverage-cases.ts | 68 ++-- packages/hooks/hooks-codex/README.md | 2 +- packages/hooks/hooks-codex/src/index.ts | 29 +- .../hooks/hooks-codex/tests/bridge.spec.ts | 14 +- .../hooks/hooks-codex/tests/coverage-cases.ts | 68 ++-- packages/host/runtime/src/api-proxy.ts | 4 +- .../host/runtime/tests/host-runtime.spec.ts | 10 +- .../tests/loader-composition.spec.ts | 2 +- packages/llm/llm-retry/tests/retry.spec.ts | 22 +- .../llm/token-meter/tests/token-meter.spec.ts | 7 +- packages/plan/plan-mode/src/index.ts | 2 +- .../plan/plan-mode/tests/integration.spec.ts | 8 +- .../plan/plan-mode/tests/plan-mode.spec.ts | 5 +- .../tests/fixtures/crash-child.ts | 2 +- .../tests/search-helpers.spec.ts | 19 +- packages/skill/tool-skill/README.md | 12 +- packages/skill/tool-skill/src/index.ts | 4 +- .../skill/tool-skill/tests/tool-skill.spec.ts | 11 +- .../tests/multi-subagent.spec.ts | 4 +- .../subagent-fork/tests/subagent-fork.spec.ts | 14 +- .../subagent/subagent-inprocess/README.md | 2 +- .../subagent/subagent-inprocess/src/index.ts | 2 +- .../tests/structured.spec.ts | 8 +- .../tests/subagent-inprocess.spec.ts | 2 +- .../subagent-spawn/tests/spawn.e2e.ts | 4 +- .../tests/subagent-spawn.spec.ts | 4 +- packages/support/acp-snapshot/README.md | 2 +- .../support/acp-snapshot/src/normalize.ts | 10 +- packages/support/acp-snapshot/src/suite.ts | 8 +- .../acp-snapshot/tests/normalize.spec.ts | 31 +- packages/tasks/tool-tasks/src/index.ts | 8 +- .../tasks/tool-tasks/tests/tool-tasks.spec.ts | 22 +- .../todo/tool-todo/tests/integration.spec.ts | 4 +- packages/ui/acp/acp-feature-support.md | 2 +- packages/ui/acp/src/index.ts | 24 +- packages/ui/acp/tests/config-options.spec.ts | 2 +- packages/ui/acp/tests/dispose.spec.ts | 4 +- packages/ui/acp/tests/edges.spec.ts | 2 +- packages/ui/acp/tests/turns.spec.ts | 45 ++- packages/ui/jsonrpc/src/server.ts | 2 +- packages/ui/jsonrpc/tests/server.spec.ts | 11 +- packages/ui/tui/src/index.ts | 7 +- packages/ui/tui/tests/harness.ts | 37 +- .../snapshots/session-reference.expected.txt | 14 +- .../snapshots/untrusted-controls.expected.txt | 27 +- packages/ui/tui/tests/tui.snapshot.ts | 5 - packages/ui/tui/tests/tui.spec.ts | 10 +- packages/ui/user-approval/src/index.ts | 8 +- .../ui/user-approval/tests/approval.spec.ts | 4 +- .../tool-ralph/tests/integration.spec.ts | 2 +- scripts/smoke-python-runtime.py | 2 - scripts/type-equiv.manifest.json | 20 - 197 files changed, 1890 insertions(+), 2132 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml index dacf0b9564..7b9f3b3418 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-22-unified-send-and-coalesced-user-messages.md: ae2d25f56fedc8fc05fbad1793003b393e3a8feb -2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 77fa248f50867faa3747c5dcea70e63032c98113 +2026-07-22-unified-send-and-coalesced-user-messages.md: 1716728432a179db0b0a6419ef20e0c2278d53f2 +2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 3569777a280c9f2a378825ace0e76cff4a2c17bb diff --git a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md index ae2d25f56f..1716728432 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md +++ b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md @@ -12,33 +12,33 @@ Separately, `context/message` and `user/message` had converged: the surface proj ## Decision -**One primitive, three preset aliases.** `Agent` is now an abstract class whose single abstract `send(content, { target, wakeup, source })` covers the (`target` × `wakeup`) matrix. Its complete options object is required, so routing and provenance remain explicit. `followup` (`next-turn`/wakeup), `steer` (`next-step`/wakeup), and `inject` (`next-step`/no-wakeup) are concrete delegates on the base class, so concrete drivers implement `send` once and callers use `followup(content)` for the ordinary user-message preset. `wakeup` means "make the model run": wake a parked driver for a `next-turn` item, or force a continuation for a running `next-step` item. `next-turn`/no-wakeup (queue without waking) is representable with no alias and no current caller. +**One primitive, three preset aliases.** The `Agent` interface's `send(input, { target, wakeup })` covers the (`target` × `wakeup`) matrix. Its `UserMessageData` input owns the inseparable model-facing `content` and producer `source`; the complete `SendOptions` owns only routing policy. `followup` (`next-turn`/wakeup), `steer` (`next-step`/wakeup), and `inject` (`next-step`/no-wakeup) each accept that one input and fix the policy. `wakeup` means "make the model run": wake a parked driver for a `next-turn` item, or force a continuation for a running `next-step` item. `next-turn`/no-wakeup (queue without waking) is representable with no alias and no current caller. -**inject keeps its mechanism.** The `next-step`/no-wakeup path is exactly the old `inject`: durable model-facing context appended at the current log position, deferred behind an executing tool batch while a turn is open and appended directly between turns while idle. It bypasses the FIFOs entirely and defaults its source to `{ kind: 'plugin', plugin: '' }`, never `{ kind: 'user' }`. +**inject keeps its mechanism.** The `next-step`/no-wakeup path is exactly the old `inject`: durable model-facing context appended at the current log position, deferred behind an executing tool batch while a turn is open and appended directly between turns while idle. It bypasses the FIFOs entirely, while its required `UserMessageData.source` preserves the caller's explicit provenance. -**context/message is gone.** Injected context is now a `user/message` whose `source` is a non-`user` kind. Typed source variants carry any domain-specific durable provenance. The surface, derivation, and `SurfaceEventType` drop `context/message`; consumers that need "is this a human prompt?" read `source.kind === 'user'` instead of the event type. This keeps goal-authority's human-authority check exactly as strict as before — an injected message defaults to a plugin source and can never satisfy `source.kind === 'user'`. +**context/message is gone.** Injected context is now a `user/message`; context producers supply the appropriate non-user `source` explicitly, and typed source variants carry any domain-specific durable provenance. The surface, derivation, and `SurfaceEventType` drop `context/message`; consumers that need "is this a human prompt?" read `source.kind === 'user'` instead of the event type. **Goal replay disambiguates by round, not type.** A goal state change is a round-zero goal-sourced `user/message` whose source carries the complete change; a positive round is an admitted continuation prompt. `decodeGoalEvent` takes a `user/message` and fails loud when goal-state content and its typed source disagree. **`send` returns an id.** `send` (and the aliases) return an opaque branded `AgentMessageId` for the accepted message; `send`'s previous return was `void`. -**Three inbox events replace agent/queued.** `agent/inbox/enqueue` (an item entered a FIFO), `agent/inbox/dequeue` (the driver claimed one), and `agent/inbox/discard` (`cancel()` dropped pending items) each type their `AgentMessage` payload with only the accepted message's returned `id`, content, and source, so a caller can correlate a queued item with its lifecycle without depending on driver routing state. Injection never touches a FIFO and emits none of these. Every FIFO entry publishes an enqueue, including the loop-authored continuation-reason steer (`agent/turn-continuation` returning `{ action: 'continue', reason }`), so the ledger stays balanced with its later dequeue or discard. The `dsh-agent` invariant companion asserts FIFO conservation: a per-agent outstanding count that dequeue and discard can never drive negative. +**Three inbox events replace agent/queued.** `agent/inbox/enqueue` (an item entered a FIFO), `agent/inbox/dequeue` (the driver claimed one), and `agent/inbox/discard` (`cancel()` dropped pending items) each type their `AgentMessage` payload with only the accepted message's returned `id`, content, and source, so a caller can correlate a queued item with its lifecycle without depending on driver routing state. Injection never touches a FIFO and emits none of these. Every FIFO entry publishes an enqueue, including steering submitted by an `agent/stopping` listener, so the ledger stays balanced with its later dequeue or discard. The `dsh-agent` invariant companion asserts FIFO conservation: a per-agent outstanding count that dequeue and discard can never drive negative. -**One accepted message keeps one representation.** Durable user-role input uses `UserMessageData { content, source }`; `AdditionalContext` names that same shape, public `AgentMessage` extends it with the correlation `id`, and the loop-private `PendingMessage` extends that with `wakeup`. A queued message that becomes steering enters the outbox as the same `PendingMessage` object, while injected and tool-produced context enters as plain `UserMessageData`. The outbox therefore stores their union directly instead of wrapping steering beside a duplicate copy of its content and source. Provider-native assistant messages remain adapter-owned output types and do not participate in this input hierarchy. +**One accepted message keeps one representation.** Durable user-role input and additional model-facing context both use `UserMessageData { content, source }` directly; public `AgentMessage` extends it with the correlation `id`, and the loop-private `PendingMessage` extends that with `wakeup`. A queued message that becomes steering enters the outbox as the same `PendingMessage` object, while injected and tool-produced context enters as plain `UserMessageData`. The outbox therefore stores their union directly instead of wrapping steering beside a duplicate copy of its content and source. Provider-native assistant messages remain adapter-owned output types and do not participate in this input hierarchy. **cancel gains keepInbox.** `cancel(cause, { keepInbox? })`; callers choose the cause explicitly, and `keepInbox: true` aborts the active turn while preserving queued and steering items (no discard event, and un-started work is not dropped). ## Alternatives considered -- **A dedicated `MessageSource` kind `context`** for injected content. Rejected because `plugin` already means "not a human," so a fourth kind would add a parallel axis the authority checks would have to learn. Injected context defaults to a plugin source instead. +- **A dedicated `MessageSource` kind `context`** for injected content. Rejected because `plugin` already means "not a human," so a fourth kind would add a parallel axis the authority checks would have to learn. Plugin-produced injected context supplies its plugin source explicitly. - **A typed discriminant field on `UserMessageData`** (e.g. `origin: 'prompt' | 'context'`) to replace the event-type split. Rejected in favor of `source`, which every consumer already carries and which the goal system already keyed on; a second discriminant would duplicate that fact. - **Keeping `agent/queued` alongside the inbox events.** Rejected as a mirror: `agent/inbox/enqueue` is the same enqueue-time signal with the added `target`/`wakeup` facts, and the dequeue/discard events complete the FIFO lifecycle the single event could not describe. ## Consequences -The delivery surface is now one primitive plus three self-documenting presets, and the (`target` × `wakeup`) matrix makes previously-unreachable combinations explicit. One durable message type serves prompts, injected context, and goal rounds, so the surface projection and every "human prompt?" check simplify to a `source` test. The cost: `Agent` became an abstract class, so object-literal test fakes must supply `followup` and cannot spread a class-typed value without re-casting (prototype methods are non-enumerable); the goal fold's channel split moved from event type to `source.round`; and every consumer that filtered `context/message` now filters `user/message` by source. An idle injection appends `user/message` between turns without opening a turn or running the model. +The delivery surface is now one primitive plus three self-documenting presets, and the (`target` × `wakeup`) matrix makes previously-unreachable combinations explicit. One durable message type serves prompts, injected context, and goal rounds, so the surface projection and every "human prompt?" check simplify to a `source` test. The `Agent` contract remains an interface, so alternate implementations and object-literal test fakes implement the same minimal structural surface. The goal fold's channel split moved from event type to `source.round`, and every consumer that filtered `context/message` now filters `user/message` by source. An idle injection appends `user/message` between turns without opening a turn or running the model. -`wakeup` is the "should the model run" signal, so the inbox distinguishes waking queued work from anything available to dequeue: a lone `next-turn`/no-wakeup item stays parked at idle and rides along the next waking send, and `whenIdle`/`cancel` settle quiescence off the waking signal. Every FIFO exit publishes exactly one lifecycle event, while domain-specific durable facts travel in typed message sources rather than a parallel metadata channel. The direct pending-item representation keeps public lifecycle events correlated without maintaining a second steering wrapper or allowing its durable data to diverge. `gen-cordis-api` collects exported classes (public members, body-stripped) so the now-class `Agent` and its transitive shapes still appear in the model-facing API catalog. +`wakeup` is the "should the model run" signal, so the inbox distinguishes waking queued work from anything available to dequeue: a lone `next-turn`/no-wakeup item stays parked at idle and rides along the next waking send, and `whenIdle`/`cancel` settle quiescence off the waking signal. Every FIFO exit publishes exactly one lifecycle event, while domain-specific durable facts travel in typed message sources rather than a parallel metadata channel. The direct pending-item representation keeps public lifecycle events correlated without maintaining a second steering wrapper or allowing its durable data to diverge. ## Related diff --git a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md index 77fa248f50..3569777a28 100644 --- a/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.zh.md @@ -12,33 +12,33 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send` ## 决策 -**一个原语,三个预设别名。** `Agent` 现在是一个抽象类,其唯一的抽象方法 `send(content, { target, wakeup, source })` 覆盖 (`target` × `wakeup`) 矩阵。完整的选项对象为必填,因此路由和 provenance 保持显式。`followup`(`next-turn`/wakeup)、`steer`(`next-step`/wakeup)和 `inject`(`next-step`/no-wakeup)是基类上的具体委托方法,因此具体驱动器只需实现一次 `send`,调用方则使用 `followup(content)` 作为普通用户消息预设。`wakeup` 意为“让模型运行”:为一个 `next-turn` 队列项唤醒处于停泊状态的驱动器,或为一个运行中的 `next-step` 队列项强制继续执行。`next-turn`/no-wakeup(入队但不唤醒)可以表达,只是没有别名,也没有当前调用方。 +**一个原语,三个预设别名。** `Agent` 接口的 `send(input, { target, wakeup })` 覆盖 (`target` × `wakeup`) 矩阵。其 `UserMessageData` 输入持有不可分割的模型可见 `content` 与生产方 `source`;完整的 `SendOptions` 只持有路由策略。`followup`(`next-turn`/wakeup)、`steer`(`next-step`/wakeup)和 `inject`(`next-step`/no-wakeup)都接收这一项输入并固定策略。`wakeup` 意为“让模型运行”:为一个 `next-turn` 队列项唤醒处于停泊状态的驱动器,或为一个运行中的 `next-step` 队列项强制继续执行。`next-turn`/no-wakeup(入队但不唤醒)可以表达,只是没有别名,也没有当前调用方。 -**inject 保留其机制。** `next-step`/no-wakeup 路径正是旧的 `inject`:持久的面向模型上下文会追加到当前日志位置;轮次打开时,它会延迟到执行中的工具批处理之后,空闲时则直接追加在两个轮次之间。它完全绕过 FIFO 队列,并把来源默认设为 `{ kind: 'plugin', plugin: '' }`,绝不是 `{ kind: 'user' }`。 +**inject 保留其机制。** `next-step`/no-wakeup 路径正是旧的 `inject`:持久的面向模型上下文会追加到当前日志位置;轮次打开时,它会延迟到执行中的工具批处理之后,空闲时则直接追加在两个轮次之间。它完全绕过 FIFO 队列,而必填的 `UserMessageData.source` 会保留调用方显式提供的来源信息。 -**context/message 已移除。** 注入的上下文现在是一条 `user/message`,其 `source` 为非 `user` 类别。类型化 source 变体携带所有特定于领域的持久 provenance。对外接口、派生逻辑和 `SurfaceEventType` 都不再包含 `context/message`;需要判断“这是不是一条人类提示词?”的消费方改为读取 `source.kind === 'user'`,而不是事件类型。这让 goal-authority 的人类授权检查与此前一样严格——注入的消息默认使用 plugin 来源,永远无法满足 `source.kind === 'user'`。 +**context/message 已移除。** 注入的上下文现在是一条 `user/message`;上下文生产方显式提供合适的非 `user` 类别 `source`,类型化 source 变体携带所有特定于领域的持久来源信息。对外接口、派生逻辑和 `SurfaceEventType` 都不再包含 `context/message`;需要判断“这是不是一条人类提示词?”的消费方改为读取 `source.kind === 'user'`,而不是事件类型。 **goal 回放靠轮次而非类型来区分。** 一次 goal 状态变更是一条第 0 轮、来源为 goal 的 `user/message`,其 source 携带完整变更;正数轮次则是一条已准入的继续执行提示词。`decodeGoalEvent` 接收一条 `user/message`,并在 goal 状态内容与其类型化 source 不一致时立即报错。 **`send` 返回一个 id。** `send`(以及其别名)为被接受的消息返回一个不透明的 branded `AgentMessageId`;`send` 此前的返回值是 `void`。 -**三个 inbox 事件取代 agent/queued。** `agent/inbox/enqueue`(一个队列项进入某个 FIFO)、`agent/inbox/dequeue`(驱动器认领了一个)和 `agent/inbox/discard`(`cancel()` 丢弃了待处理项)都将各自的 `AgentMessage` 载荷类型限定为仅包含被接受消息所返回的 `id`、内容和来源;调用方因此可以把一个排队项与其生命周期关联起来,而无需依赖驱动器的路由状态。注入从不触及 FIFO,也不发出这些事件中的任何一个。每一次 FIFO 入队都会发布一个 enqueue 事件,包括由 loop 生成的携带继续原因的 steer(`agent/turn-continuation` 返回 `{ action: 'continue', reason }`),因此账目会与其后的 dequeue 或 discard 保持平衡。`dsh-agent` 的不变量配套断言 FIFO 守恒:一个按 agent 计的未结算计数,dequeue 和 discard 永远无法把它压到负数。 +**三个 inbox 事件取代 agent/queued。** `agent/inbox/enqueue`(一个队列项进入某个 FIFO)、`agent/inbox/dequeue`(驱动器认领了一个)和 `agent/inbox/discard`(`cancel()` 丢弃了待处理项)都将各自的 `AgentMessage` 载荷类型限定为仅包含被接受消息所返回的 `id`、内容和来源;调用方因此可以把一个排队项与其生命周期关联起来,而无需依赖驱动器的路由状态。注入从不触及 FIFO,也不发出这些事件中的任何一个。每一次 FIFO 入队都会发布一个 enqueue 事件,包括 `agent/stopping` 监听器提交的 steering,因此账目会与其后的 dequeue 或 discard 保持平衡。`dsh-agent` 的不变量配套断言 FIFO 守恒:一个按 agent 计的未结算计数,dequeue 和 discard 永远无法把它压到负数。 -**一条已接受消息只保留一种表示。** 持久的用户角色输入使用 `UserMessageData { content, source }`;同一形状也以 `AdditionalContext` 命名,公开的 `AgentMessage` 在此基础上增加用于关联的 `id`,循环私有的 `PendingMessage` 再增加 `wakeup`。一条成为 steering 的排队消息会以同一个 `PendingMessage` 对象进入 outbox,而注入和工具产生的上下文则以普通 `UserMessageData` 进入。因此,outbox 直接存储这两种类型的联合,而不再把 steering 与一份重复的内容和来源副本包装在一起。提供方原生的助手消息仍是适配器拥有的输出类型,不参与这套输入层级。 +**一条已接受消息只保留一种表示。** 持久的用户角色输入和附加的模型可见上下文都直接使用 `UserMessageData { content, source }`;公开的 `AgentMessage` 在此基础上增加用于关联的 `id`,循环私有的 `PendingMessage` 再增加 `wakeup`。一条成为 steering 的排队消息会以同一个 `PendingMessage` 对象进入 outbox,而注入和工具产生的上下文则以普通 `UserMessageData` 进入。因此,outbox 直接存储这两种类型的联合,而不再把 steering 与一份重复的内容和来源副本包装在一起。提供方原生的助手消息仍是适配器拥有的输出类型,不参与这套输入层级。 **cancel 新增 keepInbox。** `cancel(cause, { keepInbox? })`;调用方显式选择 cause,且 `keepInbox: true` 会中止活跃轮次,同时保留排队项和 steering 项(不发出 discard 事件,尚未启动的工作也不会被丢弃)。 ## 考虑过的替代方案 -- **为注入内容设立专门的 `MessageSource` 类别 `context`。** 不予采纳,因为 `plugin` 已经表示“不是人类”,因此第四种类别会增加一条平行的轴,让授权检查不得不去学习它。注入的上下文改为默认使用 plugin 来源。 +- **为注入内容设立专门的 `MessageSource` 类别 `context`。** 不予采纳,因为 `plugin` 已经表示“不是人类”,因此第四种类别会增加一条平行的轴,让授权检查不得不去学习它。由插件产生的注入上下文会显式提供其 plugin 来源。 - **在 `UserMessageData` 上设一个类型化的判别字段**(例如 `origin: 'prompt' | 'context'`)来取代事件类型的区分。不予采纳,转而采用 `source`——每个消费方都已经携带它,goal 系统也已经以它为键;第二个判别字段会重复这一事实。 - **在 inbox 事件之外保留 `agent/queued`。** 作为镜像而被否决:`agent/inbox/enqueue` 是同一个入队时刻的信号,只是多带了 `target`/`wakeup` 的事实,而 dequeue/discard 事件补全了单个事件无法描述的 FIFO 生命周期。 ## 后果 -投递接口现在是一个原语加三个自解释的预设,(`target` × `wakeup`) 矩阵把此前无法表达的组合显式化。一种持久消息类型同时服务提示词、注入的上下文和 goal 轮次,因此对外接口的投影和每一处“是否人类提示词?”检查都简化为一次 `source` 判断。代价是:`Agent` 变成了抽象类,因此对象字面量形式的测试替身必须提供 `followup`,且无法在不重新做类型转换的情况下展开一个类类型的值(原型方法不可枚举);goal 折叠的通道区分从事件类型改到了 `source.round`;此前过滤 `context/message` 的每个消费方现在改为按来源过滤 `user/message`。空闲状态下的注入会在两个轮次之间追加 `user/message`,既不打开轮次,也不运行模型。 +投递接口现在是一个原语加三个自解释的预设,(`target` × `wakeup`) 矩阵把此前无法表达的组合显式化。一种持久消息类型同时服务提示词、注入的上下文和 goal 轮次,因此对外接口的投影和每一处“是否人类提示词?”检查都简化为一次 `source` 判断。`Agent` 契约仍是接口,因此其他实现和对象字面量形式的测试替身只需实现同一个最小结构接口。goal 折叠的通道区分从事件类型改到了 `source.round`;此前过滤 `context/message` 的每个消费方现在改为按来源过滤 `user/message`。空闲状态下的注入会在两个轮次之间追加 `user/message`,既不打开轮次,也不运行模型。 -`wakeup` 是“模型是否应当运行”的信号,因此 inbox 会区分能唤醒的排队工作与任何可 dequeue 的项:一个孤立的 `next-turn`/no-wakeup 队列项会停泊在空闲状态,并随下一次唤醒 send 一同带出,而 `whenIdle`/`cancel` 依据唤醒信号来结算静默。每一次 FIFO 退出都恰好发布一个生命周期事件,特定于领域的持久事实则通过类型化消息 source 传递,而非通过平行的元数据通道。直接使用待处理项的表示方式,使公开生命周期事件保持可关联,既无需维护第二个 steering 包装层,也避免其持久数据发生分歧。`gen-cordis-api` 收集导出的类(公开成员,剥除方法体),因此如今已是类的 `Agent` 及其传递涉及的形状仍会出现在面向模型的 API 目录中。 +`wakeup` 是“模型是否应当运行”的信号,因此 inbox 会区分能唤醒的排队工作与任何可 dequeue 的项:一个孤立的 `next-turn`/no-wakeup 队列项会停泊在空闲状态,并随下一次唤醒 send 一同带出,而 `whenIdle`/`cancel` 依据唤醒信号来结算静默。每一次 FIFO 退出都恰好发布一个生命周期事件,特定于领域的持久事实则通过类型化消息 source 传递,而非通过平行的元数据通道。直接使用待处理项的表示方式,使公开生命周期事件保持可关联,既无需维护第二个 steering 包装层,也避免其持久数据发生分歧。 ## 相关 diff --git a/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.i18n.yaml index e6b0fa166a..663ad725d4 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-21-compaction-summary-prefix-cache-reuse.md: 490eb57a5891bf9cd0799c5d49d25d4e9838041f -2026-07-21-compaction-summary-prefix-cache-reuse.zh.md: 02412ff07e87e12c7e7de00b5c69e1282433f735 +2026-07-21-compaction-summary-prefix-cache-reuse.md: d05d25cfa7c3984ce0ce75c38068a91a0e07dfe8 +2026-07-21-compaction-summary-prefix-cache-reuse.zh.md: edf9de6fe5388d75612946bfb05c4383d1856102 diff --git a/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md b/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md index 490eb57a58..d05d25cfa7 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md +++ b/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md @@ -6,7 +6,7 @@ English | [中文](2026-07-21-compaction-summary-prefix-cache-reuse.zh.md) ## Problem -Automatic compaction fires mid-conversation, right after the loop has warmed the provider's KV cache with the last routed request (`system` + `tools` + `messagePrefix` + derived history). The default summarizer then issued a *separate* auxiliary request whose prefix shared nothing with that warm request: a bespoke summarizer `system` prompt followed by the older history flattened to a single rendered transcript string. A provider caches on the request's leading token sequence, so a first token that differs — a different system prompt — invalidates the entire cached prefix. Every compaction therefore paid full prompt-processing cost for the whole replayed history twice: once for the conversation request that tripped pressure, and again for the summarization call, defeating the cache exactly when the conversation is largest. +Automatic compaction fires mid-conversation, right after the loop has warmed the provider's KV cache with the last routed request (`system` + `tools` + derived history). The default summarizer then issued a *separate* auxiliary request whose prefix shared nothing with that warm request: a bespoke summarizer `system` prompt followed by the older history flattened to a single rendered transcript string. A provider caches on the request's leading token sequence, so a first token that differs — a different system prompt — invalidates the entire cached prefix. Every compaction therefore paid full prompt-processing cost for the whole replayed history twice: once for the conversation request that tripped pressure, and again for the summarization call, defeating the cache exactly when the conversation is largest. ## Decision @@ -14,7 +14,7 @@ The summarization directive moves from the **front** of the request (a fresh `sy ### `SummarizationInput` carries the replayed prefix, not a rendered string -`summarize()` (and the internal `summarizeWithLlm`) take a `SummarizationInput` — `{ system?, tools?, messages }` — instead of a flat transcript string. `region.ts` builds it from `session.requestHeader()` (the durable `system`, `tools`, and `messagePrefix`) plus the shadowed region mapped through `session.deriveEventMessage`, which yields byte-identical `Message` objects to what `deriveMessages()` folded into the routed request. `summarizeWithLlm` forwards `system` and `tools` onto `GenerateOptions` and sends `[...input.messages, { role: 'user', content: COMPACTION_INSTRUCTION }]`. `tools` ride along even though the summarizer never calls one: dropping them would shorten the token sequence and break alignment with the cached request. +`summarize()` (and the internal `summarizeWithLlm`) take a `SummarizationInput` — `{ system?, tools?, messages }` — instead of a flat transcript string. `region.ts` builds it from `session.requestHeader()` (the durable `system` and `tools`) plus the shadowed region mapped through `session.deriveEventMessage`, which yields byte-identical `Message` objects to what `deriveMessages()` folded into the routed request. `summarizeWithLlm` forwards `system` and `tools` onto `GenerateOptions` and sends `[...input.messages, { role: 'user', content: COMPACTION_INSTRUCTION }]`. `tools` ride along even though the summarizer never calls one: dropping them would shorten the token sequence and break alignment with the cached request. ### The instruction is a trailing user message @@ -27,7 +27,7 @@ Auto-compaction always anchors at the surface head, so the shadowed region is th ## Alternatives considered - **Keep the summarizer system prompt but reuse the rest** — rejected: the system slot is the very first token region a provider caches on, so a distinct summarizer system prompt invalidates the whole prefix regardless of what follows. Only moving the directive off the front recovers the cache. -- **Send only the shadowed region without the `system`/`tools`/`messagePrefix` head** — rejected: a shorter or differently-headed sequence still diverges from the cached request at the first token, so it caches no better while losing the framing the summary needs. +- **Send only the shadowed region without the `system`/`tools` head** — rejected: a differently-headed sequence still diverges from the cached request at the first token, so it caches no better while losing the framing the summary needs. - **Omit `tools` from the summarization request** (the model never calls one) — rejected: tool schemas are part of the cached token sequence; omitting them misaligns every following token and defeats reuse. - **A dedicated `assistant/chunk`-emitting summarization sub-session for snapshot replay** — out of scope here; the replay gap predates this change and is tracked in the [compaction-seam note](../feature/2026-06-18-compaction-capability-seam.md). diff --git a/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.zh.md b/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.zh.md index 02412ff07e..edf9de6fe5 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.zh.md @@ -6,7 +6,7 @@ Status: implemented ## Problem -自动压缩(compaction)在对话中途触发,恰好在循环用最后一个已路由请求(`system` + `tools` + `messagePrefix` + 派生历史)预热了提供方的 KV 缓存之后。随后默认摘要器发出一个*独立的*辅助请求,其前缀与那个已预热请求没有任何共享部分:一个专门的摘要器 `system` 提示词,后接被拍平成单个渲染后 transcript(文本记录)字符串的较早历史。提供方基于请求起始的 token 序列做缓存,因此第一个 token 只要不同(即一个不同的系统提示词),整个已缓存前缀就会失效。于是每次压缩都要为整段回放的历史付出两次完整的提示词处理成本:一次用于触发压力的对话请求,另一次用于摘要调用,恰好在对话最大时让缓存失去作用。 +自动压缩(compaction)在对话中途触发,恰好在循环用最后一个已路由请求(`system` + `tools` + 派生历史)预热了提供方的 KV 缓存之后。随后默认摘要器发出一个*独立的*辅助请求,其前缀与那个已预热请求没有任何共享部分:一个专门的摘要器 `system` 提示词,后接被拍平成单个渲染后 transcript(文本记录)字符串的较早历史。提供方基于请求起始的 token 序列做缓存,因此第一个 token 只要不同(即一个不同的系统提示词),整个已缓存前缀就会失效。于是每次压缩都要为整段回放的历史付出两次完整的提示词处理成本:一次用于触发压力的对话请求,另一次用于摘要调用,恰好在对话最大时让缓存失去作用。 ## Decision @@ -14,7 +14,7 @@ Status: implemented ### `SummarizationInput` 携带回放的前缀,而非渲染后的字符串 -`summarize()`(以及内部的 `summarizeWithLlm`)接受一个 `SummarizationInput`(`{ system?, tools?, messages }`)而不是一个扁平的 transcript 字符串。`region.ts` 用 `session.requestHeader()`(持久的 `system`、`tools` 和 `messagePrefix`)加上经 `session.deriveEventMessage` 映射的被遮蔽区域来构建它,后者产出与 `deriveMessages()` 折叠进已路由请求的内容字节级一致的 `Message` 对象。`summarizeWithLlm` 把 `system` 和 `tools` 转发到 `GenerateOptions`,并发送 `[...input.messages, { role: 'user', content: COMPACTION_INSTRUCTION }]`。`tools` 会一同带上,即便摘要器从不调用任何工具:丢弃它们会缩短 token 序列,破坏与已缓存请求的对齐。 +`summarize()`(以及内部的 `summarizeWithLlm`)接受一个 `SummarizationInput`(`{ system?, tools?, messages }`)而不是一个扁平的 transcript 字符串。`region.ts` 用 `session.requestHeader()`(持久的 `system` 和 `tools`)加上经 `session.deriveEventMessage` 映射的被遮蔽区域来构建它,后者产出与 `deriveMessages()` 折叠进已路由请求的内容字节级一致的 `Message` 对象。`summarizeWithLlm` 把 `system` 和 `tools` 转发到 `GenerateOptions`,并发送 `[...input.messages, { role: 'user', content: COMPACTION_INSTRUCTION }]`。`tools` 会一同带上,即便摘要器从不调用任何工具:丢弃它们会缩短 token 序列,破坏与已缓存请求的对齐。 ### 指令是一条尾部 user 消息 @@ -27,7 +27,7 @@ Status: implemented ## Alternatives considered - **保留摘要器系统提示词但复用其余部分**——否决:system 槽位正是提供方最先做缓存的 token 区域,因此一个不同的摘要器系统提示词无论后面跟着什么都会使整个前缀失效。只有把指令移离前端才能恢复缓存。 -- **只发送被遮蔽区域而不带 `system`/`tools`/`messagePrefix` 头部**——否决:更短或头部不同的序列在第一个 token 处仍然与已缓存请求分叉,因此缓存效果并不更好,反而丢失了摘要所需的框架。 +- **只发送被遮蔽区域而不带 `system`/`tools` 头部**——否决:头部不同的序列在第一个 token 处仍然与已缓存请求分叉,因此缓存效果并不更好,反而丢失了摘要所需的框架。 - **从摘要请求中省略 `tools`**(模型从不调用任何工具)——否决:工具 schema 是已缓存 token 序列的一部分;省略它们会让后续每个 token 失去对齐,破坏复用。 - **为快照回放专门建立一个发出 `assistant/chunk` 的摘要子会话**——此处超出范围;该回放缺口早于本次改动,记录在 [compaction-seam Agent Note](../feature/2026-06-18-compaction-capability-seam.md) 中。 diff --git a/.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md b/.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md index 7c47fc78e9..8a4495db42 100644 --- a/.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md +++ b/.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md @@ -50,7 +50,7 @@ Editors can create, load, prompt, cancel, render, ask, and reconfigure multiple The bridge deliberately does not implement session list/delete/resume/close capabilities, MCP passthrough, additional directories, image/audio/embedded-resource prompts, plans, slash commands, usage updates, editor filesystem delegation, or the ACP terminal execution sub-protocol. Runtime model selection was added later through standard session config options by the [LLM catalog and ACP selection Agent Note](../architecture/2026-07-15-llm-model-catalog-and-acp-selection.md). -An idle config selection is truthful in the live response but not durable until the next `agent/prompt-submit` anchors it inside the open turn. Crashing before that boundary loses the pending selection; this is the cost of keeping session events turn-enclosed and replay-safe. +An idle config selection is truthful in the live response but not durable until the next `agent/step` anchors it inside the open turn. Crashing before that boundary loses the pending selection; this is the cost of keeping session events turn-enclosed and replay-safe. ## Verification diff --git a/.agents/notes/implemented/feature/2026-06-24-workspace-context.md b/.agents/notes/implemented/feature/2026-06-24-workspace-context.md index 394c0de708..0901af7cd6 100644 --- a/.agents/notes/implemented/feature/2026-06-24-workspace-context.md +++ b/.agents/notes/implemented/feature/2026-06-24-workspace-context.md @@ -8,13 +8,13 @@ Repository guidance such as `AGENTS.md` belongs in a coding session's effective Neighboring products establish useful conventions but differ in details. Codex treats `AGENTS.md` as native, Claude Code uses `CLAUDE.md` and familiar system-reminder-style user context, and opencode supports both names with one winner per directory plus lazy nested discovery. The harness needs cross-tool compatibility without loading duplicate or contradictory files from the same scope. -The lifecycle has two distinct classes of content. The initial applicable chain is stable enough to live in the request prefix and benefit from provider prefix caching. Nested files, edits, candidate switches, and removals happen after the session starts and belong in durable append-only history rather than the frozen prefix. +The lifecycle has two distinct classes of content. The initial applicable chain is injected once before the first request. Nested files, edits, candidate switches, and removals happen later and join the same durable append-only history. ## Decision -The implementation lives in `packages/context/workspace-context` as `@deepseek-ai/dsh-workspace-context`. It is a request-context extension, not a core service or a filesystem backend. The shared demo spine and Host Runtime mount it from an explicit `{ maxBytes } | false` deployment choice; `dsh web` enables a 65,536-byte budget while the Host Runtime's headless consumer disables it. The plugin consumes `agent/session-prefix`, `tools/post-execute`, and the optional `ctx.fs` capability. +The implementation lives in `packages/context/workspace-context` as `@deepseek-ai/dsh-workspace-context`. It is a request-context extension, not a core service or a filesystem backend. The shared demo spine and Host Runtime mount it from an explicit `{ maxBytes } | false` deployment choice; `dsh web` enables a 65,536-byte budget while the Host Runtime's headless consumer disables it. The plugin consumes `agent/step`, `tools/post-execute`, and the optional `ctx.fs` capability. -The plugin does not statically inject `fs`. Providerless product trees therefore boot normally and the plugin no-ops until a filesystem provider exists. All production reads go through that provider. Candidate probes resolve each path and stat the result, so a final-component symlink is followed to its target: a link to a regular file loads, while a missing path or a non-file target is a confirmed absence. Following repository-owned links across the trust boundary is a deliberate reversal of the original no-follow probe; the [instruction-symlink follow note](2026-07-21-follow-instruction-symlinks.md) owns that decision and its residual risk. The session-prefix signal and dynamic tool execution signal propagate through resolution, metadata probes, and streaming reads, so cancellation does not wait for an unrelated filesystem scan. A resolve or stat exception is classified as unavailable: it skips only that candidate and is never interpreted as the deletion of an already-loaded scope. +The plugin does not statically inject `fs`. Providerless product trees therefore boot normally and the plugin no-ops until a filesystem provider exists. All production reads go through that provider. Candidate probes resolve each path and stat the result, so a final-component symlink is followed to its target: a link to a regular file loads, while a missing path or a non-file target is a confirmed absence. Following repository-owned links across the trust boundary is a deliberate reversal of the original no-follow probe; the [instruction-symlink follow note](2026-07-21-follow-instruction-symlinks.md) owns that decision and its residual risk. The step signal and dynamic tool execution signal propagate through resolution, metadata probes, and streaming reads, so cancellation does not wait for an unrelated filesystem scan. A resolve or stat exception is classified as unavailable: it skips only that candidate and is never interpreted as the deletion of an already-loaded scope. ### File Names And Precedence @@ -24,13 +24,13 @@ Candidate entries are same-directory file names. Empty entries, `.`/`..`, and en The user-global file is fixed at `$DSH_HOME/AGENTS.md`, is not affected by either candidate list, and has no local overlay. `$DSH_HOME` defaults to `~/.dsh`, matching the harness-level home role of `~/.codex` or `~/.claude` rather than introducing a plugin-specific home. Tilde expansion and the default live in `dsh-paths` so future harness features share the same convention. -### Baseline Prefix +### Baseline Injection -On the first request of an agent-loop instance, the plugin contributes one user-role message through `agent/session-prefix`. It loads the user-global file first, then finds the project root by walking upward from `agent.session.header.cwd` to a configured root marker (default `.git`), then loads one candidate from each directory from the root to the cwd. A `.git` file and a `.git` directory are both valid markers, covering linked worktrees and submodules. Without a marker, the cwd itself is the root. +At the first `agent/step` of an agent-loop instance, the plugin injects one sourced user-role message before the request is derived. It loads the user-global file first, then finds the project root by walking upward from `agent.session.header.cwd` to a configured root marker (default `.git`), then loads one candidate from each directory from the root to the cwd. A `.git` file and a `.git` directory are both valid markers, covering linked worktrees and submodules. Without a marker, the cwd itself is the root. -The plugin prepends its contribution before `await next()` returns, so session-prefix contributions appear in plugin registration order. In the product spine workspace instructions are registered before a skills catalog and therefore appear first. The loop deep-freezes the composed prefix, logs it in `EpochHeader.messagePrefix`, and reuses it verbatim for that instance. It is request state, not `Session.deriveMessages()` history. +The injection becomes a durable `user/message` with plugin provenance. In the product spine workspace instructions are registered before the skills catalog, so their `agent/step` listener injects first. The loop drains both messages before deriving the first request. -A resumed agent creates a new loop instance and recomposes the baseline from current files, with the new prefix anchored by the resume request header. This permits current baseline content on resume without mutating a prefix already used by an earlier instance. +A resumed agent creates a new loop instance and injects a baseline composed from current files before its first request. This permits current baseline content on resume without mutating an earlier history event. The baseline is a user-role `` with `Instructions from: ` sections and explicit authority and precedence language. This familiar model-facing frame avoids a harness-specific XML vocabulary. Project paths are root-relative and the user-global path is `~/.dsh/AGENTS.md` for the default home or `$DSH_HOME/AGENTS.md` for a configured home. A literal `` inside file content is escaped. The package README owns the exact current [prompt shape](../../../../packages/context/workspace-context/README.md#prompt-shape). @@ -40,7 +40,7 @@ After a successful first-party `read`, `write`, or `edit` call, the `tools/post- A content edit appends `Updated instructions from: `, states that the new content replaces the previous content, and includes the complete current file. If precedence changes from one candidate to another, the message also names the previous path and says it no longer applies. If no candidate remains, the plugin appends `Instructions removed: ` and states that the previously loaded instructions no longer apply. -Dynamic messages carry their complete system-reminder framing in `content`, and every `context/message` reaches the model verbatim as a user-role message (there is no core wrapper to opt out of). `context/message.meta` carries opaque JSON state that is persisted but never rendered to the model. +Dynamic messages carry their complete system-reminder framing in `content`, and every sourced `user/message` reaches the model verbatim (there is no core wrapper to opt out of). The typed `workspace-instructions` source carries persisted state that is never rendered to the model. Shell commands are not discovery triggers. Local bash calls start fresh shells, and inferring reached paths from arbitrary command strings would require shell semantics the prompt plugin does not own. @@ -48,13 +48,13 @@ Shell commands are not discovery triggers. Local bash calls start fresh shells, Every dynamic workspace context event stores versioned metadata with `{ action, scope, path, digest? }`, where `digest` is SHA-1 over the loaded content. The model-facing prompt has no HTML comments, hidden markers, or headings that are parsed back into state. -At reconciliation time the plugin scans plugin-owned `context/message` events and derives the latest state for each visible scope. A short per-session pending map begins only after the immutable top-level `tools/result` proves an `additionalContexts` entry survived every post-execute listener, then covers the interval before the loop appends that context to the log. Each entry records the open `{ turn, step }`: an equal durable `context/message` at or after its sequence boundary confirms and removes it, while a matching `step/end` arriving first means the loop discarded its context buffer, so the plugin removes both the pending entry and its version-cache fast path. A nested Code Mode result stages its changes under the parent's opaque execution token so repeated sub-dispatches in one run do not duplicate them; the parent result rolls that provisional state back and commits only contexts retained by outer policy. +At reconciliation time the plugin scans workspace-sourced `user/message` events and derives the latest state for each visible scope. A short per-session pending map begins only after the immutable top-level `tools/result` proves an `additionalContexts` entry survived every post-execute listener, then covers the interval before the loop appends that context to the log. Each entry records the open `{ turn, step }`: an equal durable `user/message` at or after its sequence boundary confirms and removes it, while a matching `step/end` arriving first means the loop discarded its context buffer, so the plugin removes both the pending entry and its version-cache fast path. A nested Code Mode result stages its changes under the parent's opaque execution token so repeated sub-dispatches in one run do not duplicate them; the parent result rolls that provisional state back and commits only contexts retained by outer policy. An unchanged path and digest is suppressed. A logged removal is a tombstone, so a reappearing candidate becomes a new `set`. Resume works from persisted metadata. If compaction removes an instruction event from the visible surface, that state no longer suppresses a later load, matching the fact that the model can no longer see it. Only changes actually included under the byte budget enter metadata or pending state, so an omitted file remains eligible on a later touch. -The frozen baseline keeps an in-memory path/digest map for comparison. A later successful filesystem touch appends baseline edits or removals as dynamic messages; it never rewrites the prefix. During resumed prefix composition the plugin also reconciles visible dynamic scopes, so nested changes made while the agent was offline can append an update before the first resumed request. +The initial baseline keeps an in-memory path/digest map for comparison. A later successful filesystem touch appends baseline edits or removals as dynamic messages; it never rewrites the original event. During resumed baseline preparation the plugin also reconciles visible dynamic scopes, so nested changes made while the agent was offline can append an update before the first resumed request. -There is intentionally no watcher. Detection occurs at the next successful structured filesystem touch or resumed prefix composition. A provider failure produces no removal; absence is only accepted when all configured candidates in that scope were probed successfully. +There is intentionally no watcher. Detection occurs at the next successful structured filesystem touch or resumed baseline preparation. A provider failure produces no removal; absence is only accepted when all configured candidates in that scope were probed successfully. ### Byte Budget And Bounded Reads @@ -66,7 +66,7 @@ There is intentionally no watcher. Detection occurs at the next successful struc **Use a global `ctx.systemPrompt.section()`.** Rejected because one Cordis context can host sessions with different cwd values, while repository-owned text is lower-authority context rather than top-authority provider system content. -**Inject the baseline on every `agent/pre-step`.** Rejected because repeated history injection wastes tokens, complicates duplicate state, and prevents a structurally stable provider prefix. Prefix composition gives a frozen, logged, per-instance baseline while dynamic append-only messages handle changes. +**Inject the baseline on every `agent/step`.** Rejected because repeated history injection wastes tokens and complicates duplicate state. A per-session guard gives one durable baseline event while dynamic append-only messages handle changes. **Load both `AGENTS.md` and `CLAUDE.md` in one directory.** Rejected because repositories in transition commonly duplicate guidance across both files. Ordered candidates make precedence explicit and configurable. @@ -76,7 +76,7 @@ There is intentionally no watcher. Detection occurs at the next successful struc ## Consequences -Workspace guidance is isolated per session and shared by the demo front doors, Web Host, and every tool presentation mode. Initial instructions benefit from stable prefix caching, while nested and changed content remains durable and replayable. The generic session/agent context contract carries JSON metadata propagated through prompt-submit and post-tool `additionalContexts` arrays without flattening entries. +Workspace guidance is isolated per session and shared by the demo front doors, Web Host, and every tool presentation mode. Initial, nested, and changed instructions are durable and replayable. The generic session/agent context contract carries typed source data through injected messages and post-tool `additionalContexts` arrays without flattening entries. Repository text remains untrusted input. Lower-authority user-role framing, explicit precedence language, and delimiter escaping reduce risk but do not eliminate prompt injection. Following a candidate symlink to its target widens that surface to off-tree content, so the permission and sandbox layers that confine `ctx.fs` to trusted roots are the boundary that treats workspace files as data rather than authority (the [instruction-symlink follow note](2026-07-21-follow-instruction-symlinks.md) owns the residual risk). diff --git a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md index 0c3c1e13ef..3fa2eee371 100644 --- a/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md +++ b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md @@ -4,7 +4,7 @@ Status: implemented ## Problem -The harness's extension surface is its typed interception seams ([the interception-seams Agent Note](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/prompt-submit`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation`, `subagent/start`, `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)). +The harness's extension surface is its typed interception seams ([the interception-seams Agent Note](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/prompt-submit`, `tools/pre-execute`, `tools/post-execute`, `agent/stopping`, `subagent/start`, or `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)). The framing that shapes the whole design: **a bridge is a compatibility adapter, not a power tool.** Anything a bridge does (block a tool, inject context, force continuation, observe a subagent) a native cordis plugin does more powerfully — typed returns, full `ctx`, no serialization boundary. The bridge's reason to exist is to run the explicitly supported subset of external CC/Codex command hooks. That keeps each bridge thin: parse the config, pick a matcher mode, build the per-event payload, call `runHook` + `mergeHookOutputs` from the shared lib, and map the neutral outcome onto a seam Decision. The package READMEs own the exact current unsupported-event and partial-field inventory against the official protocols. @@ -25,7 +25,7 @@ Each bridge maps the neutral `MergedHookOutcome` from the shared lib onto the se | `agent/prompt-submit` | `deny`→`block`; context-only→delegate+fold | `block`→`block`; context-only→delegate+fold | | `tools/pre-execute` | `deny`→`deny`; `ask`→`ask` | `block`→`deny` (no allow/ask) | | `tools/post-execute` | `deny`→`block`+feedback; context-only→delegate+fold | same | -| `agent/turn-continuation` | blocking Stop → `continue` (reason = next-step steering) | same | +| `agent/stopping` | blocking Stop → next-step steering | same | | `subagent/start` (emit) | additionalContext → inject into a live in-process child; a remote child has no local injection target | unsupported by this bridge | | `subagent/end` (emit) | observe-only | unsupported by this bridge | @@ -33,7 +33,9 @@ The CC bridge's `ask` result is a real permission path, not a terminal bridge de ### Context source is always the plugin (the mislabel guard) -`agent.inject()` defaults a missing `MessageSource` to `{ kind: 'user' }`, so every bridge `inject()` and `HookContext` passes `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`. Unit coverage pins the resulting `context/message.source` as the plugin rather than the user. +Every bridge `inject()` and additional-context input explicitly passes `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`. Unit coverage pins the resulting `user/message.source` as the plugin rather than the user. + +`UserPromptSubmit` runs during admission, before any turn opens. It therefore writes no turn-scoped `hook/invoked` / `hook/result` pair: a block leaves no transcript, while allowed additional context is durably represented by its sourced `user/message`. The Codex payload still receives the candidate next `turn_id`; rejection does not consume that number. ### Adding context is not a veto — delegate, then prepend @@ -55,13 +57,13 @@ Hooks run in the agent's session workspace, so relative paths target the user's - **Tool-input rewrite.** A CC/Codex `updatedInput` is logged + warned, not honored — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite Agent Note](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)), because the pre-execution args are read by `tool/call` audit + `assistant/message` history + ACP/tool-bash presentation, so an honest rewrite is a design unit, not a field. - **Stop loop-guard** (`TODO(stop-loop-guard)`). Claude Code supplies `stop_hook_active` and overrides a hook after eight consecutive blocks; Codex supplies `stop_hook_active` but documents no equivalent cap. Both bridges always report `false`, so a Stop hook that unconditionally blocks force-continues every step — a hook author must self-limit until state tracking lands. -- **Hook `continue:false` (hard halt).** A hook can ask to halt the whole run (CC/Codex `continue:false`); the shared merge folds it into `MergedHookOutcome.stop`/`stopReason`, but no bridge acts on it (`TODO(hook-continue-false)`) — the interception seams have no "hard-halt the agent" primitive yet (a Decision blocks/steers a single point, not the run). Deferred with the loop-guard work; the halt request is recorded in the `hook/result` log, and the hook keeps its per-point effect (decision/context) meanwhile. +- **Hook `continue:false` (hard halt).** A hook can ask to halt the whole run (CC/Codex `continue:false`); the shared merge folds it into `MergedHookOutcome.stop`/`stopReason`, but no bridge acts on it (`TODO(hook-continue-false)`) — the interception seams have no "hard-halt the agent" primitive yet (a Decision blocks/steers a single point, not the run). Deferred with the loop-guard work; mid-turn requests record the halt in `hook/result`, and the hook keeps its per-point effect (decision/context) meanwhile. - **Config discovery.** The path is explicit in `cordis.yml` and process-level (see above); the full multi-layer CC/Codex precedence walk, per-session project-local discovery, and the trust/hash model are not reimplemented (`TODO(per-session-hook-config)`). - **Session-start / subagent-start context is best-effort (`TODO(session-start-gating)`).** Both hooks run detached from startup, so their context is injected when ready but may miss the first request or a short-lived child. Guaranteeing first-request delivery requires an awaited startup seam. ## Alternatives considered -**Concurrent per-point hook execution.** The reference engines run a point's matched hooks concurrently and fold the results. These bridges run them **serially** (`await` per hook inside the match loop) and fold with the same most-restrictive merge. Serial is deliberate: it keeps each hook's `hook/invoked`/`hook/result` pair adjacent and in a deterministic order in the session log, and the fold is order-independent for the decision (`deny > ask > allow`) so the outcome matches. The cost is latency (hook *N* waits for hook *N−1*) and that per-hook timeouts are not overlapped — acceptable for the hook counts real configs use; revisit if a config ever fans out enough for the wall-clock to matter. +**Concurrent per-point hook execution.** The reference engines run a point's matched hooks concurrently and fold the results. These bridges run them **serially** (`await` per hook inside the match loop) and fold with the same most-restrictive merge. Serial is deliberate: for turn-scoped points it keeps each hook's `hook/invoked`/`hook/result` pair adjacent and in deterministic order, and the fold is order-independent for the decision (`deny > ask > allow`) so the outcome matches. The cost is latency (hook *N* waits for hook *N−1*) and that per-hook timeouts are not overlapped — acceptable for the hook counts real configs use; revisit if a config ever fans out enough for the wall-clock to matter. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-seams.md b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md index 05d530137c..d2a5249797 100644 --- a/.agents/notes/implemented/feature/2026-06-30-interception-seams.md +++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md @@ -14,9 +14,9 @@ The canonical surface separates transformable policy, around-dispatch control, a **Agent events** (`dsh-agent`): - `agent/session-start(agent, source)` — emit, once before turn 1, carrying a `SessionStartSource` (`startup` for a fresh/forked create, `resume` for a reloaded persisted session; `clear`/`compact` reserved). A pure notification — it CANNOT block startup (a deliberate gap: a bridge logs/injects, it does not gate startup). A listener seeds context via `agent.inject()`. -- `agent/prompt-submit(agent, content, source, signal, next) → PromptDecision` — waterfall, fired for the turn's single claimed queued message before the `user/message` append. The explicit turn signal is placed before the final `next`; `allow` optionally rewrites the prompt `content` or attaches separately sourced `additionalContexts[]`, while `block` appends a durable `prompt/blocked` and rejects that zero-step turn. +- `agent/prompt-submit(agent, content, source, signal, next) → PromptDecision` — waterfall, fired for one claimed queued message before the loop opens a turn or appends `user/message`. The explicit admission signal is placed before the final `next`; `allow` optionally rewrites the prompt `content` or attaches separately sourced `additionalContexts[]`, while `block` discards the candidate without creating session history. -**`agent/turn-continuation`** receives and returns a `ContinuationDecision`. A `{action:'continue', reason?}` may carry model-facing content and source recorded as next-step steering in the same turn — the typed twin of the `/goal` step-end-steer pattern. Its narrower type does not carry attached contexts. +**`agent/stopping`** is an awaited notification at the natural stop boundary. A listener that needs another step calls `agent.steer()` with explicitly sourced model-facing content; the loop then re-reads the outbox and either continues or closes the turn. ### The tool pipeline gives each phase one kind of authority @@ -31,15 +31,13 @@ Every call follows `tools/pre-execute` → guards → `tools/execute` → dispat Core dispatch and the tool body sit inside normalization boundaries, so tool, listener, invalid canonical value, renderer/projector, non-JSON presentation, and identity-shape failures resolve as JSON-safe `isError` results rather than escaping the turn. A post-execute listener can therefore inspect a thrown tool; definition-owned final content invariants also cover outer pipeline and candidate-materialization failures; and a final observer sees the execution-local canonical value beside exactly the presentation fields the session log can persist. The [canonical tool-output contract](../architecture/2026-07-20-canonical-tool-output-contract.md) owns the value/projection and durability rules. -**`TurnEndReason.rejected`** (`dsh-session`): a zero-step turn whose claimed prompt was blocked by `prompt-submit`. - ### Three load-bearing loop decisions -1. **Open the turn before prompt policy.** A blocked prompt becomes a zero-step `rejected` turn, preserving enclosure and giving ACP a durable terminal event. The veto records `prompt/blocked` with the original prompt and reason, while every allowed `additionalContexts` entry is injected into the open turn. Each claimed ordinary-send item is the sole message in its turn under the [one-send-one-turn simplification](../simplification/2026-07-17-one-send-one-turn.md); a pre-start drop creates no turn. +1. **Run prompt policy before opening the turn.** A blocked prompt creates no turn or durable event. On allow, the loop stages the rewritten prompt followed by every returned `additionalContexts` entry, opens the turn, and drains that outbox before the first step. Each claimed ordinary-send item is the sole direct prompt in its turn under the [one-send-one-turn simplification](../simplification/2026-07-17-one-send-one-turn.md). -2. **Post-tool `additionalContexts` and asynchronous injections enter the active-batch FIFO and append when that batch settles.** `content`/`feedback` shape the result `execute()` returns, but each context is a separate `context/message`, and a single step or composite tool can produce many. Appending context immediately would interleave `result(c1) → context → result(c2)` or place nested context before its outer result, breaking tool-call/result adjacency. `ToolRunContext.deferContext()` therefore collects nested-dispatch context through failures, `execute()` surfaces the ordered array on `ToolExecutionResult`, and the loop accepts it into the same FIFO as `agent.inject()` calls made during execution. The FIFO appends after every recorded result when the batch settles, including before an interrupted turn closes. An accepted outer call preserves deferred contexts before decision contexts; an outer block discards deferred contexts and exposes only contexts explicitly supplied by the blocking decision. +2. **Post-tool `additionalContexts` and asynchronous injections enter the active-batch FIFO and append when that batch settles.** `content`/`feedback` shape the result `execute()` returns, but each context is a separate sourced `user/message`, and a single step or composite tool can produce many. Appending context immediately would interleave `result(c1) → context → result(c2)` or place nested context before its outer result, breaking tool-call/result adjacency. `ToolRunContext.deferContext()` therefore collects nested-dispatch context through failures, `execute()` surfaces the ordered array on `ToolExecutionResult`, and the loop accepts it into the same FIFO as `agent.inject()` calls made during execution. The FIFO appends after every recorded result when the batch settles, including before an interrupted turn closes. An accepted outer call preserves deferred contexts before decision contexts; an outer block discards deferred contexts and exposes only contexts explicitly supplied by the blocking decision. -3. **A forced `continue` `reason` is enqueued through the steering channel**, so the next step's top-of-loop drain records it as steering for the continued turn — next-*step* steering within the SAME turn, not a next-*turn* prompt (matching the existing `hasSteering` force-continue override). +3. **A stopping listener requests continuation through the steering channel**, so the next step's top-of-loop drain records it as steering for the continued turn — next-*step* steering within the SAME turn, not a next-*turn* prompt. ### Pre-tool input rewrite is a separate consistency decision @@ -47,7 +45,7 @@ Core dispatch and the tool body sit inside normalization boundaries, so tool, li ### Boundaries -The seam package does **not** declare `hook/*` session events (the durable hook-invocation log); those belong to `dsh-hook-protocol`, because a native plugin uses typed decisions without an external hook log. The native-plugin integration test (`packages/core/agent-loop/tests/interception.spec.ts`) composes the seams through the real loop with no `hook/*` protocol. Compaction (`PreCompact`/`PostCompact`), Notification, and Codex `PermissionRequest` remain outside this decision. The [approval seam](2026-07-06-approval-seam.md) resolves `ask` decisions through `ctx.approval`, while terminal monotonic stopping is owned separately by `agent/turn-stop`. +The seam package does **not** declare `hook/*` session events (the durable hook-invocation log); those belong to `dsh-hook-protocol`, because a native plugin uses typed decisions without an external hook log. The native-plugin integration test (`packages/core/agent-loop/tests/interception.spec.ts`) composes the seams through the real loop with no `hook/*` protocol. Compaction (`PreCompact`/`PostCompact`), Notification, and Codex `PermissionRequest` remain outside this decision. The [approval seam](2026-07-06-approval-seam.md) resolves `ask` decisions through `ctx.approval`; terminal monotonic stopping is expressed by tool-result data, while `agent/stopping` is the last chance to steer another step. ## Alternatives considered @@ -56,4 +54,4 @@ The seam package does **not** declare `hook/*` session events (the durable hook- ## Consequences -The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, prompt-submit, post-tool context buffering, and continuation; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/core-data-structures/core.md#interception-decisions), and [tool structures](../../../../docs/core-data-structures/tools.md). The ACP bridge maps `rejected` turns to its `cancelled` codec value, while hook-driven snapshots verify the observable bridge behavior end to end. +The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, pre-turn prompt admission, post-tool context buffering, and stopping; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/core-data-structures/core.md#interception-decisions), and [tool structures](../../../../docs/core-data-structures/tools.md). The ACP bridge settles an admission rejection as `cancelled` after the agent becomes idle with no owned turn, while hook-driven snapshots verify the observable bridge behavior end to end. diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.md index 507142fac9..0a6fb1eb7c 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.md +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.md @@ -163,7 +163,7 @@ What shipped pins — the tiers in Testing hold each: - A resumed session's overrides apply and are reported to the editor with no special-casing; a default changed while the process was down is narrated before the session's first new request, attributed to the operator. - Two concurrent sessions never see each other's state, notices, or config options. - Two concurrent project sessions in one Cordis context resolve independent workspace roots; bash and fs writes succeed inside the calling session's cwd and fail against its neighbor's cwd. -- `agent-loop` is untouched — everything rides `systemPrompt.section`, `SessionEventMap` merging, `agent.inject()`, `agent/pre-step`, `agent/prompt-submit`, and the ACP handler surface. +- `agent-loop` is untouched — everything rides `systemPrompt.section`, `SessionEventMap` merging, `agent.inject()`, `agent/step`, `agent/prompt-submit`, and the ACP handler surface. Costs and accepted limits: @@ -190,7 +190,7 @@ Costs and accepted limits: - **Does the sandbox restrict network or process visibility?** No — `SandboxMode` claims FILE effects only; the bwrap profile deliberately does not unshare pid, and no backend claims network. Whether network restriction becomes its own knob is left open in § The seam. - **Which tools actually run confined?** OS subprocesses through `ctx.bash` — the bash tools, and hook commands transitively — plus the filesystem tools (`read`/`write`/`edit`) through the sandboxed `ctx.fs` provider (the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)): bash confines via the OS runner, fs via an in-process path fence, both keying off the same `ctx.sandboxPolicy` mode. web/todo stay in-process and unfenced (web's only effect is network, outside the file-effect mode vocabulary). - **Does a granted escalation persist?** No. The grant is consumed by the exact foreground or background call that asked; every neighboring call keeps its own effective mode. A later background denial surfaces through `task_output` and may ground a new exact-command retry. -- **When does an editor's mode switch take effect?** Mid-turn: appended immediately, honored by the very next call's stamp. Idle: held on the bridge's session record, anchored at the next `agent/prompt-submit` inside its open turn, with N flips coalescing to at most one event (none if net-zero); a crash before anchoring reverts it and `session/load` reports the truth. The model is not told — its next command simply behaves under the new mode. +- **When does an editor's mode switch take effect?** Mid-turn: appended immediately, honored by the very next call's stamp. Idle: held on the bridge's session record, anchored at the next `agent/step` inside its open turn, with N flips coalescing to at most one event (none if net-zero); a crash before anchoring reverts it and `session/load` reports the truth. The model is not told — its next command simply behaves under the new mode. - **What survives a restart — and what if the operator changed the config default while the process was down?** Overrides replay from the session log (`effective = fold ?? config`), so a resumed session keeps its modes with zero catch-up machinery; a default that drifted offline changes behavior the same way a switch does (the approval policy, being stated, is additionally narrated with operator/config attribution). - **What does `enforcement: 'partial'` on a result mean?** The selected backend enforces the subset its kernel ABI governs — e.g. Landlock before ABI v3 does not govern path truncate — and says so structurally instead of refusing the host; the probe's report line distinguishes the cases. The bwrap and Seatbelt profiles govern every promised file effect by construction, so they always report `full`. diff --git a/.agents/notes/implemented/feature/2026-07-07-session-prefix.md b/.agents/notes/implemented/feature/2026-07-07-session-prefix.md index a8783d0b8f..e24fd58c88 100644 --- a/.agents/notes/implemented/feature/2026-07-07-session-prefix.md +++ b/.agents/notes/implemented/feature/2026-07-07-session-prefix.md @@ -2,6 +2,8 @@ Status: implemented +The request-only prefix seam described below was later removed by the [unified sourced-message decision](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md). Current producers inject durable sourced `user/message` context at `agent/step`; this record preserves the earlier design and its trade-offs. + ## Problem A plugin often owns a session-stable opener the model must always see — a skills catalog, an AGENTS.md digest, a workspace baseline. Before this seam the harness offered two homes, and both are wrong for that content. The system prompt is one rendered string: message-shaped content (a user-role `` envelope, a multi-message primer) does not fit it, and providers weight conversation messages differently from system text. Durable history (`agent.inject()`, a `context/message` at session start) makes the opener permanent: every `deriveMessages()` consumer replays it, the compaction retention walk owns it, forks bake it in stale, and a resume cannot refresh it — a catalog captured at session birth outlives the world it described. diff --git a/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml b/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml index 70ca07b414..c8cf32d77a 100644 --- a/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-model-facing-goal-tools.md: 286329390a058c0302520fd2203e5becb8c81395 -2026-07-19-model-facing-goal-tools.zh.md: b0b4fc99ada3597fbab58081f52309e21dd43bac +2026-07-19-model-facing-goal-tools.md: 795fa4040d4d6f861b1b3161a1c37146551084ad +2026-07-19-model-facing-goal-tools.zh.md: 399a38a8ef16def4525b361437767552a961b994 diff --git a/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md b/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md index 286329390a..795fa4040d 100644 --- a/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md +++ b/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md @@ -22,13 +22,13 @@ The prompt tells the model that it may infer goal intent from a direct human req All three tools use exclusive execution so a model-ordered batch observes prior mutations and their new revisions. Results are compact JSON. ACP presentation is a pure function of arguments and uses generic read or mutation cards; mutation cards select meaningful action values before the goal id, so accepted fillers cannot blank their input. Activation is reported only as live observation and is never written into replay state. -An autonomous goal round that successfully reports completion or blocking contributes the existing terminal `agent/turn-stop` decision for that physical turn, preventing an unnecessary follow-up request. Direct-human mutations do not contribute a terminal stop: the assistant can acknowledge the change, and concurrent human steering remains available to ordinary continuation folding. +An autonomous goal round that successfully reports completion or blocking marks its tool result as concluding the physical turn, preventing an unnecessary follow-up request. Direct-human mutations do not conclude the turn: the assistant can acknowledge the change, and concurrent human steering remains available to ordinary stopping checks. ### Execution authority Every call requires an `exec.agent` that is the exact running object in `AgentRegistry`, is the current inherited driver initiator, and has an open turn. These are execution-time checks and cannot be bypassed by prompt injection or hand-authored tool arguments. -Create, edit, pause, and resume additionally require an accepted user message or user steering event in the current turn of a runtime-root agent. Root ownership is derived from the live agent graph rather than durable fork ancestry: a resumed fork can receive direct human authority, while a live child remains a subagent and cannot mutate these states. User source is a host attestation: `Agent.send()` and `steer()` default an omitted source to `{ kind: 'user' }`, so non-human producers must label their own content. The runtime proves provenance, not whether the human's wording semantically warrants creation or resumption; that interpretation remains with the model. +Create, edit, pause, and resume additionally require an accepted user message or user steering event in the current turn of a runtime-root agent. Root ownership is derived from the live agent graph rather than durable fork ancestry: a resumed fork can receive direct human authority, while a live child remains a subagent and cannot mutate these states. User source is a host attestation: every `Agent.send()` or `steer()` input requires an explicit source, so the host labels direct human content `{ kind: 'user' }` and non-human producers label their own provenance. The runtime proves provenance, not whether the human's wording semantically warrants creation or resumption; that interpretation remains with the model. Complete and blocked accept either direct-human authority or the exact current goal round. Goal-round authority requires a goal-sourced `user/message` whose goal id, revision, and round all equal the folded current goal. It grants only the two terminal reports. Direct human authority may stop a goal immediately. diff --git a/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.zh.md b/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.zh.md index b0b4fc99ad..399a38a8ef 100644 --- a/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.zh.md +++ b/.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.zh.md @@ -22,13 +22,13 @@ Status: implemented 三个工具都采用独占执行,使模型排序的批次可以观察此前变更及其新修订号。结果为紧凑 JSON。ACP 展示是参数的纯函数,使用通用读取或变更卡片;变更卡片选择输入时,先取有实际意义的操作值,再取目标 id,因此允许的占位值不会使卡片输入留空。激活态仅作为实时观察返回,绝不会写入回放状态。 -自主目标回合成功报告完成或阻塞后,插件会为该物理轮次贡献现有的终止型 `agent/turn-stop` 决策,避免再发起一次不必要的模型请求。直接人类发起的变更不会贡献终止决策:智能体可以确认该变更,并且并发的人类 steering(转向)仍可参与普通的继续执行折叠。 +自主目标回合成功报告完成或阻塞后,其工具结果会被标记为结束该物理轮次,避免再发起一次不必要的模型请求。直接人类发起的变更不会结束轮次:agent 可以确认该变更,并且并发的人类 steering(中途引导)仍可参与普通的停止检查。 ### 执行权限 每次调用都要求存在 `exec.agent`,且它必须是 `AgentRegistry` 中完全相同的运行中对象、当前继承的驱动发起者,并处于开放轮次内。这些检查在执行时进行,不能通过提示词注入或手写工具参数绕过。 -创建、编辑、暂停与恢复还要求运行时根智能体的当前轮次已经接纳一条用户消息或用户 steering(转向)事件。根所有权来自实时智能体图,而非持久的 fork 祖先关系:恢复后的派生会话可以接收新的直接人类权限,实时子智能体则仍然是子智能体,不能改变这些状态。用户来源是宿主的证明:`Agent.send()` 和 `steer()` 会把省略的来源默认为 `{ kind: 'user' }`,因此非人类生产者必须标注自己的内容。运行时证明来源,而不判断人类措辞在语义上是否足以创建或恢复目标;该解释仍由模型完成。 +创建、编辑、暂停与恢复还要求运行时根 agent 的当前轮次已经接纳一条用户消息或用户 steering 事件。根所有权来自实时 agent 图,而非持久的 fork 祖先关系:恢复后的派生会话可以接收新的直接人类权限,实时子级则仍是 subagent,不能改变这些状态。用户来源是宿主的证明:每个 `Agent.send()` 或 `steer()` 输入都必须显式提供来源,因此宿主把直接人类内容标为 `{ kind: 'user' }`,非人类生产者则标注自己的来源信息。运行时证明来源,而不判断人类措辞在语义上是否足以创建或恢复目标;该解释仍由模型完成。 完成与阻塞既接受直接人类权限,也接受准确的当前目标回合。目标回合权限要求存在一条来源为目标的 `user/message`,其中目标 id、修订号和回合都与折叠后的当前目标相等。它只授予这两种终止报告权限。直接人类权限可以立即停止目标。 diff --git a/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml b/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml index f28ec1e2b7..27e29c10a9 100644 --- a/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-same-session-goal-round-driver.md: 34d59456b5a8b54c92aba581da0ff22ea045b626 -2026-07-19-same-session-goal-round-driver.zh.md: dc2afd1ce18a45964bc1db04121211a9958445f3 +2026-07-19-same-session-goal-round-driver.md: f4eb2a279da14dca9b6ab2fe7297f9329575e4f8 +2026-07-19-same-session-goal-round-driver.zh.md: ae5cf7da2560a76131c11fe0c857ea5f48bc6809 diff --git a/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md b/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md index 34d59456b5..f4eb2a279d 100644 --- a/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md +++ b/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md @@ -24,7 +24,7 @@ When an agent is idle, has no competing queued work, and its current goal is `ac The `agent/prompt-submit` waterfall is the admission fence. A positive goal source is allowed only when it exactly matches the driver's pending identity and content, the live goal still has that id and revision, activation remains armed, and the round is still the next number. The plugin checks once before delegating and again after downstream hooks return. This second check prevents an async hook from editing or pausing the goal while still admitting the old prompt. -Only the resulting `user/message` is an admitted round and advances the goal fold. A stale reservation becomes a durable `prompt/blocked` plus zero-step rejected turn, but the driver marks it stale and does not charge the round. A downstream policy rejection that is not caused by staleness blocks the goal rather than retrying around policy. +Only the resulting `user/message` is an admitted round and advances the goal fold. A stale reservation is discarded before a turn opens; the driver marks it stale and does not charge the round. A downstream policy rejection that is not caused by staleness blocks the goal rather than retrying around policy. ### Human work and revision races @@ -43,7 +43,6 @@ The driver classifies one closed goal-owned turn as follows: | `error` with code `RATE_LIMIT` or `QUOTA` | block with code `usage-limited` | | other `error` | block with code `turn-error` | | `max-tokens` | block with code `max-tokens` | -| non-stale `rejected` | block with code `prompt-rejected` | | failed durability checkpoint | disarm without changing durable phase | | `disposed` or `interrupted` | disarm | | plugin-added unknown result | block for inspection | diff --git a/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.zh.md b/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.zh.md index dc2afd1ce1..ae5cf7da25 100644 --- a/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.zh.md +++ b/.agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.zh.md @@ -24,7 +24,7 @@ Status: implemented `agent/prompt-submit` 瀑布是接纳栅栏。正数目标来源只有在完全匹配驱动器待处理的身份和内容、实时目标仍具有相同 id 与修订号、激活态仍为 armed,并且该回合仍是下一个编号时才会获准。插件在委托下游监听器前检查一次,在下游返回后再检查一次。第二次检查防止异步钩子编辑或暂停目标后,旧提示词仍被接纳。 -只有最终产生的 `user/message` 才是已接纳目标回合,并推进目标折叠。过期预留会生成持久的 `prompt/blocked` 和零步骤 rejected 轮次,但驱动器会把它标记为过期,不消耗回合数。若下游策略拒绝并非由过期导致,目标会进入 blocked,而不会绕过该策略自动重试。 +只有最终产生的 `user/message` 才是已接纳目标回合,并推进目标折叠。陈旧预留会在轮次打开前被丢弃;驱动器会把它标记为陈旧,不消耗回合数。若下游策略拒绝并非由陈旧状态导致,目标会进入 blocked,而不会绕过该策略自动重试。 ### 人类工作与修订竞争 @@ -43,7 +43,6 @@ Status: implemented | 代码为 `RATE_LIMIT` 或 `QUOTA` 的 `error` | 以 `usage-limited` 代码阻塞 | | 其他 `error` | 以 `turn-error` 代码阻塞 | | `max-tokens` | 以 `max-tokens` 代码阻塞 | -| 非过期的 `rejected` | 以 `prompt-rejected` 代码阻塞 | | 持久检查点失败 | 解除激活,但不改变持久阶段 | | `disposed` 或 `interrupted` | 解除激活 | | 插件新增的未知结果 | 阻塞并等待检查 | diff --git a/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md b/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md index aa61e859d2..02d4b36908 100644 --- a/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md +++ b/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md @@ -28,4 +28,4 @@ The [content-block vocabulary Agent Note](../architecture/2026-06-11-content-blo ## Consequences -Nothing operational changed — nothing could construct these values. The mirror-event removals ([the boundary-mirror Agent Note](2026-06-20-remove-agent-boundary-mirror-events.md), [the stream-chunk Agent Note](2026-07-02-remove-stream-chunk-mirror.md)) touch only transient `agent/*` events, never the durable vocabulary, so there is no collision. Elsewhere the admission policy already holds: `rejected`, `prompt/blocked`, and `hook/invoked`/`hook/result` each have live producers — this Agent Note extends the same bar to the three variants that lacked one. The image block's own `cache?` field belongs to [the drop-image Agent Note](2026-07-04-drop-image-content-block.md), which removed it together with the block; this Agent Note covers the two fields on the block types that remain. +Nothing operational changed — nothing could construct these values. The mirror-event removals ([the boundary-mirror Agent Note](2026-06-20-remove-agent-boundary-mirror-events.md), [the stream-chunk Agent Note](2026-07-02-remove-stream-chunk-mirror.md)) touch only transient `agent/*` events, never the durable vocabulary, so there is no collision. The later pre-turn admission cleanup removed `rejected` and `prompt/blocked` together with their producers; mid-turn `hook/invoked`/`hook/result` still demonstrate the same producer-backed admission rule. The image block's own `cache?` field belongs to [the drop-image Agent Note](2026-07-04-drop-image-content-block.md), which removed it together with the block; this Agent Note covers the two fields on the block types that remain. diff --git a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml index 60fd394329..2aad1b83d2 100644 --- a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-17-one-send-one-turn.md: 5d25d46a2d7a11be5761c49267b9f7e605035836 -2026-07-17-one-send-one-turn.zh.md: e223f5bcb93cdc7032f8b0ccf550322b9c98facf +2026-07-17-one-send-one-turn.md: d6c771a2b87e14c52b05c9cd8accaa44405fba16 +2026-07-17-one-send-one-turn.zh.md: a057ebce38208f0ed6be3b8b97dd4c434dd3e721 diff --git a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md index 5d25d46a2d..d6c771a2b8 100644 --- a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md +++ b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md @@ -20,7 +20,7 @@ Before enqueueing an item, `send()` checks the agent state and makes a detached, If messages A and B are both processed, B's turn starts only after A records `turn/end` and A's durability checkpoint settles. B's request therefore sees whatever closed result A left in the same session log. A checkpoint error is reported, but settlement only releases this ordering barrier; it does not make a failed write durable. Broad `cancel()`, disposal, or a failure before `turn/start` can instead discard an unstarted item without opening an empty turn. -Prompt admission decides one message at a time. An allowed prompt becomes that turn's `user/message`; a blocked prompt records one durable `prompt/blocked` and closes its one-message turn as `rejected`. Mixed-batch and all-blocked-batch branches do not exist. +Prompt admission decides one message at a time before a turn opens. An allowed prompt becomes that turn's `user/message`; a blocked prompt is discarded without opening a turn or writing session history. Mixed-batch and all-blocked-batch branches do not exist. The no-batching rule applies only to ordinary `send()`. Running `steer()` puts input in the outbox. While a turn remains open, the loop records that input at the next step boundary and steering makes another step the default. A failure before that boundary leaves the steering staged without waking the agent; `retry()` or a later prompt takes it, while cancellation or disposal can discard it. When the agent is idle, `steer()` delegates to `send()`, so it creates an independent ordinary queue item. @@ -35,7 +35,7 @@ The no-batching rule applies only to ordinary `send()`. Running `steer()` puts i - Unit and property tests submit sends from the same stack, neighboring microtasks, different producers, and reentrant callbacks; every message gets its own FIFO-ordered turn. - A built-stdio test submits two lines and observes two model requests and two turn boundaries. - Delayed and rejected first-turn checkpoints keep the next turn waiting and prove that its request sees the preceding assistant result. -- Failure-path tests cover prompt veto, listener failure, broad cancellation, disposal, and failure before `turn/start`; recorded turns stay balanced, messages do not merge, and surviving queued work still drains. +- Failure-path tests cover prompt veto, listener failure, broad cancellation, disposal, and failure before `turn/start`; rejected admission creates no turn, recorded turns stay balanced, messages do not merge, and surviving queued work still drains. - Separate tests cover open-turn, failed-turn, and idle `steer()`, plus `inject()`, whole-agent status, and `whenIdle()`. ## Consequences diff --git a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md index e223f5bcb9..a057ebce38 100644 --- a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md +++ b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md @@ -20,7 +20,7 @@ Status: implemented 如果消息 A、B 都进入处理,B 的轮次只能在 A 记录 `turn/end` 且 A 的持久性检查点处理结束后开始。因此,B 的请求能看到 A 在同一会话日志中留下的已关闭结果。检查点错误会照常报告,但处理结束只表示解除这道顺序屏障,不表示失败的写入已经持久化。广义 `cancel()`、dispose(资源释放)或 `turn/start` 之前的失败也可能丢弃尚未启动的队列项,而不打开一个空轮次。 -提示词准入每次只决定一条消息。获准提示词成为该轮次的 `user/message`;被阻止的提示词记录一条持久的 `prompt/blocked`,并让自己的单消息轮次以 `rejected` 关闭。实现中不存在混合批次或全阻止批次分支。 +提示词准入会在轮次打开前,每次只决定一条消息。获准提示词成为该轮次的 `user/message`;被阻止的提示词会被丢弃,不打开轮次,也不写入会话历史。实现中不存在混合批次或全阻止批次分支。 上述不合批规则只适用于普通 `send()`。agent 运行时,`steer()` 会把输入放入 outbox。只要当前轮次仍然打开,agent loop 就会在下一个步骤边界记录该输入,而 steering(中途引导)会默认让循环再执行一个步骤。在到达该边界前发生失败,会让 steering 保持暂存且不唤醒 agent;`retry()` 或后续提示词会取走它,而取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 委托给 `send()`,因此会创建一个独立的普通队列项。 @@ -35,7 +35,7 @@ Status: implemented - 单元测试和性质测试从同一调用栈、相邻微任务、不同生产方和重入回调提交 send;每条消息都会得到一个按 FIFO 排序的独立轮次。 - stdio 构建产物测试提交两行输入,并观察到两个模型请求和两个轮次边界。 - 延迟和拒绝第一个轮次的检查点,都能让下一个轮次保持等待,并证明其请求可以看到前一条助手结果。 -- 失败路径测试覆盖提示词否决、监听器失败、广义取消、dispose 和 `turn/start` 之前的失败;已记录的轮次保持边界平衡,消息不会合并,仍需处理的排队工作也能继续清空。 +- 失败路径测试覆盖提示词否决、监听器失败、广义取消、dispose 和 `turn/start` 之前的失败;准入拒绝不会创建轮次,已记录的轮次保持边界平衡,消息不会合并,仍需处理的排队工作也能继续清空。 - 其他测试分别覆盖轮次打开时、轮次失败后和空闲时的 `steer()`,以及 `inject()`、面向整个 agent 的状态和 `whenIdle()`。 ## 后果 diff --git a/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.i18n.yaml index 233cc3901c..9979320166 100644 --- a/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-24-separate-context-injection-from-turn-execution.md: 652c3d410ab625d91a828f854bce302adcb0c9e0 -2026-07-24-separate-context-injection-from-turn-execution.zh.md: 1064e7a869ab9ea46c0145eb010119894a03aacf +2026-07-24-separate-context-injection-from-turn-execution.md: ada6d222f2df06cfb004944dbfa0dc82747ef5d2 +2026-07-24-separate-context-injection-from-turn-execution.zh.md: 85cad4bef76cf2a12df8f039a6d96985d5d9c370 diff --git a/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.md b/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.md index 652c3d410a..ada6d222f2 100644 --- a/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.md +++ b/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.md @@ -18,7 +18,7 @@ Idle `inject()` exposes a second mismatch. Injection does not request model exec Make `inject()` the only caller-facing operation for adding supplementary model-facing input, and define a turn exclusively as one execution of the model loop. -Remove `SendOptions.contexts`. A caller that owns context delivers it with `inject()` and independently submits the direct message with `send()` or `steer()`. Rename `HookContext` to `AdditionalContext`; retain only `content` and `source`, and remove placement and model-hidden metadata from this shared shape. +Remove `SendOptions.contexts`. A caller that owns context delivers it with `inject()` and independently submits the direct message with `send()` or `steer()`. Use the existing `UserMessageData` content/source shape directly for additional model-facing context instead of creating another name for it. Prompt and tool extension points may still return `additionalContexts`. These values are outputs of the extension point, not attachments captured from a caller's inbox item. Prompt admission runs before `run()` opens a turn. An allowed prompt enters the outbox together with its returned additional contexts; a blocked prompt writes neither and opens no turn. Tool-produced additional contexts enter the same outbox after the corresponding tool results. @@ -59,7 +59,7 @@ This proposal preserves the caller-owned framing decision from [unwrapped inject ## Acceptance criteria - `SendOptions` and steering inbox records contain no attached contexts; `agent/queued` reports only the retained message and steering facts. -- `AdditionalContext` replaces `HookContext` across prompt interception, tool execution, hook bridges, guards, and context producers, with only `content` and `source`. +- `UserMessageData` replaces `HookContext` across prompt interception, tool execution, hook bridges, guards, and context producers. - Prompt-prefix placement, prompt envelopes, and `context/message` are absent from public types, durable events, projection, and UI replay. - Idle `inject()` appends and flushes one sourced `user/message` without a turn or model call; `whenIdle()` and disposal await the flush. - Active-turn injection and hook-produced contexts drain at safe boundaries after complete tool-result batches and before the request that consumes them. diff --git a/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.zh.md b/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.zh.md index 1064e7a869..85cad4bef7 100644 --- a/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.zh.md +++ b/.agents/notes/proposed/architecture/2026-07-24-separate-context-injection-from-turn-execution.zh.md @@ -18,7 +18,7 @@ agent API 目前用三种相互重叠的方式表示面向模型的补充输入 将 `inject()` 设为调用方添加补充模型输入的唯一操作,并把轮次严格定义为一次模型循环执行。 -移除 `SendOptions.contexts`。拥有上下文的调用方通过 `inject()` 交付上下文,再独立使用 `send()` 或 `steer()` 提交直接消息。将 `HookContext` 重命名为 `AdditionalContext`;这个共享结构只保留 `content` 和 `source`,移除放置方式与模型不可见元数据。 +移除 `SendOptions.contexts`。拥有上下文的调用方通过 `inject()` 交付上下文,再独立使用 `send()` 或 `steer()` 提交直接消息。附加的模型可见上下文直接使用已有的 `UserMessageData` content/source 形状,不再为同一结构创建另一个名字。 提示词和工具扩展点仍可返回 `additionalContexts`。这些值是扩展点的输出,而不是从调用方收件箱条目捕获的附件。提示词准入在 `run()` 打开轮次之前执行。提示词获准后,它与返回的额外上下文一同进入 outbox;提示词被阻止时,两者都不写入,也不打开轮次。工具产生的额外上下文则在对应工具结果之后进入同一个 outbox。 @@ -59,7 +59,7 @@ agent API 目前用三种相互重叠的方式表示面向模型的补充输入 ## 验收标准 - `SendOptions` 与 steering 收件箱记录不再包含附加上下文;`agent/queued` 只报告保留的消息和 steering 事实。 -- `AdditionalContext` 在提示词拦截、工具执行、hook bridge、guard 和上下文生产方中取代 `HookContext`,且只包含 `content` 与 `source`。 +- `UserMessageData` 在提示词拦截、工具执行、hook bridge、guard 和上下文生产方中取代 `HookContext`。 - 公共类型、持久事件、投影和 UI 回放中均不存在 prompt-prefix 放置方式、提示词封套与 `context/message`。 - 空闲 `inject()` 在不产生轮次或模型调用的情况下,追加并刷新一条带来源的 `user/message`;`whenIdle()` 和 dispose 会等待该刷新。 - 活跃轮次注入和钩子产生的上下文会在完整工具结果批次之后的安全边界排空,并在消费它们的请求之前进入日志。 diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index fdc8f4197d..e9b9bf2439 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -architecture.md: 1bf151c0d7db88b102dd49fa8f21ece02beec9d2 -architecture.zh.md: 43d1b6ff0a2123f4a48ded99d938e70ebf93f084 +architecture.md: 40d1641e494d2087e5fb5f9c50dbe1a2df30b103 +architecture.zh.md: 324c4ff1950cbe3a3b8f015bb545dad3869dd5b4 diff --git a/docs/architecture.md b/docs/architecture.md index 1bf151c0d7..40d1641e49 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -178,8 +178,8 @@ New behavior attaches to a documented extension point; a loop change updates thi | Add background work | register on `ctx.tasks`; generic `task_*` tools collect or stop it | | Add filesystem access or policy | implement a `ctx.fs` provider or listen on `fs/*` policy events | | Confine spawned processes | a `ctx.sandbox` backend; consumers wrap their argv before spawning | -| Intercept a request, tool, or turn | use its `agent/*` or `tools/*` event; `agent/turn-stop` is the serial terminal stop | -| Add a session-stable prefix outside history | compose `agent/session-prefix`; the request header logs it | +| Intercept a request, tool, or turn | use its `agent/*` or `tools/*` event; `agent/stopping` is the stop boundary | +| Add model-facing context | call `agent.inject()`; it appends a sourced `user/message` without creating a turn | | Add UI or editor integration | drive `ctx.agents` and render from `session/event`; terminal-only overlays use `ctx.tui` | | Add durable session state | add a `SessionEventMap` member and render/replay from the log | | Add asynchronous session-title generation | register the sole provider on `ctx.sessionTitle` | diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 43d1b6ff0a..324c4ff195 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -178,8 +178,8 @@ idle inject: | 添加后台工作 | 在 `ctx.tasks` 上注册;通用 `task_*` 工具负责收集或停止 | | 添加文件系统访问或策略 | 实现 `ctx.fs` 提供方,或监听 `fs/*` 策略事件 | | 限制生成的进程 | 使用 `ctx.sandbox` 后端;消费方在生成进程前包装 argv | -| 拦截请求、工具或轮次 | 使用相应的 `agent/*` 或 `tools/*` 事件;`agent/turn-stop` 是串行终止判定点 | -| 添加历史记录之外的会话稳定前缀 | 组合 `agent/session-prefix`;请求头会记录该前缀 | +| 拦截请求、工具或轮次 | 使用相应的 `agent/*` 或 `tools/*` 事件;`agent/stopping` 是停止边界 | +| 添加模型可见上下文 | 调用 `agent.inject()`;它会追加带来源的 `user/message`,但不创建轮次 | | 添加 UI 或编辑器集成 | 驱动 `ctx.agents` 并从 `session/event` 渲染;仅终端可用的浮层使用 `ctx.tui` | | 添加持久会话状态 | 添加一个 `SessionEventMap` 成员,并从日志渲染和回放 | | 添加异步会话标题生成 | 在 `ctx.sessionTitle` 上注册唯一提供方 | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 6600d6a172..e612f4d03e 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -27,7 +27,7 @@ export interface AcpConfig { Depends on: `Stream` (`@agentclientprotocol/sdk`) -Source: [`packages/ui/acp/src/index.ts:286`](../packages/ui/acp/src/index.ts) +Source: [`packages/ui/acp/src/index.ts:285`](../packages/ui/acp/src/index.ts) ## `@deepseek-ai/dsh-acp-demo` @@ -447,7 +447,7 @@ export interface Config { } ``` -Source: [`packages/hooks/hooks-claude/src/index.ts:44`](../packages/hooks/hooks-claude/src/index.ts) +Source: [`packages/hooks/hooks-claude/src/index.ts:45`](../packages/hooks/hooks-claude/src/index.ts) ## `@deepseek-ai/dsh-hooks-codex` @@ -472,7 +472,7 @@ export interface Config { } ``` -Source: [`packages/hooks/hooks-codex/src/index.ts:42`](../packages/hooks/hooks-codex/src/index.ts) +Source: [`packages/hooks/hooks-codex/src/index.ts:43`](../packages/hooks/hooks-codex/src/index.ts) ## `@deepseek-ai/dsh-invariants` @@ -870,7 +870,7 @@ export interface Config { } ``` -Source: [`packages/guard/repeat-tool-guard/src/index.ts:26`](../packages/guard/repeat-tool-guard/src/index.ts) +Source: [`packages/guard/repeat-tool-guard/src/index.ts:27`](../packages/guard/repeat-tool-guard/src/index.ts) ## `@deepseek-ai/dsh-sandbox-local` @@ -1631,7 +1631,7 @@ export interface TuiConfig { } ``` -Source: [`packages/ui/tui/src/index.ts:268`](../packages/ui/tui/src/index.ts) +Source: [`packages/ui/tui/src/index.ts:269`](../packages/ui/tui/src/index.ts) ## `@deepseek-ai/dsh-tui-demo` diff --git a/docs/cookbook/adding-a-tool.i18n.yaml b/docs/cookbook/adding-a-tool.i18n.yaml index 2c8f18513b..791ea8d16e 100644 --- a/docs/cookbook/adding-a-tool.i18n.yaml +++ b/docs/cookbook/adding-a-tool.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -adding-a-tool.md: 788c967cffa9df06e8d96e190930d69b9f2182ed -adding-a-tool.zh.md: 4ff9ce25a66625435ccba4454f2ac4b5472e5668 +adding-a-tool.md: 577b12e68ed8319266b5188fd036be85f61c2d0c +adding-a-tool.zh.md: 538ad84dd13d3cacd11e09e1d365d31ab4a92c5c diff --git a/docs/cookbook/adding-a-tool.md b/docs/cookbook/adding-a-tool.md index 788c967cff..577b12e68e 100644 --- a/docs/cookbook/adding-a-tool.md +++ b/docs/cookbook/adding-a-tool.md @@ -46,7 +46,7 @@ Registration is effect-based: disposing the plugin fiber unregisters the tool (w - **Throwing or returning an invalid value means `isError`.** The registry catches throws and contains schema, renderer, metadata-projector, and lossless-JSON failures before observers run. Throw for infrastructure failures. Represent a successful domain outcome in the canonical value even when its Native renderer explains a non-ideal state, such as a non-zero process exit. - **Honor `exec.signal`.** Cancel in-flight work when it fires. - **Project durable card data with `presentationMeta` (optional).** `output.presentationMeta(args, value)` derives replayable JSON from the same canonical value. The core persists it on `tool/result` and hands it to `presentResult`, so a card that needs result-time facts—such as `write`/`edit` applied hunks—survives replay without persisting the canonical value. The projector is skipped for nested Code dispatches because they have no cards. -- **Use `exec.agent` for async notifications.** `agent.inject(content, {source: {kind: 'plugin', plugin: ''}})` appends durable context the NEXT model request sees — it is not a wake-up (an idle agent stays idle). Guard against disposed agents (try/catch). +- **Use `exec.agent` for async notifications.** `agent.inject({ content, source: { kind: 'plugin', plugin: '' } })` appends durable context the NEXT model request sees — it is not a wake-up (an idle agent stays idle). Guard against disposed agents (try/catch). ## Long-running work diff --git a/docs/cookbook/adding-a-tool.zh.md b/docs/cookbook/adding-a-tool.zh.md index 4ff9ce25a6..538ad84dd1 100644 --- a/docs/cookbook/adding-a-tool.zh.md +++ b/docs/cookbook/adding-a-tool.zh.md @@ -46,7 +46,7 @@ export function apply(ctx: Context) { - **抛出异常或返回无效值意味着 `isError`。** 注册表会捕获异常,并在观察者运行前收敛 schema、渲染器、元数据投影器和无损 JSON 失败。基础设施故障请抛异常。成功的领域结果即使表示不理想的状态,也应写入规范值;其 Native 渲染器可以解释该状态,例如进程以非零状态退出。 - **遵守 `exec.signal`。** 信号触发时取消进行中的工作。 - **使用 `presentationMeta` 投影持久化的卡片数据(可选)。** `output.presentationMeta(args, value)` 从同一个规范值派生可回放的 JSON。核心将其持久化在 `tool/result` 上并传给 `presentResult`,因此需要结果期事实的卡片——例如 `write`/`edit` 的已应用 hunk——无需持久化规范值也能在回放中重现。嵌套 Code 分发没有卡片,因此会跳过该投影器。 -- **使用 `exec.agent` 发送异步通知。** `agent.inject(content, {source: {kind: 'plugin', plugin: ''}})` 追加持久化上下文,下一次模型请求会看到它——这不是唤醒(空闲的 agent(智能体)保持空闲)。请防范已 dispose 的 agent(try/catch)。 +- **使用 `exec.agent` 发送异步通知。** `agent.inject({ content, source: { kind: 'plugin', plugin: '' } })` 追加持久化上下文,下一次模型请求会看到它——这不是唤醒(空闲的 agent(智能体)保持空闲)。请防范已 dispose 的 agent(try/catch)。 ## 长时间运行的工作 diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml index 4c9900a606..7effaf88b0 100644 --- a/docs/cookbook/extension-cookbook.i18n.yaml +++ b/docs/cookbook/extension-cookbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -extension-cookbook.md: 723c169e8e39e13be23db95c8152d067058a1a24 -extension-cookbook.zh.md: a02928a94a5ffba3608de822d5afbf920d0f16bf +extension-cookbook.md: 7c5daaa33f888d7a94e962eaf9c9477fa2dcc5b8 +extension-cookbook.zh.md: 6ce86eba06d1415046f17241c7312e4f5aafc0c0 diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md index 723c169e8e..7c5daaa33f 100644 --- a/docs/cookbook/extension-cookbook.md +++ b/docs/cookbook/extension-cookbook.md @@ -54,7 +54,10 @@ export function apply(ctx: Context) { render(event.data.chunk.text) } }) - onUserInput(text => ctx.agents.get(SessionId('client-session'))?.followup([{ type: 'text', text }])) + onUserInput(text => ctx.agents.get(SessionId('client-session'))?.followup({ + content: [{ type: 'text', text }], + source: { kind: 'user' }, + })) } ``` @@ -97,10 +100,10 @@ Every product feature maps to a listener on a documented extension seam — the | Product feature | Plugin mechanism | |---|---| -| Hook system (user + project level) | listeners on `agent/session-start`, `agent/prompt-submit`, `agent/request`, `agent/step-result`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation` — each interception waterfall returns a typed Decision; the `dsh-hooks-claude` / `dsh-hooks-codex` bridges map hook config files onto these seams | +| Hook system (user + project level) | listeners on `agent/session-start`, `agent/prompt-submit`, `agent/request`, `tools/pre-execute`, `tools/post-execute`, and `agent/stopping`; the waterfall seams return typed decisions, while `agent/stopping` may steer another step; the `dsh-hooks-claude` / `dsh-hooks-codex` bridges map hook config files onto these seams | | `/goal` | `ctx.goals` owns durable state, `dsh-goal-session` schedules same-session rounds through the public `Agent`, and separate command/tool producers expose human/model control | | `/loop` | on the `turn/end` session event, `followup()` the next iteration; or force-continue | -| Dynamic workflow | `ctx.workflows` + the worker-thread engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and terminal `agent/turn-stop` | +| Dynamic workflow | `ctx.workflows` + the worker-thread engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and the structured-output execution's monotonic `concludeTurn()` marker | | Queued + steering messages | core `Agent.followup()` / `Agent.steer()` | | Context compaction (auto + manual) | the `ctx.compact` seam + `dsh-compact-basic`; automatic pressure runs on serial `agent/step`, canonical overflow recovery runs on `agent/request-error`, and manual callers use the same compact service ([compaction Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) — the model-facing `/compact` consumer tool is deferred) | | System prompt configurability | `ctx.systemPrompt.section()` with ordering and scope-local shadowing | @@ -110,7 +113,7 @@ Every product feature maps to a listener on a documented extension seam — the | ToolSearch / progressive disclosure | replace a scoped `ctx.tools.restrict()` registration as the visible set changes; the registry keeps presentation, lookup, and execution aligned | | Tool deadline / retry / metrics | wrap core dispatch with `tools/execute`; a wrapper may replace `exec.signal`, delegate, and inspect the normalized result in one lexical lifetime | | Final tool-result metrics / audit / capture | observe immutable authoritative outcomes with `tools/result`; use `tools/post-execute` instead only when the plugin must transform the result or attach context | -| Monotonic terminal turn policy | return `{ action: 'stop' }` from serial `agent/turn-stop`, after continuation and steering have already been folded | +| Monotonic terminal turn policy | call `ToolExecution.concludeTurn()` from the successful terminal tool; later tool calls in the same response remain guardable, and the loop stops after the step | | Subprocess sandbox (landlock / sandbox-exec) | use a `ctx.sandbox` backend through `dsh-bash-sandbox`; use `tools/pre-execute` for capability-level denial | | Permission system / AskUserQuestion | return `ask` from `tools/pre-execute` and answer through `ctx.approval`; register a separate model-facing ask tool for ordinary user questions | | Plan mode | Shipped: [`@deepseek-ai/dsh-plan-mode`](../../packages/plan/plan-mode/README.md) — logged `plan/mode` state, the `plan:policy` guidance section, `/plan [message]` entry, `/plan off` direct exit, and the user-reviewed `exit_plan_mode` exit; enforcement stays on the independent sandbox/approval axes | diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md index a02928a94a..6ce86eba06 100644 --- a/docs/cookbook/extension-cookbook.zh.md +++ b/docs/cookbook/extension-cookbook.zh.md @@ -54,7 +54,10 @@ export function apply(ctx: Context) { render(event.data.chunk.text) } }) - onUserInput(text => ctx.agents.get(SessionId('client-session'))?.followup([{ type: 'text', text }])) + onUserInput(text => ctx.agents.get(SessionId('client-session'))?.followup({ + content: [{ type: 'text', text }], + source: { kind: 'user' }, + })) } ``` @@ -97,10 +100,10 @@ export function apply(ctx: Context) { | 产品功能 | 插件机制 | |---|---| -| 钩子系统(用户级 + 项目级) | `agent/session-start`、`agent/prompt-submit`、`agent/request`、`agent/step-result`、`tools/pre-execute`、`tools/post-execute`、`agent/turn-continuation` 上的监听器——每个拦截 waterfall 返回一个类型化 Decision;`dsh-hooks-claude` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些 seam 上 | +| 钩子系统(用户级 + 项目级) | `agent/session-start`、`agent/prompt-submit`、`agent/request`、`tools/pre-execute`、`tools/post-execute` 和 `agent/stopping` 上的监听器;waterfall seam 返回类型化决策,`agent/stopping` 则可通过 steering 触发下一步;`dsh-hooks-claude` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些 seam 上 | | `/goal` | `ctx.goals` 管理持久状态,`dsh-goal-session` 通过公共 `Agent` 调度同会话回合,独立的命令/工具生产方分别提供人类/模型控制 | | `/loop` | 在 `turn/end` 会话事件上 `followup()` 下一次迭代;或强制继续 | -| 动态工作流 | `ctx.workflows` + worker-thread 引擎 + `workflow` 工具;结构化的进程内子任务通过作用域化的 prompt/工具注册、单调工具守卫、最终 `tools/result` 提交(包括外层 `run_code`)和终端 `agent/turn-stop` 来强制输出 | +| 动态工作流 | `ctx.workflows` + worker-thread 引擎 + `workflow` 工具;结构化的进程内子任务通过作用域化的 prompt/工具注册、单调工具守卫、最终 `tools/result` 提交(包括外层 `run_code`)和结构化输出执行的单调 `concludeTurn()` 标记来强制输出 | | 排队消息 + steering(中途引导) | 核心 `Agent.followup()` / `Agent.steer()` | | 上下文压缩(context compaction)(自动 + 手动) | `ctx.compact` seam + `dsh-compact-basic`;自动压力检查运行在串行 `agent/step`,规范化溢出恢复运行在 `agent/request-error`,手动调用方使用同一个压缩服务([压缩 Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)——面向模型的 `/compact` 消费方工具已推迟) | | 系统提示词可配置性 | `ctx.systemPrompt.section()`,支持排序与作用域局部覆盖 | @@ -110,7 +113,7 @@ export function apply(ctx: Context) { | ToolSearch / 渐进式披露 | 当可见集变化时替换一个作用域化的 `ctx.tools.restrict()` 注册;注册表保持展示、查找和执行三者对齐 | | 工具截止时间 / 重试 / 指标 | 用 `tools/execute` 包裹核心分发;包装器可替换 `exec.signal`、委托执行,并在同一词法生命周期内检视规范化结果 | | 最终工具结果指标 / 审计 / 捕获 | 用 `tools/result` 观察不可变的权威结果;仅当插件需要变换结果或附加上下文时才使用 `tools/post-execute` | -| 单调终端轮次策略 | 从串行 `agent/turn-stop` 返回 `{ action: 'stop' }`,此时 continuation 和 steering 已折叠完毕 | +| 单调终端轮次策略 | 从成功的终端工具调用 `ToolExecution.concludeTurn()`;同一响应中后续工具调用仍可由守卫阻止,循环在该步骤后停止 | | 子进程沙箱(landlock / sandbox-exec) | 通过 `dsh-bash-sandbox` 使用 `ctx.sandbox` 后端;能力级别的拒绝使用 `tools/pre-execute` | | 权限系统 / AskUserQuestion | 从 `tools/pre-execute` 返回 `ask` 并通过 `ctx.approval` 应答;为普通用户提问注册一个独立的面向模型的 ask 工具 | | Plan mode | 已交付:[`@deepseek-ai/dsh-plan-mode`](../../packages/plan/plan-mode/README.md) — 落日志的 `plan/mode` 状态、`plan:policy` 引导段、`/plan [message]` 入口、`/plan off` 直接退出,以及经用户评审的 `exit_plan_mode` 出口;强制约束留在独立的沙箱/审批轴上 | diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index 4d391d2bd7..755fb4b954 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -32,7 +32,7 @@ Effective broad cancellation was requested, before queued/outbox work is cleared Types: [Agent](../core-data-structures/core.md) · [AgentCancelCause](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:310`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:294`](../../packages/core/agent/src/types.ts) ### `agent/created` — emit @@ -54,7 +54,7 @@ A fully configured agent and live session were published. Setup is composition-o Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:252`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:236`](../../packages/core/agent/src/types.ts) ### `agent/disposed` — emit @@ -74,7 +74,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence and sco Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:261`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:245`](../../packages/core/agent/src/types.ts) ### `agent/error` — emit @@ -96,7 +96,7 @@ A step or turn errored. The machine reports a failure here (plus the logger) eve Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:418`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:403`](../../packages/core/agent/src/types.ts) ### `agent/idle` — emit @@ -119,7 +119,7 @@ One drain chain reached its terminal turn: that turn's `turn/end` is already com Types: [Agent](../core-data-structures/core.md) · [IdleReason](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:405`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:390`](../../packages/core/agent/src/types.ts) ### `agent/inbox/dequeue` — emit @@ -140,7 +140,7 @@ The driver claimed one item out of the inbox: a queued item at a turn boundary, Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:288`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:272`](../../packages/core/agent/src/types.ts) ### `agent/inbox/discard` — emit @@ -163,7 +163,7 @@ Pending inbox items were dropped without delivering them, so every enqueued id r Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:300`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:284`](../../packages/core/agent/src/types.ts) ### `agent/inbox/enqueue` — emit @@ -182,17 +182,18 @@ An item entered the queued or steering inbox. Types: [Agent](../core-data-structures/core.md) · [AgentMessage](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:278`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:262`](../../packages/core/agent/src/types.ts) ### `agent/prompt-submit` — waterfall -Allow, rewrite, or block one claimed prompt before it becomes a user message. Call `next()` for the unchanged default. The signal controls only this turn; listeners may cooperate with it but must not retain it for another turn. +Allow, rewrite, or block one claimed prompt before it becomes a user message or opens a turn. Call `next()` for the unchanged default. The signal controls only this admission attempt; listeners may cooperate with it but must not retain it for a later attempt or turn. ```ts cordis-catalog /** * Allow, rewrite, or block one claimed prompt before it becomes a user - * message. Call `next()` for the unchanged default. The signal controls only this turn; - * listeners may cooperate with it but must not retain it for another turn. + * message or opens a turn. Call `next()` for the unchanged default. The + * signal controls only this admission attempt; listeners may cooperate with + * it but must not retain it for a later attempt or turn. * @param agent - the agent whose turn claimed the message. * @param content - the claimed message's blocks, as queued. * @param source - the message's resolved source. @@ -205,7 +206,7 @@ Allow, rewrite, or block one claimed prompt before it becomes a user message. Ca Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:337`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:322`](../../packages/core/agent/src/types.ts) ### `agent/request` — waterfall @@ -229,7 +230,7 @@ Replace the frozen call configuration. `await next()` yields the config the mach Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:363`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:348`](../../packages/core/agent/src/types.ts) ### `agent/request-error` — waterfall @@ -255,7 +256,7 @@ Handle a model-request failure after its failed step has closed but before the f Types: [Agent](../core-data-structures/core.md) · [LlmFailure](../core-data-structures/llm-streaming.md) · [RequestError](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:378`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:363`](../../packages/core/agent/src/types.ts) ### `agent/session-start` — emit @@ -277,7 +278,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:323`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:307`](../../packages/core/agent/src/types.ts) ### `agent/status` — emit @@ -297,7 +298,7 @@ Agent status changed (`idle` ⇄ `running`). `send()` does not enter `running` s Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:270`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:254`](../../packages/core/agent/src/types.ts) ### `agent/step` — serial @@ -321,7 +322,7 @@ Awaited serial checkpoint before EVERY request of a turn is built (the first as Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:350`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:335`](../../packages/core/agent/src/types.ts) ### `agent/stopping` — serial @@ -347,7 +348,7 @@ The turn is about to close: the model owes no response (no live tool calls, no f Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:393`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:378`](../../packages/core/agent/src/types.ts) ## `agent-loop/*` diff --git a/docs/core-data-structures/core.i18n.yaml b/docs/core-data-structures/core.i18n.yaml index 403b25986e..f2fb8d4aa4 100644 --- a/docs/core-data-structures/core.i18n.yaml +++ b/docs/core-data-structures/core.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -core.md: 882352e7dc814443468195f57b9c3f7c9401b170 -core.zh.md: e686216530f827ad8b94b42a43e3fa66a2d3a36e +core.md: 697ac0811dc0f5b9aa3c27998bfa4ea3ff39cba3 +core.zh.md: a3fb8f9f97cf05d5476c5e6b84cc35246111ba41 diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 38ef98551a..697ac0811d 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -216,8 +216,7 @@ interface GenerateOptions { /** * Ordered conversation messages, exactly as the provider sees them (after * the `system` slot). A loop-built request assembles them as - * `EpochHeader.messagePrefix` + the derived history (dsh-agent-loop); a - * hand-built one-shot passes any list. + * the derived history (dsh-agent-loop); a hand-built one-shot passes any list. */ messages: Message[] /** System prompt text (adapters map to the provider's system slot). */ @@ -287,11 +286,11 @@ The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition` ### The request envelope: `LlmCallConfig` and the logged header -The loop builds each request from logged state. `EpochHeader` records call config, rendered prompt, authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset), and session prefix through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md). +The loop builds each request from logged state. `EpochHeader` records call config, rendered prompt, and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md). -`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, or sampling. `agent/session-prefix` composes request-only prefix messages once per loop instance, and the header records the exact result used. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests. +`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, or sampling. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests. -On the wire, a loop-built request reads in this order: the `system` slot (the rendered prompt assembly) → `messagePrefix` (the frozen session prefix) → the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The prefix never enters the derived history; its durable record is the header events, and the dev invariant recomputes exactly this equation against every loop-built request. +On the wire, a loop-built request reads the `system` slot (the rendered prompt assembly) followed by the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The dev invariant recomputes exactly this equation against every loop-built request. FIXME(call-config-shape): revisit the exact definition of this type — which fields are genuinely epoch-level for cache purposes (`model` certainly; the sampling scalars sit here out of caution), and where provider-specific extras (reasoning options, extra body params) belong when an adapter needs them. @@ -353,7 +352,7 @@ type SessionEvent = { }[T] ``` -The thirteen event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `user/message`, `prompt/blocked`, `assistant/chunk`, `assistant/message`, `tool/call`, `tool/result`, `steering/message`, `todo/write`, `request/header`), the `deriveMessages()` projection rules, the `TurnTrigger`/`TurnEndReason` reasons, and the turn-enclosure invariant are on **[session.md](session.md)**. How the log is made durable — the `SessionPersistence` seam, JSONL/SQLite backends, the `session/flush` checkpoint, crash recovery, and `SessionHeader` — is on **[persistence.md](persistence.md)**. +The twelve event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `user/message`, `assistant/chunk`, `assistant/message`, `tool/call`, `tool/result`, `steering/message`, `todo/write`, `request/header`), the `deriveMessages()` projection rules, the `TurnTrigger`/`TurnEndReason` reasons, and the turn-enclosure invariant are on **[session.md](session.md)**. How the log is made durable — the `SessionPersistence` seam, JSONL/SQLite backends, the `session/flush` checkpoint, crash recovery, and `SessionHeader` — is on **[persistence.md](persistence.md)**. ## The agent handle @@ -378,8 +377,7 @@ type SendTarget = 'next-turn' | 'next-step' * (`next-turn`/wakeup), {@link Agent.steer} (`next-step`/wakeup), and * {@link Agent.inject} (`next-step`/no-wakeup). * - * The object is complete so routing and provenance are explicit; callers that - * want the ordinary user-message preset use {@link Agent.followup}. + * The object is complete so routing policy is explicit. */ interface SendOptions { /** Queue the item joins. */ @@ -392,20 +390,10 @@ interface SendOptions { * (the injection preset). */ wakeup: boolean - /** Producer provenance; direct human input uses `{ kind: 'user' }`. */ - source: MessageSource } ``` -The fixed-preset aliases own `target` and `wakeup`, so they accept only the remaining fields: - -```ts type-equiv -/** Options accepted by the fixed-preset aliases, which own `target` and `wakeup`. */ -interface AliasSendOptions { - /** Producer provenance; each alias supplies its documented default when omitted. */ - source?: MessageSource -} -``` +The fixed-preset aliases own `target` and `wakeup`; their `UserMessageData` input carries both content and provenance. `send` returns the accepted message's opaque `AgentMessageId`, stable across that message's `agent/inbox/*` events: @@ -423,8 +411,8 @@ The `agent/inbox/*` live events carry one accepted message; injection bypasses t /** * One accepted {@link Agent.send} message, carried by the `agent/inbox/*` live * events. `id` is the value `send` returned to the caller, stable across this - * message's enqueue, dequeue, and discard events. Source defaults are already - * applied, so these are the exact values the item was accepted with. + * message's enqueue, dequeue, and discard events. Its content and source are + * the exact input values accepted by the agent. */ interface AgentMessage extends UserMessageData { /** The id `send` returned for this message. */ @@ -480,11 +468,11 @@ interface Agent { * without running the model: an open turn stages it for the next safe log * position, while an idle injection appends it immediately without opening * a turn. - * @param content - the model-facing content blocks to deliver. - * @param options - target queue, wakeup decision, and source. + * @param input - model-facing content and its producer provenance. + * @param options - target queue and wakeup decision. * @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events. */ - send(content: ContentBlock[], options: SendOptions): AgentMessageId + send(input: UserMessageData, options: SendOptions): AgentMessageId /** * Clear queued and steering work — unless `keepInbox` — and abort the active @@ -504,11 +492,10 @@ interface Agent { * Queue an ordinary follow-up turn and wake the driver — the * `next-turn`/wakeup preset of {@link send}. The item becomes the sole * ordinary message of its own turn. - * @param content - the prompt content blocks. - * @param options - message source. + * @param input - prompt content and its producer provenance. * @returns the accepted message's {@link AgentMessageId}. */ - followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId + followup(input: UserMessageData): AgentMessageId /** * Submit steering into the running turn — the `next-step`/wakeup preset of @@ -517,23 +504,20 @@ interface Agent { * remainder stays staged without waking the agent; retry or a later prompt * takes it. Idle steering falls back to a woken follow-up turn, while * cancellation or disposal may discard pending steering. - * @param content - the steering content blocks. - * @param options - message source. + * @param input - steering content and its producer provenance. * @returns the accepted message's {@link AgentMessageId}. */ - steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId + steer(input: UserMessageData): AgentMessageId /** * Append model-facing context without running the model — the * `next-step`/no-wakeup preset of {@link send}. An open-turn injection stages * at the next safe log position; an idle injection appends immediately - * without opening a turn. An omitted source defaults to - * `{ kind: 'plugin', plugin: '' }`. - * @param content - the injected context content blocks. - * @param options - context source. + * without opening a turn. + * @param input - injected context and its producer provenance. * @returns the accepted message's {@link AgentMessageId}. */ - inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId + inject(input: UserMessageData): AgentMessageId /** * Re-open a turn on the current session log without a new prompt — the @@ -558,15 +542,10 @@ The process-local initiator carried by `ctx.agents` is the exact `Agent` above, ## Interception decisions -Prompt and post-tool decisions share `AdditionalContext`, the same `UserMessageData` content/source shape used by durable user-role input. Each `additionalContexts` entry becomes a separate `user/message`, preserving its provenance. Hook bridges map their native decision fields onto these typed results. +Prompt and post-tool decisions use the same `UserMessageData` content/source shape as durable user-role input. Each `additionalContexts` entry becomes a separate `user/message`, preserving its provenance. Hook bridges map their native decision fields onto these typed results. Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) -```ts type-equiv -/** Additional model-facing context produced beside a prompt or tool result. */ -type AdditionalContext = UserMessageData -``` - `agent/prompt-submit` returns a `PromptDecision` before a turn opens. Allow may rewrite the claimed prompt or attach `additionalContexts`; block rejects admission without creating turn events: ```ts type-equiv @@ -577,7 +556,7 @@ type AdditionalContext = UserMessageData * `next()` preserves both fields unless it intentionally replaces them. */ type PromptDecision = - | { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: AdditionalContext[] } + | { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: UserMessageData[] } | { kind: 'block'; reason: string } ``` diff --git a/docs/core-data-structures/core.zh.md b/docs/core-data-structures/core.zh.md index 276e8a7b1d..a3fb8f9f97 100644 --- a/docs/core-data-structures/core.zh.md +++ b/docs/core-data-structures/core.zh.md @@ -218,8 +218,7 @@ interface GenerateOptions { /** * Ordered conversation messages, exactly as the provider sees them (after * the `system` slot). A loop-built request assembles them as - * `EpochHeader.messagePrefix` + the derived history (dsh-agent-loop); a - * hand-built one-shot passes any list. + * the derived history (dsh-agent-loop); a hand-built one-shot passes any list. */ messages: Message[] /** System prompt text (adapters map to the provider's system slot). */ @@ -289,11 +288,11 @@ interface ToolSchema { ### 请求信封:`LlmCallConfig` 与记录的 header -循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、渲染后的提示词、权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)以及会话前缀。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。 +循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、渲染后的提示词和权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。 -`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型或采样参数。`agent/session-prefix` 为每个循环实例组合一次仅用于请求的 prefix 消息,header 记录实际使用的确切结果。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。 +`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型或采样参数。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。 -在协议格式上,循环构建的请求按此顺序读取:`system` 槽位(渲染后的提示词组装)→ `messagePrefix`(冻结的会话前缀)→ 派生历史——边界快照,其尾部在轮次首步是最新的 `user/message`,在后续步骤是上一步的工具结果。前缀从不进入派生历史;它的持久记录是 header 事件,开发不变式针对每个循环构建的请求精确重算此等式。 +在协议格式上,循环构建的请求先读取 `system` 槽位(渲染后的提示词组装),再读取派生历史——边界快照,其尾部在轮次首步是最新的 `user/message`,在后续步骤是上一步的工具结果。开发不变式针对每个循环构建的请求精确重算此等式。 FIXME(call-config-shape):重新审视此类型的精确定义——出于缓存目的,哪些字段确实属于 epoch 层级(`model` 肯定属于;采样标量目前出于谨慎放在这里),以及适配器需要时,提供方特有的额外项(推理选项、额外 body 参数)应归属何处。 @@ -355,7 +354,7 @@ type SessionEvent = { }[T] ``` -十三种事件变体(`turn/start`、`turn/end`、`step/start`、`step/end`、`user/message`、`prompt/blocked`、`assistant/chunk`、`assistant/message`、`tool/call`、`tool/result`、`steering/message`、`todo/write`、`request/header`)、`deriveMessages()` 投影规则、`TurnTrigger`/`TurnEndReason` 原因以及轮次封闭不变量都在 **[session.md](session.md)** 中。日志如何持久化——`SessionPersistence` seam、JSONL/SQLite 后端、`session/flush` 检查点、崩溃恢复与 `SessionHeader`——则在 **[persistence.md](persistence.md)** 中。 +十二种事件变体(`turn/start`、`turn/end`、`step/start`、`step/end`、`user/message`、`assistant/chunk`、`assistant/message`、`tool/call`、`tool/result`、`steering/message`、`todo/write`、`request/header`)、`deriveMessages()` 投影规则、`TurnTrigger`/`TurnEndReason` 原因以及轮次封闭不变量都在 **[session.md](session.md)** 中。日志如何持久化——`SessionPersistence` seam、JSONL/SQLite 后端、`session/flush` 检查点、崩溃恢复与 `SessionHeader`——则在 **[persistence.md](persistence.md)** 中。 ## Agent 句柄 @@ -380,8 +379,7 @@ type SendTarget = 'next-turn' | 'next-step' * (`next-turn`/wakeup), {@link Agent.steer} (`next-step`/wakeup), and * {@link Agent.inject} (`next-step`/no-wakeup). * - * The object is complete so routing and provenance are explicit; callers that - * want the ordinary user-message preset use {@link Agent.followup}. + * The object is complete so routing policy is explicit. */ interface SendOptions { /** Queue the item joins. */ @@ -394,20 +392,10 @@ interface SendOptions { * (the injection preset). */ wakeup: boolean - /** Producer provenance; direct human input uses `{ kind: 'user' }`. */ - source: MessageSource } ``` -固定预设别名拥有 `target` 和 `wakeup`,因此只接受其余字段: - -```ts type-equiv -/** Options accepted by the fixed-preset aliases, which own `target` and `wakeup`. */ -interface AliasSendOptions { - /** Producer provenance; each alias supplies its documented default when omitted. */ - source?: MessageSource -} -``` +固定预设别名拥有 `target` 与 `wakeup`;它们的 `UserMessageData` 输入同时携带内容与来源。 `send` 返回被接受消息的不透明 `AgentMessageId`,并在该消息的 `agent/inbox/*` 事件中保持稳定: @@ -425,8 +413,8 @@ type AgentMessageId = Branded<'AgentMessageId'> /** * One accepted {@link Agent.send} message, carried by the `agent/inbox/*` live * events. `id` is the value `send` returned to the caller, stable across this - * message's enqueue, dequeue, and discard events. Source defaults are already - * applied, so these are the exact values the item was accepted with. + * message's enqueue, dequeue, and discard events. Its content and source are + * the exact input values accepted by the agent. */ interface AgentMessage extends UserMessageData { /** The id `send` returned for this message. */ @@ -482,11 +470,11 @@ interface Agent { * without running the model: an open turn stages it for the next safe log * position, while an idle injection appends it immediately without opening * a turn. - * @param content - the model-facing content blocks to deliver. - * @param options - target queue, wakeup decision, and source. + * @param input - model-facing content and its producer provenance. + * @param options - target queue and wakeup decision. * @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events. */ - send(content: ContentBlock[], options: SendOptions): AgentMessageId + send(input: UserMessageData, options: SendOptions): AgentMessageId /** * Clear queued and steering work — unless `keepInbox` — and abort the active @@ -506,11 +494,10 @@ interface Agent { * Queue an ordinary follow-up turn and wake the driver — the * `next-turn`/wakeup preset of {@link send}. The item becomes the sole * ordinary message of its own turn. - * @param content - the prompt content blocks. - * @param options - message source. + * @param input - prompt content and its producer provenance. * @returns the accepted message's {@link AgentMessageId}. */ - followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId + followup(input: UserMessageData): AgentMessageId /** * Submit steering into the running turn — the `next-step`/wakeup preset of @@ -519,23 +506,20 @@ interface Agent { * remainder stays staged without waking the agent; retry or a later prompt * takes it. Idle steering falls back to a woken follow-up turn, while * cancellation or disposal may discard pending steering. - * @param content - the steering content blocks. - * @param options - message source. + * @param input - steering content and its producer provenance. * @returns the accepted message's {@link AgentMessageId}. */ - steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId + steer(input: UserMessageData): AgentMessageId /** * Append model-facing context without running the model — the * `next-step`/no-wakeup preset of {@link send}. An open-turn injection stages * at the next safe log position; an idle injection appends immediately - * without opening a turn. An omitted source defaults to - * `{ kind: 'plugin', plugin: '' }`. - * @param content - the injected context content blocks. - * @param options - context source. + * without opening a turn. + * @param input - injected context and its producer provenance. * @returns the accepted message's {@link AgentMessageId}. */ - inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId + inject(input: UserMessageData): AgentMessageId /** * Re-open a turn on the current session log without a new prompt — the @@ -560,15 +544,10 @@ cause 是必选且由 TypeScript 强制约束的同进程输入。活跃持有 ## 拦截决策 -提示词决策与工具后决策共享 `AdditionalContext`,它与持久用户角色输入使用相同的 `UserMessageData` content/source 形状。每个 `additionalContexts` 项都会成为一条单独的 `user/message`,并保留其来源信息。钩子桥接层会把原生决策字段映射到这些类型化结果。 +提示词决策与工具后决策直接使用 `UserMessageData`,即持久用户角色输入使用的 content/source 形状。每个 `additionalContexts` 项都会成为一条单独的 `user/message`,并保留其来源信息。钩子桥接层会把原生决策字段映射到这些类型化结果。 源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) -```ts type-equiv -/** Additional model-facing context produced beside a prompt or tool result. */ -type AdditionalContext = UserMessageData -``` - `agent/prompt-submit` 在轮次打开前返回 `PromptDecision`。`allow` 可以改写已领取的提示词或附加 `additionalContexts`;`block` 会拒绝接纳,且不创建轮次事件: ```ts type-equiv @@ -579,7 +558,7 @@ type AdditionalContext = UserMessageData * `next()` preserves both fields unless it intentionally replaces them. */ type PromptDecision = - | { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: AdditionalContext[] } + | { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: UserMessageData[] } | { kind: 'block'; reason: string } ``` diff --git a/docs/core-data-structures/llm-streaming.md b/docs/core-data-structures/llm-streaming.md index dbd907d335..9bc54079e2 100644 --- a/docs/core-data-structures/llm-streaming.md +++ b/docs/core-data-structures/llm-streaming.md @@ -62,7 +62,7 @@ Every adapter MUST obey these, and every consumer may rely on them: - **Provider stalls are bounded at the transport.** Both shipping remote adapters expose positive finite `streamIdleTimeoutMs` with a five-minute default. The watchdog arms only while iterator `next()` is outstanding, uses one stable signal for the whole request, maps its own expiry to `TIMEOUT`, and keeps an earlier caller abort as `ABORTED`. - **Context overflow has one canonical code.** Both DeepSeek adapters classify explicit provider detail through `isContextWindowExceededError()` and surface `CONTEXT_WINDOW_EXCEEDED`, whether the failure arrives as a thrown HTTP `LlmError` or an in-band finish error. Consumers route on the code, never provider text. - **Every provider HTTP request carries the app-attribution header.** Adapters send `attributionHeaders()` (below) - the `User-Agent` baseline - and prove it with a wire-level test (mock server asserting the received header, or the library's header hook for a library-backed adapter). -- **Replay state is adapter-owned.** A successful `finish` may carry lossless-JSON state needed to reconstruct a native provider response. The loop stores it with the assembled assistant message unless an `agent/step-result` listener rewrote the content. On a later request, `LlmService` passes the state only when the historical provider and target provider are currently registered to the exact same adapter instance. That adapter validates the state and owns any cross-model or cross-provider conversion; other adapters receive the provider-neutral content and provenance without the private state. +- **Replay state is adapter-owned.** A successful `finish` may carry lossless-JSON state needed to reconstruct a native provider response. The loop stores it with the assembled assistant message. On a later request, `LlmService` passes the state only when the historical provider and target provider are currently registered to the exact same adapter instance. That adapter validates the state and owns any cross-model or cross-provider conversion; other adapters receive the provider-neutral content and provenance without the private state. This contract is pinned down by two deliberately independent implementations: `dsh-llm-deepseek` (hand-rolled fetch/SSE) and `dsh-llm-pi-ai` (a generic multi-provider adapter through `@earendil-works/pi-ai`). The library-backed adapter exercises the finish-chunk error path, while transport-boundary tests prove each idle watchdog stops its actual request. diff --git a/docs/core-data-structures/session-reference.md b/docs/core-data-structures/session-reference.md index c33704c14f..b46b5a30be 100644 --- a/docs/core-data-structures/session-reference.md +++ b/docs/core-data-structures/session-reference.md @@ -44,7 +44,7 @@ interface PreparedReferencedMessage { /** Readable message content after host mention tokens are removed. */ content: ContentBlock[] /** Aggregated untrusted snapshot, absent when the message has no references. */ - additionalContext?: AdditionalContext + additionalContext?: UserMessageData } ``` diff --git a/docs/core-data-structures/session.i18n.yaml b/docs/core-data-structures/session.i18n.yaml index a998473448..5ece9d987a 100644 --- a/docs/core-data-structures/session.i18n.yaml +++ b/docs/core-data-structures/session.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -session.md: cd646d1d5913b0850bb9e1596a5f5f9795d74e4b -session.zh.md: cf29447ecfd2383193a5185fbe78891253d48be0 +session.md: b46206a84103fa262b2b62b0e18654b9eb7e7cb0 +session.zh.md: b89597cd01af7e5178a63b675c20551e47ca7621 diff --git a/docs/core-data-structures/session.md b/docs/core-data-structures/session.md index cd646d1d59..b46206a841 100644 --- a/docs/core-data-structures/session.md +++ b/docs/core-data-structures/session.md @@ -58,11 +58,6 @@ interface SessionEventMap { * injection may append this event between turns without running the model. */ 'user/message': UserMessageData - /** - * Durable record of a prompt veto and its reason. It is log-only: the blocked - * prompt never enters the model-visible surface, and its turn runs zero steps. - */ - 'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string } /** Raw stream chunk — token-level replay fidelity. */ 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk } /** @@ -152,13 +147,13 @@ interface TodoItem { ### The request header event: `request/header` -The request envelope — the `EpochHeader` (call config + rendered system prompt + assembled tool schemas + the session prefix) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message. +The request envelope — the `EpochHeader` (call config + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message. ```ts type-equiv /** - * Logged request state outside derived history: call config, system prompt, - * tools, and prefix. The latest full `request/header` snapshot reconstructs it; - * canonical empty optional fields are absent. + * Logged request state outside derived history: call config, system prompt, and + * tools. The latest full `request/header` snapshot reconstructs it; canonical + * empty optional fields are absent. */ interface EpochHeader { /** The conversation's call configuration (provider, model, and sampling scalars). */ @@ -167,18 +162,10 @@ interface EpochHeader { system?: string /** Assembled tool schemas; absent for a tool-less request. */ tools?: ToolSchema[] - /** - * The session prefix: request-only messages sent BEFORE the entire derived - * history (the `agent/session-prefix` waterfall's product, composed once - * per loop instance and reused for every request it sends). Not session - * history — `deriveMessages()` never returns it — so the header is its - * only durable record; absent when the instance composed none. - */ - messagePrefix?: Message[] } ``` -Canonical form: an empty system prompt, an empty tool list, and an empty session prefix are absent fields, matching how requests are built. `messagePrefix` is the durable record of the `agent/session-prefix` waterfall's product (the request is `messagePrefix + derived history`); it is composed once per loop instance and included in every full snapshot that instance records. Legacy v0 logs containing the removed `request/header-delta` event or its full-snapshot `fallback` reason are rejected at seed, append, and persistence-load boundaries rather than replayed incompletely. +Canonical form represents an empty system prompt or tool list as an absent field, matching how requests are built. Legacy v0 logs containing the removed `request/header-delta` event or its full-snapshot `fallback` reason are rejected at seed, append, and persistence-load boundaries rather than replayed incompletely. ## `SessionEvent` — one log entry @@ -518,11 +505,6 @@ interface TurnEndReasonMap { disposed: { kind: 'disposed' } /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */ 'max-tokens': { kind: 'max-tokens' } - /** - * Policy blocked the turn's claimed prompt before the first step. The - * zero-step turn still records a balanced durable boundary and veto reason. - */ - rejected: { kind: 'rejected'; reason: string } /** * A persistence backend closed a crash-orphaned turn on reload. The loop never * emits this marker, and the events recorded before the crash remain intact. @@ -531,7 +513,7 @@ interface TurnEndReasonMap { } ``` -`max-tokens` mirrors the model-call `FinishReason` of the same name: any `max-tokens` step in a turn makes the whole turn end `max-tokens` rather than `completed` (the cut-short fact wins over a later continuation), so a consumer can tell a clean stop from a truncated one — but only over `completed`: the `disposed`/`aborted`/`error` outcomes take precedence. `rejected` is a zero-step turn whose claimed prompt an `agent/prompt-submit` hook blocked (the ACP bridge maps it to `cancelled`). `interrupted` is the one reason no loop emits — it is synthesized by crash recovery (see [persistence.md](persistence.md)). Both maps are merge-extensible. +`max-tokens` mirrors the model-call `FinishReason` of the same name: any `max-tokens` step in a turn makes the whole turn end `max-tokens` rather than `completed` (the cut-short fact wins over a later continuation), so a consumer can tell a clean stop from a truncated one — but only over `completed`: the `disposed`/`aborted`/`error` outcomes take precedence. `interrupted` is the one reason no loop emits — it is synthesized by crash recovery (see [persistence.md](persistence.md)). Both maps are merge-extensible. ## The turn-enclosure invariant @@ -541,7 +523,7 @@ Every session event lives **inside** a turn (between a `turn/start` and its `tur A plugin may declaration-merge extra `SessionEventMap` types. These are **log-only**: NOT `SurfaceEventType`s (they carry no `surfaceOp` and contribute nothing to derived history), but, like every event, they must sit inside an open turn. The full per-event enumeration — core and plugin-contributed alike, with payloads and provenance — is the generated [persistence log event catalog](../persistence-catalog.md); the compaction seam's `compact/*` semantics are discussed on [compaction.md](compaction.md). -The hook bridges' `hook/invoked` / `hook/result` provenance pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. The mid-turn hook points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record — its injected `user/message` is the durable evidence — because it has no open turn to enclose one (see [the hook-bridges Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md)). +The hook bridges' `hook/invoked` / `hook/result` provenance pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. The mid-turn hook points (`PreToolUse`/`PostToolUse`/`Stop`) fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` and the pre-turn `UserPromptSubmit` admission seam get no `hook/*` record because neither has an open turn to enclose one; allowed context is instead evidenced by its sourced `user/message` (see [the hook-bridges Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md)). ## Durability contract diff --git a/docs/core-data-structures/session.zh.md b/docs/core-data-structures/session.zh.md index cf29447ecf..b89597cd01 100644 --- a/docs/core-data-structures/session.zh.md +++ b/docs/core-data-structures/session.zh.md @@ -58,11 +58,6 @@ interface SessionEventMap { * injection may append this event between turns without running the model. */ 'user/message': UserMessageData - /** - * Durable record of a prompt veto and its reason. It is log-only: the blocked - * prompt never enters the model-visible surface, and its turn runs zero steps. - */ - 'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string } /** Raw stream chunk — token-level replay fidelity. */ 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk } /** @@ -152,13 +147,13 @@ interface TodoItem { ### 请求头事件:`request/header` -请求信封(即 `EpochHeader`:调用配置 + 渲染后的系统提示词 + 已组装的工具 schema + 会话前缀)会作为会话状态写入日志,因此每个对话请求都是日志的纯函数(见可重建性 Agent Note)。带有 reason `'initial'` 或 `'resume'` 的完整 `request/header` 快照记录每个 agent loop 实例的边界;之后请求发生变化时,系统会以 reason `'change'` 记录另一份完整快照。`foldRequestHeader(events)` 通过选择最新快照重建请求头。该事件不是 `SurfaceEventType`,不产生 LLM 消息。 +请求信封(即 `EpochHeader`:调用配置 + 渲染后的系统提示词 + 已组装的工具 schema)会作为会话状态写入日志,因此每个对话请求都是日志的纯函数(见可重建性 Agent Note)。带有 reason `'initial'` 或 `'resume'` 的完整 `request/header` 快照记录每个 agent loop 实例的边界;之后请求发生变化时,系统会以 reason `'change'` 记录另一份完整快照。`foldRequestHeader(events)` 通过选择最新快照重建请求头。该事件不是 `SurfaceEventType`,不产生 LLM 消息。 ```ts type-equiv /** - * Logged request state outside derived history: call config, system prompt, - * tools, and prefix. The latest full `request/header` snapshot reconstructs it; - * canonical empty optional fields are absent. + * Logged request state outside derived history: call config, system prompt, and + * tools. The latest full `request/header` snapshot reconstructs it; canonical + * empty optional fields are absent. */ interface EpochHeader { /** The conversation's call configuration (provider, model, and sampling scalars). */ @@ -167,18 +162,10 @@ interface EpochHeader { system?: string /** Assembled tool schemas; absent for a tool-less request. */ tools?: ToolSchema[] - /** - * The session prefix: request-only messages sent BEFORE the entire derived - * history (the `agent/session-prefix` waterfall's product, composed once - * per loop instance and reused for every request it sends). Not session - * history — `deriveMessages()` never returns it — so the header is its - * only durable record; absent when the instance composed none. - */ - messagePrefix?: Message[] } ``` -规范形式:空系统提示词、空工具列表和空会话前缀都表示为字段缺失,与请求构建方式一致。`messagePrefix` 是 `agent/session-prefix` waterfall(瀑布式事件)产物的持久记录(请求 = `messagePrefix + derived history`);每个 agent loop 实例只组合一次,并包含在该实例记录的每份完整快照中。包含已移除的 `request/header-delta` 事件或完整快照原因为 `fallback` 的旧版 v0 日志,会在 seed、append 和持久化加载边界被拒绝,而不会以不完整方式回放。 +规范形式将空系统提示词和空工具列表表示为字段缺失,与请求构建方式一致。包含已移除的 `request/header-delta` 事件或完整快照原因为 `fallback` 的旧版 v0 日志,会在 seed、append 和持久化加载边界被拒绝,而不会以不完整方式回放。 ## `SessionEvent`:一条日志条目 @@ -518,11 +505,6 @@ interface TurnEndReasonMap { disposed: { kind: 'disposed' } /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */ 'max-tokens': { kind: 'max-tokens' } - /** - * Policy blocked the turn's claimed prompt before the first step. The - * zero-step turn still records a balanced durable boundary and veto reason. - */ - rejected: { kind: 'rejected'; reason: string } /** * A persistence backend closed a crash-orphaned turn on reload. The loop never * emits this marker, and the events recorded before the crash remain intact. @@ -531,7 +513,7 @@ interface TurnEndReasonMap { } ``` -`max-tokens` 与模型调用中同名的 `FinishReason` 对应:只要轮次内有任何步骤以 `max-tokens` 结束,整个轮次就以 `max-tokens` 而不是 `completed` 结束(即使之后继续执行,截断事实仍优先),让消费方能够区分正常停止和截断停止;但它只优先于 `completed`,`disposed`/`aborted`/`error` 结果的优先级更高。`rejected` 表示一个零步骤轮次,其已认领的提示词被 `agent/prompt-submit` 钩子阻止(ACP(Agent Client Protocol)桥接层将其映射为 `cancelled`)。`interrupted` 是唯一不会由任何 loop 发出的原因:它由崩溃恢复合成(见 [persistence.md](persistence.md))。两个 map 均可通过合并扩展。 +`max-tokens` 与模型调用中同名的 `FinishReason` 对应:只要轮次内有任何步骤以 `max-tokens` 结束,整个轮次就以 `max-tokens` 而不是 `completed` 结束(即使之后继续执行,截断事实仍优先),让消费方能够区分正常停止和截断停止;但它只优先于 `completed`,`disposed`/`aborted`/`error` 结果的优先级更高。`interrupted` 是唯一不会由任何 loop 发出的原因:它由崩溃恢复合成(见 [persistence.md](persistence.md))。两个 map 均可通过合并扩展。 ## 轮次封闭不变式 @@ -541,7 +523,7 @@ interface TurnEndReasonMap { 插件可以通过 declaration merging 添加额外的 `SessionEventMap` 类型。这些是**仅日志**事件:不是 `SurfaceEventType`(不携带 `surfaceOp`,不参与派生历史),但与所有事件一样,必须位于一个打开的轮次内。完整的逐事件枚举(核心与插件贡献的,含 payload 与溯源信息)见生成的[持久化日志事件目录](../persistence-catalog.md);压缩 seam 的 `compact/*` 语义在 [compaction.md](compaction.md) 中讨论。 -钩子桥接层的 `hook/invoked` / `hook/result` 溯源对(来自 `@deepseek-ai/dsh-hook-protocol`)通过 `handlerId` 关联。轮次中间的钩子点(`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`)在 loop 已打开的轮次内触发,因此其 `hook/*` 记录天然位于轮次之内。`SessionStart` 不生成 `hook/*` 记录:它注入的 `user/message` 已是持久证据,而且当时没有已打开的轮次可容纳该记录(见[钩子桥接 Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md))。 +钩子桥接层的 `hook/invoked` / `hook/result` 溯源对(来自 `@deepseek-ai/dsh-hook-protocol`)通过 `handlerId` 关联。轮次中间的钩子点(`PreToolUse`/`PostToolUse`/`Stop`)在 loop 已打开的轮次内触发,因此其 `hook/*` 记录天然位于轮次之内。`SessionStart` 与轮次前的 `UserPromptSubmit` 接纳 seam 都不生成 `hook/*` 记录,因为当时没有已打开的轮次可容纳记录;获准附加的上下文则由带来源的 `user/message` 作为持久证据(见[钩子桥接 Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md))。 ## 持久性契约 diff --git a/docs/core-data-structures/skills.md b/docs/core-data-structures/skills.md index e367adf006..cf9dab5022 100644 --- a/docs/core-data-structures/skills.md +++ b/docs/core-data-structures/skills.md @@ -152,6 +152,6 @@ interface Config { ## Session catalog and tool contract -`dsh-tool-skill` contributes a user-role `` through `agent/session-prefix`. The catalog contains sorted skill `name` and normalized, XML-escaped `description` only; it omits bodies, paths, sources, providers, and routing hints. Prefix discovery forwards the caller's abort signal through `SkillLookupOptions`. `catalogDescriptionMaxLength` is the consumer config for the description bound, with default `500` and integer minimum `3`. Its request-only, header-logged lifecycle is defined by the [session-prefix Agent Note](../../.agents/notes/implemented/feature/2026-07-07-session-prefix.md). +`dsh-tool-skill` injects a durable user-role `` at the first `agent/step` of a live session. The catalog contains sorted skill `name` and normalized, XML-escaped `description` only; it omits bodies, paths, sources, providers, and routing hints. Discovery forwards the step's abort signal through `SkillLookupOptions`. `catalogDescriptionMaxLength` is the consumer config for the description bound, with default `500` and integer minimum `3`. The model-facing `skill({ name })` tool validates the kebab-case name, loads the complete definition for the calling agent cwd, reports an unresolved skill as unknown or no longer available, rejects `disableModelInvocation` skills, and returns a tool result containing ``, ``, and ``. `resourceBase` resolves explicitly referenced scripts, references, and assets only as needed; the loaded result does not enumerate a skill directory. The tool result is the model-visible path for complete instructions. diff --git a/docs/core-data-structures/tools.md b/docs/core-data-structures/tools.md index b48fe1ef40..af54aa6a82 100644 --- a/docs/core-data-structures/tools.md +++ b/docs/core-data-structures/tools.md @@ -213,7 +213,7 @@ interface ToolRunContext extends ToolExecution { * the agent loop. Contexts retain their individual source and metadata and * are emitted in call order. */ - deferContext(context: AdditionalContext): void + deferContext(context: UserMessageData): void /** Mark a successful final result as terminal for the current agent turn. */ concludeTurn(): void } @@ -292,7 +292,7 @@ interface ToolExecutionSuccess { readonly content: ContentBlock[] readonly error?: never readonly meta?: JsonValue - readonly additionalContexts?: AdditionalContext[] + readonly additionalContexts?: UserMessageData[] /** The agent loop stops after committing this successful result batch. */ readonly concludesTurn?: true } @@ -306,7 +306,7 @@ interface ToolExecutionFailure { readonly value?: never readonly content: ContentBlock[] readonly meta?: JsonValue - readonly additionalContexts?: AdditionalContext[] + readonly additionalContexts?: UserMessageData[] readonly concludesTurn?: never } ``` @@ -343,9 +343,9 @@ type PreToolDecision = * next request, or block by turning corrective feedback into an error result. */ type PostToolDecision = - | { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: AdditionalContext[] } - | { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: AdditionalContext[] } - | { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: AdditionalContext[] } + | { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: UserMessageData[] } + | { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: UserMessageData[] } + | { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: UserMessageData[] } ``` Call `next()` for the default or return a decision to short-circuit. Pre-policy may deny or ask; only `allowed-once` proceeds, while a non-grant, missing approval channel or service, or agent-less request becomes a denial. Guards may still impose a final denial. Arguments cannot be rewritten because history, audit, UI, and execution must agree. diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 5d973b85ca..b5a7398db3 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -8,21 +8,21 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:140`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`tui`](../packages/ui/tui) | -| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:310`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal-session`](../packages/goal/goal-session) | -| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:252`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:261`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | -| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:418`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) | -| `agent/idle` | `emit` | [`packages/core/agent/src/types.ts:405`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:288`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`tui`](../packages/ui/tui) | -| `agent/inbox/discard` | `emit` | [`packages/core/agent/src/types.ts:300`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`tui`](../packages/ui/tui) | -| `agent/inbox/enqueue` | `emit` | [`packages/core/agent/src/types.ts:278`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session) | -| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:337`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | -| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:363`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:378`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:323`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | -| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:270`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) | -| `agent/step` | `serial` | [`packages/core/agent/src/types.ts:350`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`acp`](../packages/ui/acp), [`compact-basic`](../packages/compact/compact-basic), [`plan-mode`](../packages/plan/plan-mode), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`time-context`](../packages/context/time-context), [`tool-skill`](../packages/skill/tool-skill), [`user-approval`](../packages/ui/user-approval), [`workspace-context`](../packages/context/workspace-context) | -| `agent/stopping` | `serial` | [`packages/core/agent/src/types.ts:393`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | +| `agent/cancel-requested` | `emit` | [`packages/core/agent/src/types.ts:294`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal-session`](../packages/goal/goal-session) | +| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:236`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:245`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | +| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:403`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) | +| `agent/idle` | `emit` | [`packages/core/agent/src/types.ts:390`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/inbox/dequeue` | `emit` | [`packages/core/agent/src/types.ts:272`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`tui`](../packages/ui/tui) | +| `agent/inbox/discard` | `emit` | [`packages/core/agent/src/types.ts:284`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`tui`](../packages/ui/tui) | +| `agent/inbox/enqueue` | `emit` | [`packages/core/agent/src/types.ts:262`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session) | +| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:322`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | +| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:348`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:363`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:307`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | +| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:254`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) | +| `agent/step` | `serial` | [`packages/core/agent/src/types.ts:335`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`acp`](../packages/ui/acp), [`compact-basic`](../packages/compact/compact-basic), [`plan-mode`](../packages/plan/plan-mode), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`time-context`](../packages/context/time-context), [`tool-skill`](../packages/skill/tool-skill), [`user-approval`](../packages/ui/user-approval), [`workspace-context`](../packages/context/workspace-context) | +| `agent/stopping` | `serial` | [`packages/core/agent/src/types.ts:378`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | | `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:30`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/ui/acp) | | `commands/change` | `emit` | [`packages/ui/commands/src/index.ts:103`](../packages/ui/commands/src/index.ts) | [`commands`](../packages/ui/commands) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`tui`](../packages/ui/tui) | | `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:62`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 77c3e33ed5..14a6ace6b1 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -78,7 +78,7 @@ export type SessionEvent = { }[T] ``` -Sources: [`packages/core/session/src/types.ts:288`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:301`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:330`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:362`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:270`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:283`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:312`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:344`](../packages/core/session/src/types.ts) ## Events @@ -150,7 +150,7 @@ Source: [`packages/ui/user-approval/src/index.ts:67`](../packages/ui/user-approv Types: [StreamChunk](core-data-structures/llm-streaming.md) -Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:216`](../packages/core/session/src/types.ts) #### `assistant/message` — surface @@ -166,7 +166,7 @@ Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/ Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md) -Source: [`packages/core/session/src/types.ts:241`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:223`](../packages/core/session/src/types.ts) ### `compact/*` @@ -315,22 +315,6 @@ Source: [`packages/ui/permission/src/index.ts:36`](../packages/ui/permission/src Source: [`packages/plan/plan-mode/src/index.ts:41`](../packages/plan/plan-mode/src/index.ts) -### `prompt/*` - -#### `prompt/blocked` — log-only - -```ts persistence-catalog -/** - * Durable record of a prompt veto and its reason. It is log-only: the blocked - * prompt never enters the model-visible surface, and its turn runs zero steps. - */ -'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string } -``` - -Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md) - -Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts) - ### `request/*` #### `request/header` — log-only @@ -343,7 +327,7 @@ Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/ 'request/header': { header: EpochHeader; reason: RequestHeaderReason } ``` -Source: [`packages/core/session/src/types.ts:276`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:258`](../packages/core/session/src/types.ts) ### `sandbox/*` @@ -399,7 +383,7 @@ Source: [`packages/session-title/session-title-llm/src/index.ts:44`](../packages 'steering/message': UserMessageData & { turn: number } ``` -Source: [`packages/core/session/src/types.ts:269`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts) ### `step/*` @@ -410,7 +394,7 @@ Source: [`packages/core/session/src/types.ts:269`](../packages/core/session/src/ 'step/end': { turn: number; step: number } ``` -Source: [`packages/core/session/src/types.ts:218`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:205`](../packages/core/session/src/types.ts) #### `step/start` — log-only @@ -419,7 +403,7 @@ Source: [`packages/core/session/src/types.ts:218`](../packages/core/session/src/ 'step/start': { turn: number; step: number } ``` -Source: [`packages/core/session/src/types.ts:216`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:203`](../packages/core/session/src/types.ts) ### `todo/*` @@ -432,7 +416,7 @@ Source: [`packages/core/session/src/types.ts:216`](../packages/core/session/src/ Types: [TodoItem](core-data-structures/session.md) -Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:253`](../packages/core/session/src/types.ts) ### `tool/*` @@ -449,7 +433,7 @@ Source: [`packages/core/session/src/types.ts:271`](../packages/core/session/src/ Types: [CallId](core-data-structures/core.md) -Source: [`packages/core/session/src/types.ts:247`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:229`](../packages/core/session/src/types.ts) #### `tool/code-dispatch` — log-only @@ -503,7 +487,7 @@ Source: [`packages/core/tools/src/code-mode.ts:34`](../packages/core/tools/src/c Types: [CallId](core-data-structures/core.md) · [ContentBlock](core-data-structures/core.md) -Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:241`](../packages/core/session/src/types.ts) ### `turn/*` @@ -521,7 +505,7 @@ Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/ Types: [TurnEndReason](core-data-structures/session.md) -Source: [`packages/core/session/src/types.ts:214`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/types.ts) #### `turn/start` — log-only @@ -534,7 +518,7 @@ Source: [`packages/core/session/src/types.ts:214`](../packages/core/session/src/ Types: [TurnTrigger](core-data-structures/session.md) -Source: [`packages/core/session/src/types.ts:207`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:194`](../packages/core/session/src/types.ts) ### `user/*` @@ -552,4 +536,4 @@ Source: [`packages/core/session/src/types.ts:207`](../packages/core/session/src/ 'user/message': UserMessageData ``` -Source: [`packages/core/session/src/types.ts:227`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:214`](../packages/core/session/src/types.ts) diff --git a/docs/user/develop/framework/events.i18n.yaml b/docs/user/develop/framework/events.i18n.yaml index 9704eff7c5..b20764a24d 100644 --- a/docs/user/develop/framework/events.i18n.yaml +++ b/docs/user/develop/framework/events.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -events.md: 0c57681a55ea0200fe8f33293176fc94f09a4ce5 -events.zh.md: 3e14739d4a97ba014d545c9f226000507aaeacef +events.md: 5cd5d22f854d0b4e271e892cbdb1ccebe687ae49 +events.zh.md: 5fd4d5de53897e32523ab478626965ad7c9602ba diff --git a/docs/user/develop/framework/events.md b/docs/user/develop/framework/events.md index 0c57681a55..5cd5d22f85 100644 --- a/docs/user/develop/framework/events.md +++ b/docs/user/develop/framework/events.md @@ -101,7 +101,7 @@ declare module 'cordis' { ## Cordis events and session records -Harness Cordis events use `namespace/action` names, including `agent/pre-step`, `agent/request`, `agent/step-result`, `tools/result`, and `session/event`. The generated [event catalog](../../../cordis-catalog/events.md) records complete signatures and modes. +Harness Cordis events use `namespace/action` names, including `agent/step`, `agent/request`, `agent/request-error`, `tools/result`, and `session/event`. The generated [event catalog](../../../cordis-catalog/events.md) records complete signatures and modes. `turn/*`, `step/*`, `tool/call`, `tool/result`, and `compact/*` are durable session-event types, not same-named Cordis events. To observe them, listen to `session/event` and inspect `event.type`. diff --git a/docs/user/develop/framework/events.zh.md b/docs/user/develop/framework/events.zh.md index 3e14739d4a..5fd4d5de53 100644 --- a/docs/user/develop/framework/events.zh.md +++ b/docs/user/develop/framework/events.zh.md @@ -101,7 +101,7 @@ declare module 'cordis' { ## Cordis 事件与会话记录 -Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-step`、`agent/request`、`agent/step-result`、`tools/result` 和 `session/event`。完整签名与触发模式见[Events 目录](../../../cordis-catalog/events.md)。 +Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/step`、`agent/request`、`agent/request-error`、`tools/result` 和 `session/event`。完整签名与触发模式见[Events 目录](../../../cordis-catalog/events.md)。 `turn/*`、`step/*`、`tool/call`、`tool/result` 和 `compact/*` 是持久化的会话事件类型,不是同名 Cordis 事件。需要观察它们时,监听 `session/event` 并检查 `event.type`。 diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts index 7d014991ed..c0a3cf5db8 100644 --- a/examples/acp-agent/tests/acp.snapshot.ts +++ b/examples/acp-agent/tests/acp.snapshot.ts @@ -195,10 +195,10 @@ const SCENARIOS: Scenario[] = [ headerClass: 'advanced', configPath: ADVANCED_CONFIG, }, - // Prompt-submit blocks are authored keylessly: they persist a rejected turn - // and hook events without starting a model step, so their logs still compare. - { name: 'hook-cc-promptsubmit-block', hasModelTurn: false, comparesLog: true, recorded: false }, - { name: 'hook-codex-promptsubmit-block', hasModelTurn: false, comparesLog: true, recorded: false }, + // Prompt-submit blocks are authored keylessly. Admission rejects before a + // turn opens, so only the ACP stop reason is observable and no log is harvested. + { name: 'hook-cc-promptsubmit-block', hasModelTurn: false, recorded: false }, + { name: 'hook-codex-promptsubmit-block', hasModelTurn: false, recorded: false }, // The mid-turn seams fire during a real model turn, so each is recorded with its hook active // (the model's reaction to a deny/block/force-continue is part of the captured transcript). // SessionStart/SubagentStart are excluded because detached injection races log diff --git a/examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl b/examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl index d598b34e37..efeb40d2ab 100644 --- a/examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl +++ b/examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl @@ -12,7 +12,7 @@ {"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_goal_create","name":"create_goal","arguments":"{\"objective\":\"Finish the ACP goal-session snapshot proof\",\"max_goal_rounds\":2}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":20,"outputTokens":8}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} {"type":"tool/call","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"call_goal_create","name":"create_goal","arguments":"{\"objective\":\"Finish the ACP goal-session snapshot proof\",\"max_goal_rounds\":2}"}} {"type":"tool/result","seq":12,"time":0,"data":{"turn":1,"step":1,"callId":"call_goal_create","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the ACP goal-session snapshot proof\",\"phase\":\"active\",\"roundsStarted\":0,\"maxGoalRounds\":2},\"activation\":\"armed\"}"}],"isError":false},"sourceEventSeqs":[11],"surfaceOp":"append"} -{"type":"user/message","seq":13,"time":0,"data":{"content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the ACP goal-session snapshot proof\",\"phase\":\"active\",\"maxGoalRounds\":2},\"roundsStarted\":0,\"createdAt\":0,\"updatedAt\":0}"}],"source":{"kind":"goal","goalId":"goal-{{sessionId}}","revision":1,"round":0},"meta":{"kind":"goal/change","version":1,"operation":"create","goal":{"id":"goal-{{sessionId}}","revision":1,"objective":"Finish the ACP goal-session snapshot proof","phase":"active","maxGoalRounds":2},"roundsStarted":0,"createdAt":0,"updatedAt":0}},"surfaceOp":"append"} +{"type":"user/message","seq":13,"time":0,"data":{"content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the ACP goal-session snapshot proof\",\"phase\":\"active\",\"maxGoalRounds\":2},\"roundsStarted\":0,\"createdAt\":0,\"updatedAt\":0}"}],"source":{"kind":"goal","goalId":"goal-{{sessionId}}","revision":1,"round":0,"change":{"kind":"goal/change","version":1,"operation":"create","goal":{"id":"goal-{{sessionId}}","revision":1,"objective":"Finish the ACP goal-session snapshot proof","phase":"active","maxGoalRounds":2},"roundsStarted":0,"createdAt":0,"updatedAt":0}}},"surfaceOp":"append"} {"type":"step/end","seq":14,"time":0,"data":{"turn":1,"step":1}} {"type":"step/start","seq":15,"time":0,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -49,6 +49,6 @@ {"type":"step/start","seq":47,"time":0,"data":{"turn":3,"step":1}} {"type":"assistant/chunk","seq":48,"time":0,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":49,"time":0,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":0,"text":"partial"}}} -{"type":"user/message","seq":50,"time":0,"data":{"content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":2,\"objective\":\"Finish the ACP goal-session snapshot proof\",\"phase\":\"paused\",\"maxGoalRounds\":2},\"roundsStarted\":2,\"createdAt\":0,\"updatedAt\":0}"}],"source":{"kind":"goal","goalId":"goal-{{sessionId}}","revision":2,"round":0},"meta":{"kind":"goal/change","version":1,"operation":"pause","goal":{"id":"goal-{{sessionId}}","revision":2,"objective":"Finish the ACP goal-session snapshot proof","phase":"paused","maxGoalRounds":2},"roundsStarted":2,"createdAt":0,"updatedAt":0}},"surfaceOp":"append"} -{"type":"step/end","seq":51,"time":0,"data":{"turn":3,"step":1}} -{"type":"turn/end","seq":52,"time":0,"data":{"turn":3,"reason":{"kind":"aborted"}}} +{"type":"step/end","seq":50,"time":0,"data":{"turn":3,"step":1}} +{"type":"turn/end","seq":51,"time":0,"data":{"turn":3,"reason":{"kind":"aborted"}}} +{"type":"user/message","seq":52,"time":0,"data":{"content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":2,\"objective\":\"Finish the ACP goal-session snapshot proof\",\"phase\":\"paused\",\"maxGoalRounds\":2},\"roundsStarted\":2,\"createdAt\":0,\"updatedAt\":0}"}],"source":{"kind":"goal","goalId":"goal-{{sessionId}}","revision":2,"round":0,"change":{"kind":"goal/change","version":1,"operation":"pause","goal":{"id":"goal-{{sessionId}}","revision":2,"objective":"Finish the ACP goal-session snapshot proof","phase":"paused","maxGoalRounds":2},"roundsStarted":2,"createdAt":0,"updatedAt":0}}},"surfaceOp":"append"} diff --git a/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl b/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl index eb4488858e..4098356eba 100644 --- a/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl +++ b/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl @@ -11,7 +11,7 @@ {"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":10,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_spill","name":"bash","arguments":"{\"command\":\"node -e \\\"process.stdout.write('SPILL_START-' + 'x'.repeat(2000) + '-SPILL_END')\\\"\",\"description\":\"Print large deterministic output\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} {"type":"tool/call","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"call_spill","name":"bash","arguments":"{\"command\":\"node -e \\\"process.stdout.write('SPILL_START-' + 'x'.repeat(2000) + '-SPILL_END')\\\"\",\"description\":\"Print large deterministic output\"}"}} -{"type":"tool/result","seq":12,"time":0,"data":{"turn":1,"step":1,"callId":"call_spill","content":[{"type":"text","text":"SPILL_START-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-SPILL_END\n\n(Omitted 1417 bytes. Full formatted result stored at: /tmp/dsh-acp-snap-ee77dff02/session-fbfcf2f560a0/1bddd2b64176-bash.txt. Use read with offset/limit, or grep this path to search within it.)"}],"isError":false},"sourceEventSeqs":[11],"surfaceOp":"append"} +{"type":"tool/result","seq":12,"time":0,"data":{"turn":1,"step":1,"callId":"call_spill","content":[{"type":"text","text":"SPILL_START-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-SPILL_END\n\n(Omitted 1417 bytes. Full formatted result stored at: /tmp/dsh-acp-snap-ee77dff02/session-1ac5afb4ed32/999874114a0d-bash.txt. Use read with offset/limit, or grep this path to search within it.)"}],"isError":false},"sourceEventSeqs":[11],"surfaceOp":"append"} {"type":"step/end","seq":13,"time":0,"data":{"turn":1,"step":1}} {"type":"step/start","seq":14,"time":0,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} diff --git a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl index 3f664e8e09..8481b0bab3 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl @@ -2,189 +2,190 @@ {"type":"turn/start","seq":0,"time":1783921765269,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783921765269,"data":{"content":[{"type":"text","text":"Using ONE run_code program, call tools.read on nested/task.txt. After the program finishes, answer the workspace handshake question using the newly discovered instructions: What is the Code Mode workspace handshake?"}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"session/title","seq":2,"time":1783921765269,"data":{"title":"Using ONE run_code program, call","messageSeqs":[1],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":3,"time":1783921765275,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":4,"time":1783921765275,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":[{"role":"user","content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n"}]}]},"reason":"initial"}} -{"type":"assistant/chunk","seq":5,"time":1783921766287,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":6,"time":1783921766287,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":7,"time":1783921766483,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":8,"time":1783921766519,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} -{"type":"assistant/chunk","seq":9,"time":1783921766520,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":10,"time":1783921766520,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":11,"time":1783921766520,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" write"}}} -{"type":"assistant/chunk","seq":12,"time":1783921766537,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} -{"type":"assistant/chunk","seq":13,"time":1783921766538,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} -{"type":"assistant/chunk","seq":14,"time":1783921766538,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} -{"type":"assistant/chunk","seq":15,"time":1783921766573,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_code"}}} -{"type":"assistant/chunk","seq":16,"time":1783921766573,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" program"}}} -{"type":"assistant/chunk","seq":17,"time":1783921766574,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} -{"type":"assistant/chunk","seq":18,"time":1783921766574,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reads"}}} -{"type":"assistant/chunk","seq":19,"time":1783921766598,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} -{"type":"assistant/chunk","seq":20,"time":1783921766598,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} -{"type":"assistant/chunk","seq":21,"time":1783921766599,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" called"}}} -{"type":"assistant/chunk","seq":22,"time":1783921766624,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" nested"}}} -{"type":"assistant/chunk","seq":23,"time":1783921766654,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"/t"}}} -{"type":"assistant/chunk","seq":24,"time":1783921766654,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ask"}}} -{"type":"assistant/chunk","seq":25,"time":1783921766654,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".txt"}}} -{"type":"assistant/chunk","seq":26,"time":1783921766654,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":27,"time":1783921766655,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} -{"type":"assistant/chunk","seq":28,"time":1783921766684,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" answer"}}} -{"type":"assistant/chunk","seq":29,"time":1783921766717,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":30,"time":1783921766717,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" hand"}}} -{"type":"assistant/chunk","seq":31,"time":1783921766717,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"shake"}}} -{"type":"assistant/chunk","seq":32,"time":1783921766743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" question"}}} -{"type":"assistant/chunk","seq":33,"time":1783921766743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" based"}}} -{"type":"assistant/chunk","seq":34,"time":1783921766743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" on"}}} -{"type":"assistant/chunk","seq":35,"time":1783921766743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" what"}}} -{"type":"assistant/chunk","seq":36,"time":1783921766776,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} -{"type":"assistant/chunk","seq":37,"time":1783921766776,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} -{"type":"assistant/chunk","seq":38,"time":1783921766798,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} -{"type":"assistant/chunk","seq":39,"time":1783921766799,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} -{"type":"assistant/chunk","seq":40,"time":1783921766799,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":41,"time":1783921766799,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} -{"type":"assistant/chunk","seq":42,"time":1783921766799,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":43,"time":1783921766799,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" start"}}} -{"type":"assistant/chunk","seq":44,"time":1783921766845,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" by"}}} -{"type":"assistant/chunk","seq":45,"time":1783921766845,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reading"}}} -{"type":"assistant/chunk","seq":46,"time":1783921766845,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":47,"time":1783921766845,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} -{"type":"assistant/chunk","seq":48,"time":1783921766845,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":49,"time":1783921766926,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":50,"time":1783921766926,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":""}}} -{"type":"assistant/chunk","seq":51,"time":1783921766952,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"{"}}} -{"type":"assistant/chunk","seq":52,"time":1783921766952,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":53,"time":1783921766952,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"code"}}} -{"type":"assistant/chunk","seq":54,"time":1783921766952,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":55,"time":1783921766952,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":": "}}} -{"type":"assistant/chunk","seq":56,"time":1783921766978,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":57,"time":1783921766978,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"const"}}} -{"type":"assistant/chunk","seq":58,"time":1783921766978,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" content"}}} -{"type":"assistant/chunk","seq":59,"time":1783921767011,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" ="}}} -{"type":"assistant/chunk","seq":60,"time":1783921767011,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" await"}}} -{"type":"assistant/chunk","seq":61,"time":1783921767011,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" tools"}}} -{"type":"assistant/chunk","seq":62,"time":1783921767011,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":".read"}}} -{"type":"assistant/chunk","seq":63,"time":1783921767011,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"({"}}} -{"type":"assistant/chunk","seq":64,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" file"}}} -{"type":"assistant/chunk","seq":65,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"_path"}}} -{"type":"assistant/chunk","seq":66,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":":"}}} -{"type":"assistant/chunk","seq":67,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" \\\""}}} -{"type":"assistant/chunk","seq":68,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"n"}}} -{"type":"assistant/chunk","seq":69,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"ested"}}} -{"type":"assistant/chunk","seq":70,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"/t"}}} -{"type":"assistant/chunk","seq":71,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"ask"}}} -{"type":"assistant/chunk","seq":72,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":".txt"}}} -{"type":"assistant/chunk","seq":73,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"\\\""}}} -{"type":"assistant/chunk","seq":74,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" });\\n"}}} -{"type":"assistant/chunk","seq":75,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"return"}}} -{"type":"assistant/chunk","seq":76,"time":1783921767095,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" content.lines.map(line => line.text).join(String.fromCharCode(10))"}}} -{"type":"assistant/chunk","seq":77,"time":1783921767095,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":";"}}} -{"type":"assistant/chunk","seq":78,"time":1783921767095,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":79,"time":1783921767121,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"}"}}} -{"type":"assistant/chunk","seq":80,"time":1783921767205,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single run_code program that reads a file called nested/task.txt and then answer the handshake question based on what's in that file. Let me start by reading the file."}}}} -{"type":"assistant/chunk","seq":81,"time":1783921767205,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","arguments":"{\"code\": \"const content = await tools.read({ file_path: \\\"nested/task.txt\\\" });\\nreturn content.lines.map(line => line.text).join(String.fromCharCode(10));\"}"}}}} -{"type":"assistant/chunk","seq":82,"time":1783921767205,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3195,"outputTokens":106,"cacheReadTokens":0,"reasoningTokens":43}}}} -{"type":"assistant/chunk","seq":83,"time":1783921767205,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":84,"time":1783921767208,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to write a single run_code program that reads a file called nested/task.txt and then answer the handshake question based on what's in that file. Let me start by reading the file."},{"type":"tool-call","id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","arguments":"{\"code\": \"const content = await tools.read({ file_path: \\\"nested/task.txt\\\" });\\nreturn content.lines.map(line => line.text).join(String.fromCharCode(10));\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3195,"outputTokens":106,"cacheReadTokens":0,"reasoningTokens":43}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83],"surfaceOp":"append"} -{"type":"tool/call","seq":85,"time":1783921767208,"data":{"turn":1,"step":1,"callId":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","arguments":"{\"code\": \"const content = await tools.read({ file_path: \\\"nested/task.txt\\\" });\\nreturn content.lines.map(line => line.text).join(String.fromCharCode(10));\"}"}} -{"type":"tool/code-dispatch","seq":86,"time":1783921767270,"data":{"parentCallId":"call_00_6APApmaKLRDlXKMdIcWL5139","subCallId":"call_00_6APApmaKLRDlXKMdIcWL5139:code:1","name":"read","arguments":{"file_path":"nested/task.txt"},"isError":false,"resultSummary":"./nested/task.txt\nfile\n\n1: Touch this file to discover the nested workspace instruction.\n\n(End of file - total 1 lines)\n"}} -{"type":"tool/result","seq":87,"time":1783921767271,"data":{"turn":1,"step":1,"callId":"call_00_6APApmaKLRDlXKMdIcWL5139","content":[{"type":"text","text":"Touch this file to discover the nested workspace instruction."}],"isError":false},"sourceEventSeqs":[85],"surfaceOp":"append"} -{"type":"user/message","seq":88,"time":1784811336862,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"plugin","plugin":"workspace-context"},"meta":{"kind":"workspace-instructions","version":1,"changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]}},"surfaceOp":"append"} -{"type":"step/end","seq":89,"time":1783921767272,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":90,"time":1783921767272,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":91,"time":1783921768339,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":92,"time":1783921768340,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":93,"time":1783921768466,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" nested"}}} -{"type":"assistant/chunk","seq":94,"time":1783921768474,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"/t"}}} -{"type":"assistant/chunk","seq":95,"time":1783921768500,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ask"}}} -{"type":"assistant/chunk","seq":96,"time":1783921768501,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".txt"}}} -{"type":"assistant/chunk","seq":97,"time":1783921768535,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" told"}}} -{"type":"assistant/chunk","seq":98,"time":1783921768535,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":99,"time":1783921768535,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":100,"time":1783921768535,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":101,"time":1783921768564,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"Touch"}}} -{"type":"assistant/chunk","seq":102,"time":1783921768565,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" this"}}} -{"type":"assistant/chunk","seq":103,"time":1783921768565,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} -{"type":"assistant/chunk","seq":104,"time":1783921768565,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":105,"time":1783921768565,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" discover"}}} -{"type":"assistant/chunk","seq":106,"time":1783921768565,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":107,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" nested"}}} -{"type":"assistant/chunk","seq":108,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" workspace"}}} -{"type":"assistant/chunk","seq":109,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instruction"}}} -{"type":"assistant/chunk","seq":110,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".\""}}} -{"type":"assistant/chunk","seq":111,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":112,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":113,"time":1783921768621,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" nested"}}} -{"type":"assistant/chunk","seq":114,"time":1783921768621,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"/"}}} -{"type":"assistant/chunk","seq":115,"time":1783921768647,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"AG"}}} -{"type":"assistant/chunk","seq":116,"time":1783921768648,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ENTS"}}} -{"type":"assistant/chunk","seq":117,"time":1783921768648,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} -{"type":"assistant/chunk","seq":118,"time":1783921768648,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instructions"}}} -{"type":"assistant/chunk","seq":119,"time":1783921768688,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" say"}}} -{"type":"assistant/chunk","seq":120,"time":1783921768703,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} -{"type":"assistant/chunk","seq":121,"time":1783921768740,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":122,"time":1783921768740,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"When"}}} -{"type":"assistant/chunk","seq":123,"time":1783921768741,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} -{"type":"assistant/chunk","seq":124,"time":1783921768741,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" for"}}} -{"type":"assistant/chunk","seq":125,"time":1783921768741,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":126,"time":1783921768741,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Code"}}} -{"type":"assistant/chunk","seq":127,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Mode"}}} -{"type":"assistant/chunk","seq":128,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" workspace"}}} -{"type":"assistant/chunk","seq":129,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" hand"}}} -{"type":"assistant/chunk","seq":130,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"shake"}}} -{"type":"assistant/chunk","seq":131,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":132,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" answer"}}} -{"type":"assistant/chunk","seq":133,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} -{"type":"assistant/chunk","seq":134,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" `"}}} -{"type":"assistant/chunk","seq":135,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"CODE"}}} -{"type":"assistant/chunk","seq":136,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_M"}}} -{"type":"assistant/chunk","seq":137,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ODE"}}} -{"type":"assistant/chunk","seq":138,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_CONT"}}} -{"type":"assistant/chunk","seq":139,"time":1783921768824,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"EXT"}}} -{"type":"assistant/chunk","seq":140,"time":1783921768825,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_OK"}}} -{"type":"assistant/chunk","seq":141,"time":1783921768825,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"`"}}} -{"type":"assistant/chunk","seq":142,"time":1783921768825,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":143,"time":1783921768825,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" nothing"}}} -{"type":"assistant/chunk","seq":144,"time":1783921768825,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" else"}}} -{"type":"assistant/chunk","seq":145,"time":1783921768846,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".\"\n\n"}}} -{"type":"assistant/chunk","seq":146,"time":1783921768846,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"So"}}} -{"type":"assistant/chunk","seq":147,"time":1783921768847,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":148,"time":1783921768847,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" answer"}}} -{"type":"assistant/chunk","seq":149,"time":1783921768847,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":150,"time":1783921768873,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":151,"time":1783921768874,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" hand"}}} -{"type":"assistant/chunk","seq":152,"time":1783921768903,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"shake"}}} -{"type":"assistant/chunk","seq":153,"time":1783921768903,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" question"}}} -{"type":"assistant/chunk","seq":154,"time":1783921768903,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} -{"type":"assistant/chunk","seq":155,"time":1783921768903,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" `"}}} -{"type":"assistant/chunk","seq":156,"time":1783921768903,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"CODE"}}} -{"type":"assistant/chunk","seq":157,"time":1783921768929,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_M"}}} -{"type":"assistant/chunk","seq":158,"time":1783921768930,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ODE"}}} -{"type":"assistant/chunk","seq":159,"time":1783921768930,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_CONT"}}} -{"type":"assistant/chunk","seq":160,"time":1783921768930,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"EXT"}}} -{"type":"assistant/chunk","seq":161,"time":1783921768930,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_OK"}}} -{"type":"assistant/chunk","seq":162,"time":1783921768963,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"`."}}} -{"type":"assistant/chunk","seq":163,"time":1783921768963,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":164,"time":1783921768963,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"The"}}} -{"type":"assistant/chunk","seq":165,"time":1783921768963,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" **"}}} -{"type":"assistant/chunk","seq":166,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"Code"}}} -{"type":"assistant/chunk","seq":167,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" Mode"}}} -{"type":"assistant/chunk","seq":168,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" workspace"}}} -{"type":"assistant/chunk","seq":169,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" hand"}}} -{"type":"assistant/chunk","seq":170,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"shake"}}} -{"type":"assistant/chunk","seq":171,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"**"}}} -{"type":"assistant/chunk","seq":172,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" is"}}} -{"type":"assistant/chunk","seq":173,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":":"}}} -{"type":"assistant/chunk","seq":174,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} -{"type":"assistant/chunk","seq":175,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"CODE"}}} -{"type":"assistant/chunk","seq":176,"time":1783921769046,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_M"}}} -{"type":"assistant/chunk","seq":177,"time":1783921769046,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ODE"}}} -{"type":"assistant/chunk","seq":178,"time":1783921769047,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_CONT"}}} -{"type":"assistant/chunk","seq":179,"time":1783921769047,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"EXT"}}} -{"type":"assistant/chunk","seq":180,"time":1783921769047,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_OK"}}} -{"type":"assistant/chunk","seq":181,"time":1783921769047,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} -{"type":"assistant/chunk","seq":182,"time":1783921769101,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The nested/task.txt told me to \"Touch this file to discover the nested workspace instruction.\" and the nested/AGENTS.md instructions say: \"When asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\"\n\nSo the answer to the handshake question is `CODE_MODE_CONTEXT_OK`."}}}} -{"type":"assistant/chunk","seq":183,"time":1783921769101,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The **Code Mode workspace handshake** is: `CODE_MODE_CONTEXT_OK`"}}}} -{"type":"assistant/chunk","seq":184,"time":1783921769101,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":277,"outputTokens":90,"cacheReadTokens":3200,"reasoningTokens":71}}}} -{"type":"assistant/chunk","seq":185,"time":1783921769101,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":186,"time":1783921769101,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The nested/task.txt told me to \"Touch this file to discover the nested workspace instruction.\" and the nested/AGENTS.md instructions say: \"When asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\"\n\nSo the answer to the handshake question is `CODE_MODE_CONTEXT_OK`."},{"type":"text","text":"The **Code Mode workspace handshake** is: `CODE_MODE_CONTEXT_OK`"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":277,"outputTokens":90,"cacheReadTokens":3200,"reasoningTokens":71}},"sourceEventSeqs":[91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185],"surfaceOp":"append"} -{"type":"step/end","seq":187,"time":1783921769101,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":188,"time":1783921769101,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"user/message","seq":3,"time":1784903369847,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n"}],"source":{"kind":"plugin","plugin":"workspace-context"}},"surfaceOp":"append"} +{"type":"step/start","seq":4,"time":1784903369850,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":5,"time":1784903369850,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":6,"time":1783921766287,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":7,"time":1783921766483,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":8,"time":1783921766519,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":9,"time":1783921766520,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":10,"time":1783921766520,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":11,"time":1783921766520,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":12,"time":1783921766537,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" write"}}} +{"type":"assistant/chunk","seq":13,"time":1783921766538,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":14,"time":1783921766538,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":15,"time":1783921766573,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} +{"type":"assistant/chunk","seq":16,"time":1783921766573,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_code"}}} +{"type":"assistant/chunk","seq":17,"time":1783921766574,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" program"}}} +{"type":"assistant/chunk","seq":18,"time":1783921766574,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":19,"time":1783921766598,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reads"}}} +{"type":"assistant/chunk","seq":20,"time":1783921766598,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":21,"time":1783921766599,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":22,"time":1783921766624,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" called"}}} +{"type":"assistant/chunk","seq":23,"time":1783921766654,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" nested"}}} +{"type":"assistant/chunk","seq":24,"time":1783921766654,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"/t"}}} +{"type":"assistant/chunk","seq":25,"time":1783921766654,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ask"}}} +{"type":"assistant/chunk","seq":26,"time":1783921766654,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".txt"}}} +{"type":"assistant/chunk","seq":27,"time":1783921766655,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":28,"time":1783921766684,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} +{"type":"assistant/chunk","seq":29,"time":1783921766717,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" answer"}}} +{"type":"assistant/chunk","seq":30,"time":1783921766717,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":31,"time":1783921766717,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" hand"}}} +{"type":"assistant/chunk","seq":32,"time":1783921766743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"shake"}}} +{"type":"assistant/chunk","seq":33,"time":1783921766743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" question"}}} +{"type":"assistant/chunk","seq":34,"time":1783921766743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" based"}}} +{"type":"assistant/chunk","seq":35,"time":1783921766743,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" on"}}} +{"type":"assistant/chunk","seq":36,"time":1783921766776,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" what"}}} +{"type":"assistant/chunk","seq":37,"time":1783921766776,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} +{"type":"assistant/chunk","seq":38,"time":1783921766798,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} +{"type":"assistant/chunk","seq":39,"time":1783921766799,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":40,"time":1783921766799,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":41,"time":1783921766799,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":42,"time":1783921766799,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":43,"time":1783921766799,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":44,"time":1783921766845,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" start"}}} +{"type":"assistant/chunk","seq":45,"time":1783921766845,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" by"}}} +{"type":"assistant/chunk","seq":46,"time":1783921766845,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reading"}}} +{"type":"assistant/chunk","seq":47,"time":1783921766845,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":48,"time":1783921766845,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":49,"time":1783921766926,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":50,"time":1783921766926,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":51,"time":1783921766952,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":52,"time":1783921766952,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":53,"time":1783921766952,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":54,"time":1783921766952,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"code"}}} +{"type":"assistant/chunk","seq":55,"time":1783921766952,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":56,"time":1783921766978,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":57,"time":1783921766978,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":58,"time":1783921766978,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"const"}}} +{"type":"assistant/chunk","seq":59,"time":1783921767011,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" content"}}} +{"type":"assistant/chunk","seq":60,"time":1783921767011,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" ="}}} +{"type":"assistant/chunk","seq":61,"time":1783921767011,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" await"}}} +{"type":"assistant/chunk","seq":62,"time":1783921767011,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" tools"}}} +{"type":"assistant/chunk","seq":63,"time":1783921767011,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":".read"}}} +{"type":"assistant/chunk","seq":64,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"({"}}} +{"type":"assistant/chunk","seq":65,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" file"}}} +{"type":"assistant/chunk","seq":66,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"_path"}}} +{"type":"assistant/chunk","seq":67,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":":"}}} +{"type":"assistant/chunk","seq":68,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":69,"time":1783921767050,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"n"}}} +{"type":"assistant/chunk","seq":70,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"ested"}}} +{"type":"assistant/chunk","seq":71,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"/t"}}} +{"type":"assistant/chunk","seq":72,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"ask"}}} +{"type":"assistant/chunk","seq":73,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":74,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":75,"time":1783921767068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" });\\n"}}} +{"type":"assistant/chunk","seq":76,"time":1783921767095,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"return"}}} +{"type":"assistant/chunk","seq":77,"time":1783921767095,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":" content.lines.map(line => line.text).join(String.fromCharCode(10))"}}} +{"type":"assistant/chunk","seq":78,"time":1783921767095,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":";"}}} +{"type":"assistant/chunk","seq":79,"time":1783921767121,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":80,"time":1783921767205,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":81,"time":1783921767205,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to write a single run_code program that reads a file called nested/task.txt and then answer the handshake question based on what's in that file. Let me start by reading the file."}}}} +{"type":"assistant/chunk","seq":82,"time":1783921767205,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","arguments":"{\"code\": \"const content = await tools.read({ file_path: \\\"nested/task.txt\\\" });\\nreturn content.lines.map(line => line.text).join(String.fromCharCode(10));\"}"}}}} +{"type":"assistant/chunk","seq":83,"time":1783921767205,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3195,"outputTokens":106,"cacheReadTokens":0,"reasoningTokens":43}}}} +{"type":"assistant/chunk","seq":84,"time":1784903369854,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":85,"time":1784903369854,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to write a single run_code program that reads a file called nested/task.txt and then answer the handshake question based on what's in that file. Let me start by reading the file."},{"type":"tool-call","id":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","arguments":"{\"code\": \"const content = await tools.read({ file_path: \\\"nested/task.txt\\\" });\\nreturn content.lines.map(line => line.text).join(String.fromCharCode(10));\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3195,"outputTokens":106,"cacheReadTokens":0,"reasoningTokens":43}},"sourceEventSeqs":[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84],"surfaceOp":"append"} +{"type":"tool/call","seq":86,"time":1784903369854,"data":{"turn":1,"step":1,"callId":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","arguments":"{\"code\": \"const content = await tools.read({ file_path: \\\"nested/task.txt\\\" });\\nreturn content.lines.map(line => line.text).join(String.fromCharCode(10));\"}"}} +{"type":"tool/code-dispatch","seq":87,"time":1784903369915,"data":{"parentCallId":"call_00_6APApmaKLRDlXKMdIcWL5139","subCallId":"call_00_6APApmaKLRDlXKMdIcWL5139:code:1","name":"read","arguments":{"file_path":"nested/task.txt"},"isError":false,"resultSummary":"./nested/task.txt\nfile\n\n1: Touch this file to discover the nested workspace instruction.\n\n(End of file - total 1 lines)\n"}} +{"type":"tool/result","seq":88,"time":1784903369917,"data":{"turn":1,"step":1,"callId":"call_00_6APApmaKLRDlXKMdIcWL5139","content":[{"type":"text","text":"Touch this file to discover the nested workspace instruction."}],"isError":false},"sourceEventSeqs":[86],"surfaceOp":"append"} +{"type":"user/message","seq":89,"time":1784903369918,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]}},"surfaceOp":"append"} +{"type":"step/end","seq":90,"time":1784903369918,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":91,"time":1784903369926,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":92,"time":1783921768340,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":93,"time":1783921768466,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":94,"time":1783921768474,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" nested"}}} +{"type":"assistant/chunk","seq":95,"time":1783921768500,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"/t"}}} +{"type":"assistant/chunk","seq":96,"time":1783921768501,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ask"}}} +{"type":"assistant/chunk","seq":97,"time":1783921768535,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".txt"}}} +{"type":"assistant/chunk","seq":98,"time":1783921768535,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" told"}}} +{"type":"assistant/chunk","seq":99,"time":1783921768535,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":100,"time":1783921768535,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":101,"time":1783921768564,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":102,"time":1783921768565,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"Touch"}}} +{"type":"assistant/chunk","seq":103,"time":1783921768565,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" this"}}} +{"type":"assistant/chunk","seq":104,"time":1783921768565,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":105,"time":1783921768565,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":106,"time":1783921768565,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" discover"}}} +{"type":"assistant/chunk","seq":107,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":108,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" nested"}}} +{"type":"assistant/chunk","seq":109,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" workspace"}}} +{"type":"assistant/chunk","seq":110,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instruction"}}} +{"type":"assistant/chunk","seq":111,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".\""}}} +{"type":"assistant/chunk","seq":112,"time":1783921768595,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":113,"time":1783921768621,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":114,"time":1783921768621,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" nested"}}} +{"type":"assistant/chunk","seq":115,"time":1783921768647,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"/"}}} +{"type":"assistant/chunk","seq":116,"time":1783921768648,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"AG"}}} +{"type":"assistant/chunk","seq":117,"time":1783921768648,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ENTS"}}} +{"type":"assistant/chunk","seq":118,"time":1783921768648,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":119,"time":1783921768688,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instructions"}}} +{"type":"assistant/chunk","seq":120,"time":1783921768703,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" say"}}} +{"type":"assistant/chunk","seq":121,"time":1783921768740,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} +{"type":"assistant/chunk","seq":122,"time":1783921768740,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":123,"time":1783921768741,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"When"}}} +{"type":"assistant/chunk","seq":124,"time":1783921768741,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} +{"type":"assistant/chunk","seq":125,"time":1783921768741,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" for"}}} +{"type":"assistant/chunk","seq":126,"time":1783921768741,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":127,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Code"}}} +{"type":"assistant/chunk","seq":128,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Mode"}}} +{"type":"assistant/chunk","seq":129,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" workspace"}}} +{"type":"assistant/chunk","seq":130,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" hand"}}} +{"type":"assistant/chunk","seq":131,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"shake"}}} +{"type":"assistant/chunk","seq":132,"time":1783921768773,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":133,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" answer"}}} +{"type":"assistant/chunk","seq":134,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":135,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":136,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"CODE"}}} +{"type":"assistant/chunk","seq":137,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_M"}}} +{"type":"assistant/chunk","seq":138,"time":1783921768790,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ODE"}}} +{"type":"assistant/chunk","seq":139,"time":1783921768824,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_CONT"}}} +{"type":"assistant/chunk","seq":140,"time":1783921768825,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"EXT"}}} +{"type":"assistant/chunk","seq":141,"time":1783921768825,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_OK"}}} +{"type":"assistant/chunk","seq":142,"time":1783921768825,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":143,"time":1783921768825,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":144,"time":1783921768825,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" nothing"}}} +{"type":"assistant/chunk","seq":145,"time":1783921768846,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" else"}}} +{"type":"assistant/chunk","seq":146,"time":1783921768846,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".\"\n\n"}}} +{"type":"assistant/chunk","seq":147,"time":1783921768847,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"So"}}} +{"type":"assistant/chunk","seq":148,"time":1783921768847,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":149,"time":1783921768847,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" answer"}}} +{"type":"assistant/chunk","seq":150,"time":1783921768873,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":151,"time":1783921768874,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":152,"time":1783921768903,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" hand"}}} +{"type":"assistant/chunk","seq":153,"time":1783921768903,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"shake"}}} +{"type":"assistant/chunk","seq":154,"time":1783921768903,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" question"}}} +{"type":"assistant/chunk","seq":155,"time":1783921768903,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} +{"type":"assistant/chunk","seq":156,"time":1783921768903,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":157,"time":1783921768929,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"CODE"}}} +{"type":"assistant/chunk","seq":158,"time":1783921768930,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_M"}}} +{"type":"assistant/chunk","seq":159,"time":1783921768930,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ODE"}}} +{"type":"assistant/chunk","seq":160,"time":1783921768930,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_CONT"}}} +{"type":"assistant/chunk","seq":161,"time":1783921768930,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"EXT"}}} +{"type":"assistant/chunk","seq":162,"time":1783921768963,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_OK"}}} +{"type":"assistant/chunk","seq":163,"time":1783921768963,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"`."}}} +{"type":"assistant/chunk","seq":164,"time":1783921768963,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":165,"time":1783921768963,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"The"}}} +{"type":"assistant/chunk","seq":166,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" **"}}} +{"type":"assistant/chunk","seq":167,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"Code"}}} +{"type":"assistant/chunk","seq":168,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" Mode"}}} +{"type":"assistant/chunk","seq":169,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" workspace"}}} +{"type":"assistant/chunk","seq":170,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" hand"}}} +{"type":"assistant/chunk","seq":171,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"shake"}}} +{"type":"assistant/chunk","seq":172,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":173,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" is"}}} +{"type":"assistant/chunk","seq":174,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":":"}}} +{"type":"assistant/chunk","seq":175,"time":1783921769024,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":176,"time":1783921769046,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"CODE"}}} +{"type":"assistant/chunk","seq":177,"time":1783921769046,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_M"}}} +{"type":"assistant/chunk","seq":178,"time":1783921769047,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ODE"}}} +{"type":"assistant/chunk","seq":179,"time":1783921769047,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_CONT"}}} +{"type":"assistant/chunk","seq":180,"time":1783921769047,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"EXT"}}} +{"type":"assistant/chunk","seq":181,"time":1783921769047,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"_OK"}}} +{"type":"assistant/chunk","seq":182,"time":1783921769101,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":183,"time":1783921769101,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The nested/task.txt told me to \"Touch this file to discover the nested workspace instruction.\" and the nested/AGENTS.md instructions say: \"When asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\"\n\nSo the answer to the handshake question is `CODE_MODE_CONTEXT_OK`."}}}} +{"type":"assistant/chunk","seq":184,"time":1783921769101,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The **Code Mode workspace handshake** is: `CODE_MODE_CONTEXT_OK`"}}}} +{"type":"assistant/chunk","seq":185,"time":1783921769101,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":277,"outputTokens":90,"cacheReadTokens":3200,"reasoningTokens":71}}}} +{"type":"assistant/chunk","seq":186,"time":1784903369930,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":187,"time":1784903369930,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The nested/task.txt told me to \"Touch this file to discover the nested workspace instruction.\" and the nested/AGENTS.md instructions say: \"When asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\"\n\nSo the answer to the handshake question is `CODE_MODE_CONTEXT_OK`."},{"type":"text","text":"The **Code Mode workspace handshake** is: `CODE_MODE_CONTEXT_OK`"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":277,"outputTokens":90,"cacheReadTokens":3200,"reasoningTokens":71}},"sourceEventSeqs":[92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186],"surfaceOp":"append"} +{"type":"step/end","seq":188,"time":1784903369930,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":189,"time":1784903369931,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl index cdbe2e4bd1..d5e142901c 100644 --- a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl +++ b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl @@ -11,7 +11,7 @@ {"type":"assistant/chunk","seq":9,"time":1783951000009,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":10,"time":1784449176722,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"inspect-tools-api","name":"cordis_inspect","arguments":"{\"what\":\"api\",\"name\":\"tools\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} {"type":"tool/call","seq":11,"time":1784449176722,"data":{"turn":1,"step":1,"callId":"inspect-tools-api","name":"cordis_inspect","arguments":"{\"what\":\"api\",\"name\":\"tools\"}"}} -{"type":"tool/result","seq":12,"time":1784449176732,"data":{"turn":1,"step":1,"callId":"inspect-tools-api","content":[{"type":"text","text":"## api\n- tools — Tool registry and execution pipeline.\n /**\n * Register globally or in the calling agent scope. Scoped tools shadow\n * globals; duplicates within one layer and the reserved `run_code` name fail.\n * @param definition - tool schema, execution, and optional finalization/presentation callbacks.\n * @returns the exact disposer that unregisters the tool.\n */\n register(definition: ToolDefinition): () => void\n /**\n * Restrict global tools for the calling agent scope. Empty filters, unknown\n * names, scope-local names, and reserved transport names fail. Restrictions\n * intersect; scoped registrations remain visible.\n * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).\n * @returns the exact disposer that lifts this restriction.\n */\n restrict(filter: ToolRestriction): () => void\n /**\n * Register a monotonic guard after the extensible `tools/pre-execute`\n * waterfall. A plain-context guard applies globally; one registered through\n * `agent.ctx` applies only to that agent. Any matching guard may deny by\n * returning a reason, while no guard can force-allow a call another guard\n * denied. The exact effect disposer is returned for ordered ownership and\n * HMR cleanup.\n * @param guard - synchronous check; a returned string denies the execution.\n * @returns the exact disposer that unregisters the guard.\n */\n guard(guard: ToolGuard): () => void\n /**\n * Look up a tool as one scope sees it (scoped\n * shadows global; a restricted-away global reads as absent). Presenters pass\n * the calling agent so the rendered card matches the definition that\n * actually executed.\n * @param name - the tool name as registered.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns the definition the scope resolves, or undefined when none is visible.\n */\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined\n /**\n * Project visible definitions onto the allowlisted model-facing schema fields,\n * excluding execution and presentation callbacks.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns one deep-cloned schema per visible tool.\n */\n schemas(scope?: ScopeKey): ToolSchema[]\n /**\n * Classify a pending call through the caller's visible tool definition. Only\n * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or\n * throwing classifiers are exclusive.\n * @param exec - call name, parsed arguments, and optional agent scope.\n * @returns the fail-closed scheduling mode.\n */\n executionMode(exec: ToolExecutionInput): ToolExecutionMode\n /**\n * Execute through pre-policy, guards, around-dispatch, post-policy,\n * definition-owned content finalization, and final notification. Tool and\n * listener failures resolve as materialized error results; an invisible tool\n * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen\n * snapshot final observers receive. Cancellation\n * arriving after entry and before final result materialization skips a\n * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a\n * successful started outcome with `ABORTED`; already-started work is still\n * drained and may retain a tool-owned structured error.\n * @param exec - the typed same-process call input. The registry assigns its\n * correlation token before policy begins.\n * @returns the materialized final result.\n */\n async execute(exec: ToolExecutionInput): Promise\ntype shapes (referenced by the signatures above — read these before assuming a field is a string):\n export type AdditionalContext = UserMessageData;\n export abstract class Agent {\n abstract readonly id: SessionId;\n abstract readonly options: AgentOptions;\n abstract readonly session: Session;\n abstract readonly status: AgentStatus;\n abstract readonly ctx: Context;\n abstract send(content: ContentBlock[], options: SendOptions): AgentMessageId;\n abstract cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n abstract whenIdle(): Promise;\n followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n abstract retry(): void;\n }\n export type AgentCancelCause = {\n readonly kind: 'user';\n } | {\n readonly kind: 'parent';\n };\n export type AgentMessageId = Branded<'AgentMessageId'>;\n export interface AgentOptions {\n provider?: string;\n model?: string;\n }\n export type AgentStatus = 'idle' | 'running';\n export interface AliasSendOptions {\n source?: MessageSource;\n }\n export interface AssistantProvenance {\n provider: string;\n model: string;\n replayState?: unknown;\n }\n export type Branded = string & {\n readonly [BRAND]: B;\n };\n export type CallId = Branded<'CallId'>;\n export interface CancelOptions {\n keepInbox?: boolean;\n }\n export interface ContentBlockMap {\n 'text': TextBlock;\n 'reasoning': ReasoningBlock;\n 'tool-call': ToolCallBlock;\n 'tool-result': ToolResultBlock;\n }\n export type ContentBlockType = keyof ContentBlockMap;\n export interface DiffCallView {\n card: 'diff';\n title: string;\n diffs: FileDiff[];\n locations?: FileLocation[];\n }\n export interface DiffResultView {\n card: 'diff';\n title?: string;\n diffs: FileDiff[];\n }\n export interface EpochHeader {\n config: LlmCallConfig;\n system?: string;\n tools?: ToolSchema[];\n messagePrefix?: Message[];\n }\n export interface FileDiff {\n path: string;\n oldText: string | null;\n newText: string;\n }\n export interface FileLocation {\n path: string;\n line?: number;\n }\n export type FinishReason = FinishReasonMap[keyof FinishReasonMap];\n export interface FinishReasonMap {\n 'stop': {\n kind: 'stop';\n };\n 'tool-calls': {\n kind: 'tool-calls';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n 'aborted': {\n kind: 'aborted';\n failure: LlmFailure;\n };\n 'error': {\n kind: 'error';\n failure: LlmFailure;\n };\n }\n export interface GenericCallView {\n card: 'generic';\n title: string;\n kind?: ToolCallKind;\n rawInput?: unknown;\n content?: ContentBlock[];\n locations?: FileLocation[];\n }\n export interface GenericResultView {\n card: 'generic';\n title?: string;\n content?: ContentBlock[];\n }\n export interface JsonSchemaNode {\n type?: JsonSchemaType;\n oneOf?: JsonSchemaNode[];\n properties?: Record;\n required?: string[];\n additionalProperties?: boolean;\n items?: JsonSchemaNode;\n enum?: JsonSchemaScalar[];\n const?: JsonSchemaScalar;\n description?: string;\n title?: string;\n default?: JsonValue;\n examples?: JsonValue;\n }\n export type JsonSchemaScalar = string | number | boolean | null;\n export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';\n export type JsonValue = null | boolean | number | string | JsonValue[] | {\n [key: string]: JsonValue;\n };\n export interface LlmCallConfig {\n provider: string;\n model: string;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n }\n export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n }\n export interface Message {\n role: 'system' | 'user' | 'assistant';\n content: ContentBlock[];\n provenance?: AssistantProvenance;\n }\n export type MessageSource = MessageSourceMap[keyof MessageSourceMap];\n export interface MessageSourceMap {\n user: {\n kind: 'user';\n };\n plugin: {\n kind: 'plugin';\n plugin: string;\n };\n }\n export type ProviderRequestId = Branded<'ProviderRequestId'>;\n export interface ReasoningBlock {\n type: 'reasoning';\n text: string;\n }\n export type RequestHeaderReason = 'initial' | 'resume' | 'change';\n export type ScopeKey = object;\n export interface SendOptions {\n target: SendTarget;\n wakeup: boolean;\n source: MessageSource;\n }\n export type SendTarget = 'next-turn' | 'next-step';\n export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n get id(): SessionId;\n constructor(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader);\n get events(): readonly SessionEvent[];\n get seq(): number;\n append(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent;\n requestHeader(): EpochHeader | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n }\n export type SessionEvent = {\n [K in SessionEventType]: {\n type: K;\n seq: number;\n time: number;\n data: SessionEventMap[K];\n } & (K extends SurfaceEventType ? {\n sourceEventSeqs?: number[];\n surfaceOp?: SurfaceOp;\n } : object);\n }[T];\n export interface SessionEventMap {\n 'turn/start': {\n turn: number;\n trigger: TurnTrigger;\n };\n 'turn/end': {\n turn: number;\n reason: TurnEndReason;\n };\n 'step/start': {\n turn: number;\n step: number;\n };\n 'step/end': {\n turn: number;\n step: number;\n };\n 'user/message': UserMessageData;\n 'prompt/blocked': {\n content: ContentBlock[];\n source: MessageSource;\n reason: string;\n };\n 'assistant/chunk': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n 'assistant/message': {\n turn: number;\n step: number;\n content: ContentBlock[];\n provenance: AssistantProvenance;\n usage?: TokenUsage;\n };\n 'tool/call': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n 'tool/result': {\n turn: number;\n step: number;\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n 'steering/message': UserMessageData & {\n turn: number;\n };\n 'todo/write': {\n todos: TodoItem[];\n };\n 'request/header': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n }\n export type SessionEventType = keyof SessionEventMap;\n export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly delegationDepth?: number;\n }\n export type SessionId = Branded<'SessionId'>;\n export interface SessionSurface {\n readonly nodes: readonly number[];\n readonly replaceGeneration: number;\n }\n export type StreamChunk = {\n type: 'block-start';\n index: number;\n blockType: ContentBlockType;\n } | {\n type: 'text-delta';\n index: number;\n text: string;\n } | {\n type: 'reasoning-delta';\n index: number;\n text: string;\n } | {\n type: 'tool-call-delta';\n index: number;\n id: CallId;\n name?: string;\n argumentsDelta: string;\n } | {\n type: 'block-end';\n index: number;\n block: ContentBlock;\n } | {\n type: 'usage';\n usage: TokenUsage;\n } | {\n type: 'finish';\n reason: FinishReason;\n replayState?: unknown;\n };\n export type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result' | 'steering/message';\n export interface SurfaceIntent {\n surfaceOp: SurfaceOp;\n sourceEventSeqs?: number[];\n }\n export type SurfaceOp = 'append' | {\n op: 'replace';\n start: number;\n end: number;\n };\n export interface TerminalCallView {\n card: 'terminal';\n title: string;\n description?: string;\n cwd?: string;\n }\n export interface TerminalResultView {\n card: 'terminal';\n title?: string;\n output?: string;\n exitCode?: number;\n signal?: string;\n }\n export interface TodoItem {\n content: string;\n status: 'pending' | 'in_progress' | 'completed';\n }\n export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n }\n export interface ToolCallBlock {\n type: 'tool-call';\n id: CallId;\n name: string;\n arguments: string;\n }\n export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\n export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\n export interface ToolDefinition extends ToolSchema {\n readonly output: ToolOutputDefinition;\n execute(args: unknown, exec: ToolRunContext): Promise;\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n timeoutMs?: number;\n isConcurrencySafe?(args: unknown): boolean;\n presentCall?(args: unknown): ToolCallView | undefined;\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\n }\n export interface ToolErrorInfo {\n name: string;\n code: string;\n }\n export interface ToolExecution extends ToolExecutionInput {\n readonly token: ToolExecutionToken;\n }\n export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: AdditionalContext[];\n readonly concludesTurn?: never;\n }\n export interface ToolExecutionInput {\n readonly callId: CallId;\n readonly name: string;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n }\n export type ToolExecutionMode = {\n kind: 'parallel';\n } | {\n kind: 'exclusive';\n };\n export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure;\n export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: AdditionalContext[];\n readonly concludesTurn?: true;\n }\n export type ToolExecutionToken = symbol & {\n readonly [toolExecutionTokenBrand]: true;\n };\n export interface ToolFailure {\n message: string;\n info?: ToolErrorInfo;\n }\n export type ToolGuard = (execution: Readonly) => string | undefined;\n export interface ToolOutputDefinition {\n readonly schema: JsonSchemaNode;\n render(args: unknown, value: JsonValue): ContentBlock[];\n presentationMeta?(args: unknown, value: JsonValue): JsonValue;\n }\n export interface ToolRestriction {\n readonly allow?: readonly string[];\n readonly deny?: readonly string[];\n }\n export interface ToolResult {\n content: ContentBlock[];\n isError: boolean;\n meta?: JsonValue;\n }\n export interface ToolResultBlock {\n type: 'tool-result';\n toolCallId: CallId;\n content: ContentBlock[];\n isError?: boolean;\n }\n export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView;\n export interface ToolRunContext extends ToolExecution {\n deferContext(context: AdditionalContext): void;\n concludeTurn(): void;\n }\n export interface ToolSchema {\n name: string;\n description: string;\n parameters: Record;\n }\n export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];\n export interface TurnEndReasonMap {\n completed: {\n kind: 'completed';\n };\n aborted: {\n kind: 'aborted';\n };\n error: {\n kind: 'error';\n step: number;\n } & ({\n failure: LlmFailure;\n message?: never;\n code?: never;\n } | {\n message: string;\n code?: string;\n failure?: never;\n });\n disposed: {\n kind: 'disposed';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n rejected: {\n kind: 'rejected';\n reason: string;\n };\n interrupted: {\n kind: 'interrupted';\n };\n }\n export type TurnTrigger = TurnTriggerMap[keyof TurnTriggerMap];\n export interface TurnTriggerMap {\n message: {\n kind: 'message';\n source: MessageSource;\n };\n retry: {\n kind: 'retry';\n };\n injection: {\n kind: 'injection';\n source: MessageSource;\n };\n }\n export interface UserMessageData {\n content: ContentBlock[];\n source: MessageSource;\n }"}],"isError":false},"sourceEventSeqs":[11],"surfaceOp":"append"} +{"type":"tool/result","seq":12,"time":1784449176732,"data":{"turn":1,"step":1,"callId":"inspect-tools-api","content":[{"type":"text","text":"## api\n- tools — Tool registry and execution pipeline.\n /**\n * Register globally or in the calling agent scope. Scoped tools shadow\n * globals; duplicates within one layer and the reserved `run_code` name fail.\n * @param definition - tool schema, execution, and optional finalization/presentation callbacks.\n * @returns the exact disposer that unregisters the tool.\n */\n register(definition: ToolDefinition): () => void\n /**\n * Restrict global tools for the calling agent scope. Empty filters, unknown\n * names, scope-local names, and reserved transport names fail. Restrictions\n * intersect; scoped registrations remain visible.\n * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).\n * @returns the exact disposer that lifts this restriction.\n */\n restrict(filter: ToolRestriction): () => void\n /**\n * Register a monotonic guard after the extensible `tools/pre-execute`\n * waterfall. A plain-context guard applies globally; one registered through\n * `agent.ctx` applies only to that agent. Any matching guard may deny by\n * returning a reason, while no guard can force-allow a call another guard\n * denied. The exact effect disposer is returned for ordered ownership and\n * HMR cleanup.\n * @param guard - synchronous check; a returned string denies the execution.\n * @returns the exact disposer that unregisters the guard.\n */\n guard(guard: ToolGuard): () => void\n /**\n * Look up a tool as one scope sees it (scoped\n * shadows global; a restricted-away global reads as absent). Presenters pass\n * the calling agent so the rendered card matches the definition that\n * actually executed.\n * @param name - the tool name as registered.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns the definition the scope resolves, or undefined when none is visible.\n */\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined\n /**\n * Project visible definitions onto the allowlisted model-facing schema fields,\n * excluding execution and presentation callbacks.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns one deep-cloned schema per visible tool.\n */\n schemas(scope?: ScopeKey): ToolSchema[]\n /**\n * Classify a pending call through the caller's visible tool definition. Only\n * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or\n * throwing classifiers are exclusive.\n * @param exec - call name, parsed arguments, and optional agent scope.\n * @returns the fail-closed scheduling mode.\n */\n executionMode(exec: ToolExecutionInput): ToolExecutionMode\n /**\n * Execute through pre-policy, guards, around-dispatch, post-policy,\n * definition-owned content finalization, and final notification. Tool and\n * listener failures resolve as materialized error results; an invisible tool\n * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen\n * snapshot final observers receive. Cancellation\n * arriving after entry and before final result materialization skips a\n * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a\n * successful started outcome with `ABORTED`; already-started work is still\n * drained and may retain a tool-owned structured error.\n * @param exec - the typed same-process call input. The registry assigns its\n * correlation token before policy begins.\n * @returns the materialized final result.\n */\n async execute(exec: ToolExecutionInput): Promise\ntype shapes (referenced by the signatures above — read these before assuming a field is a string):\n export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly status: AgentStatus;\n readonly ctx: Context;\n send(input: UserMessageData, options: SendOptions): AgentMessageId;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise;\n followup(input: UserMessageData): AgentMessageId;\n steer(input: UserMessageData): AgentMessageId;\n inject(input: UserMessageData): AgentMessageId;\n retry(): void;\n }\n export type AgentCancelCause = {\n readonly kind: 'user';\n } | {\n readonly kind: 'parent';\n };\n export type AgentMessageId = Branded<'AgentMessageId'>;\n export interface AgentOptions {\n provider?: string;\n model?: string;\n }\n export type AgentStatus = 'idle' | 'running';\n export interface AssistantProvenance {\n provider: string;\n model: string;\n replayState?: unknown;\n }\n export type Branded = string & {\n readonly [BRAND]: B;\n };\n export type CallId = Branded<'CallId'>;\n export interface CancelOptions {\n keepInbox?: boolean;\n }\n export interface ContentBlockMap {\n 'text': TextBlock;\n 'reasoning': ReasoningBlock;\n 'tool-call': ToolCallBlock;\n 'tool-result': ToolResultBlock;\n }\n export type ContentBlockType = keyof ContentBlockMap;\n export interface DiffCallView {\n card: 'diff';\n title: string;\n diffs: FileDiff[];\n locations?: FileLocation[];\n }\n export interface DiffResultView {\n card: 'diff';\n title?: string;\n diffs: FileDiff[];\n }\n export interface EpochHeader {\n config: LlmCallConfig;\n system?: string;\n tools?: ToolSchema[];\n }\n export interface FileDiff {\n path: string;\n oldText: string | null;\n newText: string;\n }\n export interface FileLocation {\n path: string;\n line?: number;\n }\n export type FinishReason = FinishReasonMap[keyof FinishReasonMap];\n export interface FinishReasonMap {\n 'stop': {\n kind: 'stop';\n };\n 'tool-calls': {\n kind: 'tool-calls';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n 'aborted': {\n kind: 'aborted';\n failure: LlmFailure;\n };\n 'error': {\n kind: 'error';\n failure: LlmFailure;\n };\n }\n export interface GenericCallView {\n card: 'generic';\n title: string;\n kind?: ToolCallKind;\n rawInput?: unknown;\n content?: ContentBlock[];\n locations?: FileLocation[];\n }\n export interface GenericResultView {\n card: 'generic';\n title?: string;\n content?: ContentBlock[];\n }\n export interface JsonSchemaNode {\n type?: JsonSchemaType;\n oneOf?: JsonSchemaNode[];\n properties?: Record;\n required?: string[];\n additionalProperties?: boolean;\n items?: JsonSchemaNode;\n enum?: JsonSchemaScalar[];\n const?: JsonSchemaScalar;\n description?: string;\n title?: string;\n default?: JsonValue;\n examples?: JsonValue;\n }\n export type JsonSchemaScalar = string | number | boolean | null;\n export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';\n export type JsonValue = null | boolean | number | string | JsonValue[] | {\n [key: string]: JsonValue;\n };\n export interface LlmCallConfig {\n provider: string;\n model: string;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n }\n export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n }\n export interface Message {\n role: 'system' | 'user' | 'assistant';\n content: ContentBlock[];\n provenance?: AssistantProvenance;\n }\n export type MessageSource = MessageSourceMap[keyof MessageSourceMap];\n export interface MessageSourceMap {\n user: {\n kind: 'user';\n };\n plugin: {\n kind: 'plugin';\n plugin: string;\n };\n }\n export type ProviderRequestId = Branded<'ProviderRequestId'>;\n export interface ReasoningBlock {\n type: 'reasoning';\n text: string;\n }\n export type RequestHeaderReason = 'initial' | 'resume' | 'change';\n export type ScopeKey = object;\n export interface SendOptions {\n target: SendTarget;\n wakeup: boolean;\n }\n export type SendTarget = 'next-turn' | 'next-step';\n export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n get id(): SessionId;\n constructor(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader);\n get events(): readonly SessionEvent[];\n get seq(): number;\n append(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent;\n requestHeader(): EpochHeader | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n }\n export type SessionEvent = {\n [K in SessionEventType]: {\n type: K;\n seq: number;\n time: number;\n data: SessionEventMap[K];\n } & (K extends SurfaceEventType ? {\n sourceEventSeqs?: number[];\n surfaceOp?: SurfaceOp;\n } : object);\n }[T];\n export interface SessionEventMap {\n 'turn/start': {\n turn: number;\n trigger: TurnTrigger;\n };\n 'turn/end': {\n turn: number;\n reason: TurnEndReason;\n };\n 'step/start': {\n turn: number;\n step: number;\n };\n 'step/end': {\n turn: number;\n step: number;\n };\n 'user/message': UserMessageData;\n 'assistant/chunk': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n 'assistant/message': {\n turn: number;\n step: number;\n content: ContentBlock[];\n provenance: AssistantProvenance;\n usage?: TokenUsage;\n };\n 'tool/call': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n 'tool/result': {\n turn: number;\n step: number;\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n 'steering/message': UserMessageData & {\n turn: number;\n };\n 'todo/write': {\n todos: TodoItem[];\n };\n 'request/header': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n }\n export type SessionEventType = keyof SessionEventMap;\n export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly delegationDepth?: number;\n }\n export type SessionId = Branded<'SessionId'>;\n export interface SessionSurface {\n readonly nodes: readonly number[];\n readonly replaceGeneration: number;\n }\n export type StreamChunk = {\n type: 'block-start';\n index: number;\n blockType: ContentBlockType;\n } | {\n type: 'text-delta';\n index: number;\n text: string;\n } | {\n type: 'reasoning-delta';\n index: number;\n text: string;\n } | {\n type: 'tool-call-delta';\n index: number;\n id: CallId;\n name?: string;\n argumentsDelta: string;\n } | {\n type: 'block-end';\n index: number;\n block: ContentBlock;\n } | {\n type: 'usage';\n usage: TokenUsage;\n } | {\n type: 'finish';\n reason: FinishReason;\n replayState?: unknown;\n };\n export type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result' | 'steering/message';\n export interface SurfaceIntent {\n surfaceOp: SurfaceOp;\n sourceEventSeqs?: number[];\n }\n export type SurfaceOp = 'append' | {\n op: 'replace';\n start: number;\n end: number;\n };\n export interface TerminalCallView {\n card: 'terminal';\n title: string;\n description?: string;\n cwd?: string;\n }\n export interface TerminalResultView {\n card: 'terminal';\n title?: string;\n output?: string;\n exitCode?: number;\n signal?: string;\n }\n export interface TodoItem {\n content: string;\n status: 'pending' | 'in_progress' | 'completed';\n }\n export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n }\n export interface ToolCallBlock {\n type: 'tool-call';\n id: CallId;\n name: string;\n arguments: string;\n }\n export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\n export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\n export interface ToolDefinition extends ToolSchema {\n readonly output: ToolOutputDefinition;\n execute(args: unknown, exec: ToolRunContext): Promise;\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n timeoutMs?: number;\n isConcurrencySafe?(args: unknown): boolean;\n presentCall?(args: unknown): ToolCallView | undefined;\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\n }\n export interface ToolErrorInfo {\n name: string;\n code: string;\n }\n export interface ToolExecution extends ToolExecutionInput {\n readonly token: ToolExecutionToken;\n }\n export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessageData[];\n readonly concludesTurn?: never;\n }\n export interface ToolExecutionInput {\n readonly callId: CallId;\n readonly name: string;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n }\n export type ToolExecutionMode = {\n kind: 'parallel';\n } | {\n kind: 'exclusive';\n };\n export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure;\n export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessageData[];\n readonly concludesTurn?: true;\n }\n export type ToolExecutionToken = symbol & {\n readonly [toolExecutionTokenBrand]: true;\n };\n export interface ToolFailure {\n message: string;\n info?: ToolErrorInfo;\n }\n export type ToolGuard = (execution: Readonly) => string | undefined;\n export interface ToolOutputDefinition {\n readonly schema: JsonSchemaNode;\n render(args: unknown, value: JsonValue): ContentBlock[];\n presentationMeta?(args: unknown, value: JsonValue): JsonValue;\n }\n export interface ToolRestriction {\n readonly allow?: readonly string[];\n readonly deny?: readonly string[];\n }\n export interface ToolResult {\n content: ContentBlock[];\n isError: boolean;\n meta?: JsonValue;\n }\n export interface ToolResultBlock {\n type: 'tool-result';\n toolCallId: CallId;\n content: ContentBlock[];\n isError?: boolean;\n }\n export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView;\n export interface ToolRunContext extends ToolExecution {\n deferContext(context: UserMessageData): void;\n concludeTurn(): void;\n }\n export interface ToolSchema {\n name: string;\n description: string;\n parameters: Record;\n }\n export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];\n export interface TurnEndReasonMap {\n completed: {\n kind: 'completed';\n };\n aborted: {\n kind: 'aborted';\n };\n error: {\n kind: 'error';\n step: number;\n } & ({\n failure: LlmFailure;\n message?: never;\n code?: never;\n } | {\n message: string;\n code?: string;\n failure?: never;\n });\n disposed: {\n kind: 'disposed';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n interrupted: {\n kind: 'interrupted';\n };\n }\n export type TurnTrigger = TurnTriggerMap[keyof TurnTriggerMap];\n export interface TurnTriggerMap {\n message: {\n kind: 'message';\n source: MessageSource;\n };\n retry: {\n kind: 'retry';\n };\n injection: {\n kind: 'injection';\n source: MessageSource;\n };\n }\n export interface UserMessageData {\n content: ContentBlock[];\n source: MessageSource;\n }"}],"isError":false},"sourceEventSeqs":[11],"surfaceOp":"append"} {"type":"step/end","seq":13,"time":1784449176732,"data":{"turn":1,"step":1}} {"type":"step/start","seq":14,"time":1784449176733,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":15,"time":1783951000015,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} diff --git a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl index df0b9c9c96..c76e81c9bf 100644 --- a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl @@ -3,7 +3,7 @@ {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"available_commands_update","availableCommands":[{"name":"goal","description":"set or view the goal for a long-running task","input":{"hint":"[|clear|edit |pause|resume]"}},{"name":"plan","description":"Enter or leave plan mode","input":{"hint":"[off|message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Inspect the exact tools service","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"inspect-tools-api","title":"Inspect cordis runtime: api: tools","kind":"read","status":"in_progress"}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"inspect-tools-api","status":"completed","content":[{"type":"content","content":{"type":"text","text":"## api\n- tools — Tool registry and execution pipeline.\n /**\n * Register globally or in the calling agent scope. Scoped tools shadow\n * globals; duplicates within one layer and the reserved `run_code` name fail.\n * @param definition - tool schema, execution, and optional finalization/presentation callbacks.\n * @returns the exact disposer that unregisters the tool.\n */\n register(definition: ToolDefinition): () => void\n /**\n * Restrict global tools for the calling agent scope. Empty filters, unknown\n * names, scope-local names, and reserved transport names fail. Restrictions\n * intersect; scoped registrations remain visible.\n * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).\n * @returns the exact disposer that lifts this restriction.\n */\n restrict(filter: ToolRestriction): () => void\n /**\n * Register a monotonic guard after the extensible `tools/pre-execute`\n * waterfall. A plain-context guard applies globally; one registered through\n * `agent.ctx` applies only to that agent. Any matching guard may deny by\n * returning a reason, while no guard can force-allow a call another guard\n * denied. The exact effect disposer is returned for ordered ownership and\n * HMR cleanup.\n * @param guard - synchronous check; a returned string denies the execution.\n * @returns the exact disposer that unregisters the guard.\n */\n guard(guard: ToolGuard): () => void\n /**\n * Look up a tool as one scope sees it (scoped\n * shadows global; a restricted-away global reads as absent). Presenters pass\n * the calling agent so the rendered card matches the definition that\n * actually executed.\n * @param name - the tool name as registered.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns the definition the scope resolves, or undefined when none is visible.\n */\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined\n /**\n * Project visible definitions onto the allowlisted model-facing schema fields,\n * excluding execution and presentation callbacks.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns one deep-cloned schema per visible tool.\n */\n schemas(scope?: ScopeKey): ToolSchema[]\n /**\n * Classify a pending call through the caller's visible tool definition. Only\n * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or\n * throwing classifiers are exclusive.\n * @param exec - call name, parsed arguments, and optional agent scope.\n * @returns the fail-closed scheduling mode.\n */\n executionMode(exec: ToolExecutionInput): ToolExecutionMode\n /**\n * Execute through pre-policy, guards, around-dispatch, post-policy,\n * definition-owned content finalization, and final notification. Tool and\n * listener failures resolve as materialized error results; an invisible tool\n * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen\n * snapshot final observers receive. Cancellation\n * arriving after entry and before final result materialization skips a\n * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a\n * successful started outcome with `ABORTED`; already-started work is still\n * drained and may retain a tool-owned structured error.\n * @param exec - the typed same-process call input. The registry assigns its\n * correlation token before policy begins.\n * @returns the materialized final result.\n */\n async execute(exec: ToolExecutionInput): Promise\ntype shapes (referenced by the signatures above — read these before assuming a field is a string):\n export type AdditionalContext = UserMessageData;\n export abstract class Agent {\n abstract readonly id: SessionId;\n abstract readonly options: AgentOptions;\n abstract readonly session: Session;\n abstract readonly status: AgentStatus;\n abstract readonly ctx: Context;\n abstract send(content: ContentBlock[], options: SendOptions): AgentMessageId;\n abstract cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n abstract whenIdle(): Promise;\n followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n abstract retry(): void;\n }\n export type AgentCancelCause = {\n readonly kind: 'user';\n } | {\n readonly kind: 'parent';\n };\n export type AgentMessageId = Branded<'AgentMessageId'>;\n export interface AgentOptions {\n provider?: string;\n model?: string;\n }\n export type AgentStatus = 'idle' | 'running';\n export interface AliasSendOptions {\n source?: MessageSource;\n }\n export interface AssistantProvenance {\n provider: string;\n model: string;\n replayState?: unknown;\n }\n export type Branded = string & {\n readonly [BRAND]: B;\n };\n export type CallId = Branded<'CallId'>;\n export interface CancelOptions {\n keepInbox?: boolean;\n }\n export interface ContentBlockMap {\n 'text': TextBlock;\n 'reasoning': ReasoningBlock;\n 'tool-call': ToolCallBlock;\n 'tool-result': ToolResultBlock;\n }\n export type ContentBlockType = keyof ContentBlockMap;\n export interface DiffCallView {\n card: 'diff';\n title: string;\n diffs: FileDiff[];\n locations?: FileLocation[];\n }\n export interface DiffResultView {\n card: 'diff';\n title?: string;\n diffs: FileDiff[];\n }\n export interface EpochHeader {\n config: LlmCallConfig;\n system?: string;\n tools?: ToolSchema[];\n messagePrefix?: Message[];\n }\n export interface FileDiff {\n path: string;\n oldText: string | null;\n newText: string;\n }\n export interface FileLocation {\n path: string;\n line?: number;\n }\n export type FinishReason = FinishReasonMap[keyof FinishReasonMap];\n export interface FinishReasonMap {\n 'stop': {\n kind: 'stop';\n };\n 'tool-calls': {\n kind: 'tool-calls';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n 'aborted': {\n kind: 'aborted';\n failure: LlmFailure;\n };\n 'error': {\n kind: 'error';\n failure: LlmFailure;\n };\n }\n export interface GenericCallView {\n card: 'generic';\n title: string;\n kind?: ToolCallKind;\n rawInput?: unknown;\n content?: ContentBlock[];\n locations?: FileLocation[];\n }\n export interface GenericResultView {\n card: 'generic';\n title?: string;\n content?: ContentBlock[];\n }\n export interface JsonSchemaNode {\n type?: JsonSchemaType;\n oneOf?: JsonSchemaNode[];\n properties?: Record;\n required?: string[];\n additionalProperties?: boolean;\n items?: JsonSchemaNode;\n enum?: JsonSchemaScalar[];\n const?: JsonSchemaScalar;\n description?: string;\n title?: string;\n default?: JsonValue;\n examples?: JsonValue;\n }\n export type JsonSchemaScalar = string | number | boolean | null;\n export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';\n export type JsonValue = null | boolean | number | string | JsonValue[] | {\n [key: string]: JsonValue;\n };\n export interface LlmCallConfig {\n provider: string;\n model: string;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n }\n export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n }\n export interface Message {\n role: 'system' | 'user' | 'assistant';\n content: ContentBlock[];\n provenance?: AssistantProvenance;\n }\n export type MessageSource = MessageSourceMap[keyof MessageSourceMap];\n export interface MessageSourceMap {\n user: {\n kind: 'user';\n };\n plugin: {\n kind: 'plugin';\n plugin: string;\n };\n }\n export type ProviderRequestId = Branded<'ProviderRequestId'>;\n export interface ReasoningBlock {\n type: 'reasoning';\n text: string;\n }\n export type RequestHeaderReason = 'initial' | 'resume' | 'change';\n export type ScopeKey = object;\n export interface SendOptions {\n target: SendTarget;\n wakeup: boolean;\n source: MessageSource;\n }\n export type SendTarget = 'next-turn' | 'next-step';\n export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n get id(): SessionId;\n constructor(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader);\n get events(): readonly SessionEvent[];\n get seq(): number;\n append(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent;\n requestHeader(): EpochHeader | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n }\n export type SessionEvent = {\n [K in SessionEventType]: {\n type: K;\n seq: number;\n time: number;\n data: SessionEventMap[K];\n } & (K extends SurfaceEventType ? {\n sourceEventSeqs?: number[];\n surfaceOp?: SurfaceOp;\n } : object);\n }[T];\n export interface SessionEventMap {\n 'turn/start': {\n turn: number;\n trigger: TurnTrigger;\n };\n 'turn/end': {\n turn: number;\n reason: TurnEndReason;\n };\n 'step/start': {\n turn: number;\n step: number;\n };\n 'step/end': {\n turn: number;\n step: number;\n };\n 'user/message': UserMessageData;\n 'prompt/blocked': {\n content: ContentBlock[];\n source: MessageSource;\n reason: string;\n };\n 'assistant/chunk': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n 'assistant/message': {\n turn: number;\n step: number;\n content: ContentBlock[];\n provenance: AssistantProvenance;\n usage?: TokenUsage;\n };\n 'tool/call': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n 'tool/result': {\n turn: number;\n step: number;\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n 'steering/message': UserMessageData & {\n turn: number;\n };\n 'todo/write': {\n todos: TodoItem[];\n };\n 'request/header': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n }\n export type SessionEventType = keyof SessionEventMap;\n export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly delegationDepth?: number;\n }\n export type SessionId = Branded<'SessionId'>;\n export interface SessionSurface {\n readonly nodes: readonly number[];\n readonly replaceGeneration: number;\n }\n export type StreamChunk = {\n type: 'block-start';\n index: number;\n blockType: ContentBlockType;\n } | {\n type: 'text-delta';\n index: number;\n text: string;\n } | {\n type: 'reasoning-delta';\n index: number;\n text: string;\n } | {\n type: 'tool-call-delta';\n index: number;\n id: CallId;\n name?: string;\n argumentsDelta: string;\n } | {\n type: 'block-end';\n index: number;\n block: ContentBlock;\n } | {\n type: 'usage';\n usage: TokenUsage;\n } | {\n type: 'finish';\n reason: FinishReason;\n replayState?: unknown;\n };\n export type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result' | 'steering/message';\n export interface SurfaceIntent {\n surfaceOp: SurfaceOp;\n sourceEventSeqs?: number[];\n }\n export type SurfaceOp = 'append' | {\n op: 'replace';\n start: number;\n end: number;\n };\n export interface TerminalCallView {\n card: 'terminal';\n title: string;\n description?: string;\n cwd?: string;\n }\n export interface TerminalResultView {\n card: 'terminal';\n title?: string;\n output?: string;\n exitCode?: number;\n signal?: string;\n }\n export interface TodoItem {\n content: string;\n status: 'pending' | 'in_progress' | 'completed';\n }\n export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n }\n export interface ToolCallBlock {\n type: 'tool-call';\n id: CallId;\n name: string;\n arguments: string;\n }\n export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\n export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\n export interface ToolDefinition extends ToolSchema {\n readonly output: ToolOutputDefinition;\n execute(args: unknown, exec: ToolRunContext): Promise;\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n timeoutMs?: number;\n isConcurrencySafe?(args: unknown): boolean;\n presentCall?(args: unknown): ToolCallView | undefined;\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\n }\n export interface ToolErrorInfo {\n name: string;\n code: string;\n }\n export interface ToolExecution extends ToolExecutionInput {\n readonly token: ToolExecutionToken;\n }\n export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: AdditionalContext[];\n readonly concludesTurn?: never;\n }\n export interface ToolExecutionInput {\n readonly callId: CallId;\n readonly name: string;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n }\n export type ToolExecutionMode = {\n kind: 'parallel';\n } | {\n kind: 'exclusive';\n };\n export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure;\n export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: AdditionalContext[];\n readonly concludesTurn?: true;\n }\n export type ToolExecutionToken = symbol & {\n readonly [toolExecutionTokenBrand]: true;\n };\n export interface ToolFailure {\n message: string;\n info?: ToolErrorInfo;\n }\n export type ToolGuard = (execution: Readonly) => string | undefined;\n export interface ToolOutputDefinition {\n readonly schema: JsonSchemaNode;\n render(args: unknown, value: JsonValue): ContentBlock[];\n presentationMeta?(args: unknown, value: JsonValue): JsonValue;\n }\n export interface ToolRestriction {\n readonly allow?: readonly string[];\n readonly deny?: readonly string[];\n }\n export interface ToolResult {\n content: ContentBlock[];\n isError: boolean;\n meta?: JsonValue;\n }\n export interface ToolResultBlock {\n type: 'tool-result';\n toolCallId: CallId;\n content: ContentBlock[];\n isError?: boolean;\n }\n export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView;\n export interface ToolRunContext extends ToolExecution {\n deferContext(context: AdditionalContext): void;\n concludeTurn(): void;\n }\n export interface ToolSchema {\n name: string;\n description: string;\n parameters: Record;\n }\n export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];\n export interface TurnEndReasonMap {\n completed: {\n kind: 'completed';\n };\n aborted: {\n kind: 'aborted';\n };\n error: {\n kind: 'error';\n step: number;\n } & ({\n failure: LlmFailure;\n message?: never;\n code?: never;\n } | {\n message: string;\n code?: string;\n failure?: never;\n });\n disposed: {\n kind: 'disposed';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n rejected: {\n kind: 'rejected';\n reason: string;\n };\n interrupted: {\n kind: 'interrupted';\n };\n }\n export type TurnTrigger = TurnTriggerMap[keyof TurnTriggerMap];\n export interface TurnTriggerMap {\n message: {\n kind: 'message';\n source: MessageSource;\n };\n retry: {\n kind: 'retry';\n };\n injection: {\n kind: 'injection';\n source: MessageSource;\n };\n }\n export interface UserMessageData {\n content: ContentBlock[];\n source: MessageSource;\n }"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"inspect-tools-api","status":"completed","content":[{"type":"content","content":{"type":"text","text":"## api\n- tools — Tool registry and execution pipeline.\n /**\n * Register globally or in the calling agent scope. Scoped tools shadow\n * globals; duplicates within one layer and the reserved `run_code` name fail.\n * @param definition - tool schema, execution, and optional finalization/presentation callbacks.\n * @returns the exact disposer that unregisters the tool.\n */\n register(definition: ToolDefinition): () => void\n /**\n * Restrict global tools for the calling agent scope. Empty filters, unknown\n * names, scope-local names, and reserved transport names fail. Restrictions\n * intersect; scoped registrations remain visible.\n * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).\n * @returns the exact disposer that lifts this restriction.\n */\n restrict(filter: ToolRestriction): () => void\n /**\n * Register a monotonic guard after the extensible `tools/pre-execute`\n * waterfall. A plain-context guard applies globally; one registered through\n * `agent.ctx` applies only to that agent. Any matching guard may deny by\n * returning a reason, while no guard can force-allow a call another guard\n * denied. The exact effect disposer is returned for ordered ownership and\n * HMR cleanup.\n * @param guard - synchronous check; a returned string denies the execution.\n * @returns the exact disposer that unregisters the guard.\n */\n guard(guard: ToolGuard): () => void\n /**\n * Look up a tool as one scope sees it (scoped\n * shadows global; a restricted-away global reads as absent). Presenters pass\n * the calling agent so the rendered card matches the definition that\n * actually executed.\n * @param name - the tool name as registered.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns the definition the scope resolves, or undefined when none is visible.\n */\n get(name: string, scope?: ScopeKey): ToolDefinition | undefined\n /**\n * Project visible definitions onto the allowlisted model-facing schema fields,\n * excluding execution and presentation callbacks.\n * @param scope - the viewing scope (the agent); omitted = the global view.\n * @returns one deep-cloned schema per visible tool.\n */\n schemas(scope?: ScopeKey): ToolSchema[]\n /**\n * Classify a pending call through the caller's visible tool definition. Only\n * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or\n * throwing classifiers are exclusive.\n * @param exec - call name, parsed arguments, and optional agent scope.\n * @returns the fail-closed scheduling mode.\n */\n executionMode(exec: ToolExecutionInput): ToolExecutionMode\n /**\n * Execute through pre-policy, guards, around-dispatch, post-policy,\n * definition-owned content finalization, and final notification. Tool and\n * listener failures resolve as materialized error results; an invisible tool\n * reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen\n * snapshot final observers receive. Cancellation\n * arriving after entry and before final result materialization skips a\n * not-yet-started body with `ABORTED_BEFORE_DISPATCH` or replaces a\n * successful started outcome with `ABORTED`; already-started work is still\n * drained and may retain a tool-owned structured error.\n * @param exec - the typed same-process call input. The registry assigns its\n * correlation token before policy begins.\n * @returns the materialized final result.\n */\n async execute(exec: ToolExecutionInput): Promise\ntype shapes (referenced by the signatures above — read these before assuming a field is a string):\n export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly status: AgentStatus;\n readonly ctx: Context;\n send(input: UserMessageData, options: SendOptions): AgentMessageId;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise;\n followup(input: UserMessageData): AgentMessageId;\n steer(input: UserMessageData): AgentMessageId;\n inject(input: UserMessageData): AgentMessageId;\n retry(): void;\n }\n export type AgentCancelCause = {\n readonly kind: 'user';\n } | {\n readonly kind: 'parent';\n };\n export type AgentMessageId = Branded<'AgentMessageId'>;\n export interface AgentOptions {\n provider?: string;\n model?: string;\n }\n export type AgentStatus = 'idle' | 'running';\n export interface AssistantProvenance {\n provider: string;\n model: string;\n replayState?: unknown;\n }\n export type Branded = string & {\n readonly [BRAND]: B;\n };\n export type CallId = Branded<'CallId'>;\n export interface CancelOptions {\n keepInbox?: boolean;\n }\n export interface ContentBlockMap {\n 'text': TextBlock;\n 'reasoning': ReasoningBlock;\n 'tool-call': ToolCallBlock;\n 'tool-result': ToolResultBlock;\n }\n export type ContentBlockType = keyof ContentBlockMap;\n export interface DiffCallView {\n card: 'diff';\n title: string;\n diffs: FileDiff[];\n locations?: FileLocation[];\n }\n export interface DiffResultView {\n card: 'diff';\n title?: string;\n diffs: FileDiff[];\n }\n export interface EpochHeader {\n config: LlmCallConfig;\n system?: string;\n tools?: ToolSchema[];\n }\n export interface FileDiff {\n path: string;\n oldText: string | null;\n newText: string;\n }\n export interface FileLocation {\n path: string;\n line?: number;\n }\n export type FinishReason = FinishReasonMap[keyof FinishReasonMap];\n export interface FinishReasonMap {\n 'stop': {\n kind: 'stop';\n };\n 'tool-calls': {\n kind: 'tool-calls';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n 'aborted': {\n kind: 'aborted';\n failure: LlmFailure;\n };\n 'error': {\n kind: 'error';\n failure: LlmFailure;\n };\n }\n export interface GenericCallView {\n card: 'generic';\n title: string;\n kind?: ToolCallKind;\n rawInput?: unknown;\n content?: ContentBlock[];\n locations?: FileLocation[];\n }\n export interface GenericResultView {\n card: 'generic';\n title?: string;\n content?: ContentBlock[];\n }\n export interface JsonSchemaNode {\n type?: JsonSchemaType;\n oneOf?: JsonSchemaNode[];\n properties?: Record;\n required?: string[];\n additionalProperties?: boolean;\n items?: JsonSchemaNode;\n enum?: JsonSchemaScalar[];\n const?: JsonSchemaScalar;\n description?: string;\n title?: string;\n default?: JsonValue;\n examples?: JsonValue;\n }\n export type JsonSchemaScalar = string | number | boolean | null;\n export type JsonSchemaType = 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null';\n export type JsonValue = null | boolean | number | string | JsonValue[] | {\n [key: string]: JsonValue;\n };\n export interface LlmCallConfig {\n provider: string;\n model: string;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n }\n export interface LlmFailure {\n readonly message: string;\n readonly code: string;\n readonly status?: number;\n readonly providerRetryAfterMs?: number;\n readonly requestId?: ProviderRequestId;\n }\n export interface Message {\n role: 'system' | 'user' | 'assistant';\n content: ContentBlock[];\n provenance?: AssistantProvenance;\n }\n export type MessageSource = MessageSourceMap[keyof MessageSourceMap];\n export interface MessageSourceMap {\n user: {\n kind: 'user';\n };\n plugin: {\n kind: 'plugin';\n plugin: string;\n };\n }\n export type ProviderRequestId = Branded<'ProviderRequestId'>;\n export interface ReasoningBlock {\n type: 'reasoning';\n text: string;\n }\n export type RequestHeaderReason = 'initial' | 'resume' | 'change';\n export type ScopeKey = object;\n export interface SendOptions {\n target: SendTarget;\n wakeup: boolean;\n }\n export type SendTarget = 'next-turn' | 'next-step';\n export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n get id(): SessionId;\n constructor(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader);\n get events(): readonly SessionEvent[];\n get seq(): number;\n append(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [\n opts: SurfaceIntent\n ] : [\n ]): SessionEvent;\n requestHeader(): EpochHeader | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n }\n export type SessionEvent = {\n [K in SessionEventType]: {\n type: K;\n seq: number;\n time: number;\n data: SessionEventMap[K];\n } & (K extends SurfaceEventType ? {\n sourceEventSeqs?: number[];\n surfaceOp?: SurfaceOp;\n } : object);\n }[T];\n export interface SessionEventMap {\n 'turn/start': {\n turn: number;\n trigger: TurnTrigger;\n };\n 'turn/end': {\n turn: number;\n reason: TurnEndReason;\n };\n 'step/start': {\n turn: number;\n step: number;\n };\n 'step/end': {\n turn: number;\n step: number;\n };\n 'user/message': UserMessageData;\n 'assistant/chunk': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n 'assistant/message': {\n turn: number;\n step: number;\n content: ContentBlock[];\n provenance: AssistantProvenance;\n usage?: TokenUsage;\n };\n 'tool/call': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n 'tool/result': {\n turn: number;\n step: number;\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n 'steering/message': UserMessageData & {\n turn: number;\n };\n 'todo/write': {\n todos: TodoItem[];\n };\n 'request/header': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n }\n export type SessionEventType = keyof SessionEventMap;\n export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly delegationDepth?: number;\n }\n export type SessionId = Branded<'SessionId'>;\n export interface SessionSurface {\n readonly nodes: readonly number[];\n readonly replaceGeneration: number;\n }\n export type StreamChunk = {\n type: 'block-start';\n index: number;\n blockType: ContentBlockType;\n } | {\n type: 'text-delta';\n index: number;\n text: string;\n } | {\n type: 'reasoning-delta';\n index: number;\n text: string;\n } | {\n type: 'tool-call-delta';\n index: number;\n id: CallId;\n name?: string;\n argumentsDelta: string;\n } | {\n type: 'block-end';\n index: number;\n block: ContentBlock;\n } | {\n type: 'usage';\n usage: TokenUsage;\n } | {\n type: 'finish';\n reason: FinishReason;\n replayState?: unknown;\n };\n export type SurfaceEventType = 'user/message' | 'assistant/message' | 'tool/result' | 'steering/message';\n export interface SurfaceIntent {\n surfaceOp: SurfaceOp;\n sourceEventSeqs?: number[];\n }\n export type SurfaceOp = 'append' | {\n op: 'replace';\n start: number;\n end: number;\n };\n export interface TerminalCallView {\n card: 'terminal';\n title: string;\n description?: string;\n cwd?: string;\n }\n export interface TerminalResultView {\n card: 'terminal';\n title?: string;\n output?: string;\n exitCode?: number;\n signal?: string;\n }\n export interface TodoItem {\n content: string;\n status: 'pending' | 'in_progress' | 'completed';\n }\n export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n }\n export interface ToolCallBlock {\n type: 'tool-call';\n id: CallId;\n name: string;\n arguments: string;\n }\n export type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\n export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\n export interface ToolDefinition extends ToolSchema {\n readonly output: ToolOutputDefinition;\n execute(args: unknown, exec: ToolRunContext): Promise;\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n timeoutMs?: number;\n isConcurrencySafe?(args: unknown): boolean;\n presentCall?(args: unknown): ToolCallView | undefined;\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\n }\n export interface ToolErrorInfo {\n name: string;\n code: string;\n }\n export interface ToolExecution extends ToolExecutionInput {\n readonly token: ToolExecutionToken;\n }\n export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessageData[];\n readonly concludesTurn?: never;\n }\n export interface ToolExecutionInput {\n readonly callId: CallId;\n readonly name: string;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n }\n export type ToolExecutionMode = {\n kind: 'parallel';\n } | {\n kind: 'exclusive';\n };\n export type ToolExecutionResult = ToolExecutionSuccess | ToolExecutionFailure;\n export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessageData[];\n readonly concludesTurn?: true;\n }\n export type ToolExecutionToken = symbol & {\n readonly [toolExecutionTokenBrand]: true;\n };\n export interface ToolFailure {\n message: string;\n info?: ToolErrorInfo;\n }\n export type ToolGuard = (execution: Readonly) => string | undefined;\n export interface ToolOutputDefinition {\n readonly schema: JsonSchemaNode;\n render(args: unknown, value: JsonValue): ContentBlock[];\n presentationMeta?(args: unknown, value: JsonValue): JsonValue;\n }\n export interface ToolRestriction {\n readonly allow?: readonly string[];\n readonly deny?: readonly string[];\n }\n export interface ToolResult {\n content: ContentBlock[];\n isError: boolean;\n meta?: JsonValue;\n }\n export interface ToolResultBlock {\n type: 'tool-result';\n toolCallId: CallId;\n content: ContentBlock[];\n isError?: boolean;\n }\n export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView;\n export interface ToolRunContext extends ToolExecution {\n deferContext(context: UserMessageData): void;\n concludeTurn(): void;\n }\n export interface ToolSchema {\n name: string;\n description: string;\n parameters: Record;\n }\n export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap];\n export interface TurnEndReasonMap {\n completed: {\n kind: 'completed';\n };\n aborted: {\n kind: 'aborted';\n };\n error: {\n kind: 'error';\n step: number;\n } & ({\n failure: LlmFailure;\n message?: never;\n code?: never;\n } | {\n message: string;\n code?: string;\n failure?: never;\n });\n disposed: {\n kind: 'disposed';\n };\n 'max-tokens': {\n kind: 'max-tokens';\n };\n interrupted: {\n kind: 'interrupted';\n };\n }\n export type TurnTrigger = TurnTriggerMap[keyof TurnTriggerMap];\n export interface TurnTriggerMap {\n message: {\n kind: 'message';\n source: MessageSource;\n };\n retry: {\n kind: 'retry';\n };\n injection: {\n kind: 'injection';\n source: MessageSource;\n };\n }\n export interface UserMessageData {\n content: ContentBlock[];\n source: MessageSource;\n }"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"inspect-tools-event","title":"Inspect cordis runtime: events: tools/pre-execute","kind":"read","status":"in_progress"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"inspect-tools-event","status":"completed","content":[{"type":"content","content":{"type":"text","text":"## events\n- tools/pre-execute [waterfall] — Allow, deny, or ask before dispatch.\n /**\n * Allow, deny, or ask before dispatch. `next()` delegates to allow; missing\n * approval support turns `ask` into denial. Async gates must observe\n * `exec.signal`; the registry rechecks cancellation after they settle but\n * never abandons their promise.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.\n * @param exec - the pending call (name, parsed arguments, caller agent).\n * @mode waterfall\n */\n 'tools/pre-execute'(this: Scoped, exec: ToolExecution, next: () => Promise): Promise\nwaterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() vetoes the chain."}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"CORDIS_INSPECT_JSDOC_OK"}}}} diff --git a/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl b/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl index 4438550b97..c3539505c0 100644 --- a/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl +++ b/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl @@ -1,189 +1,190 @@ {"type":"session","version":0,"id":"f3cbd087-fb45-4b32-b0f2-3082d65bfcb4","createdAt":1783860675270,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-cbBLh2","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783860675271,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"permission/preset","seq":1,"time":1783962245380,"data":{"preset":"workspace-write"}} -{"type":"sandbox/mode","seq":2,"time":1784518116517,"data":{"mode":"workspace-write"}} -{"type":"approval/policy","seq":3,"time":1783962245380,"data":{"policy":"ask"}} -{"type":"user/message","seq":4,"time":1783962245380,"data":{"content":[{"type":"text","text":"The sandbox already denied writing /tmp/dsh-escalated.txt earlier (it is outside this workspace). Retry it now exactly once: one single bash call with the command printf 'escalated\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt, with sandbox_permissions set to danger-full-access and the justification 'the user asked to write a file outside the workspace'. Do not run it without sandbox_permissions first. I will approve the permission prompt. After the result, reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"session/title","seq":5,"time":1783962245380,"data":{"title":"The sandbox already denied writing","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":6,"time":1783962245382,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":7,"time":1783962245382,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"assistant/chunk","seq":8,"time":1783860676464,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":9,"time":1783860676465,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"user/message","seq":1,"time":1784903374565,"data":{"content":[{"type":"text","text":"The sandbox already denied writing /tmp/dsh-escalated.txt earlier (it is outside this workspace). Retry it now exactly once: one single bash call with the command printf 'escalated\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt, with sandbox_permissions set to danger-full-access and the justification 'the user asked to write a file outside the workspace'. Do not run it without sandbox_permissions first. I will approve the permission prompt. After the result, reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"session/title","seq":2,"time":1784903374565,"data":{"title":"The sandbox already denied writing","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"permission/preset","seq":3,"time":1784903374584,"data":{"preset":"workspace-write"}} +{"type":"sandbox/mode","seq":4,"time":1784903374584,"data":{"mode":"workspace-write"}} +{"type":"approval/policy","seq":5,"time":1784903374584,"data":{"policy":"ask"}} +{"type":"step/start","seq":6,"time":1784903374585,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":7,"time":1784903374585,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":8,"time":1784903374594,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":9,"time":1783860676464,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} {"type":"assistant/chunk","seq":10,"time":1783860676465,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} {"type":"assistant/chunk","seq":11,"time":1783860676465,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} {"type":"assistant/chunk","seq":12,"time":1783860676465,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":13,"time":1783860676499,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":13,"time":1783860676465,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} {"type":"assistant/chunk","seq":14,"time":1783860676499,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} {"type":"assistant/chunk","seq":15,"time":1783860676499,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} -{"type":"assistant/chunk","seq":16,"time":1783860676522,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} -{"type":"assistant/chunk","seq":17,"time":1783860676525,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":16,"time":1783860676499,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} +{"type":"assistant/chunk","seq":17,"time":1783860676522,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} {"type":"assistant/chunk","seq":18,"time":1783860676525,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sand"}}} {"type":"assistant/chunk","seq":19,"time":1783860676525,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"box"}}} -{"type":"assistant/chunk","seq":20,"time":1783860676553,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_per"}}} +{"type":"assistant/chunk","seq":20,"time":1783860676525,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_per"}}} {"type":"assistant/chunk","seq":21,"time":1783860676553,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"missions"}}} -{"type":"assistant/chunk","seq":22,"time":1783860676554,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" set"}}} +{"type":"assistant/chunk","seq":22,"time":1783860676553,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" set"}}} {"type":"assistant/chunk","seq":23,"time":1783860676554,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} {"type":"assistant/chunk","seq":24,"time":1783860676554,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" danger"}}} -{"type":"assistant/chunk","seq":25,"time":1783860676583,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-full"}}} +{"type":"assistant/chunk","seq":25,"time":1783860676554,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-full"}}} {"type":"assistant/chunk","seq":26,"time":1783860676583,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-access"}}} -{"type":"assistant/chunk","seq":27,"time":1783860676611,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":28,"time":1783860676639,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" no"}}} -{"type":"assistant/chunk","seq":29,"time":1783860676640,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" prior"}}} -{"type":"assistant/chunk","seq":30,"time":1783860676672,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} -{"type":"assistant/chunk","seq":31,"time":1783860676673,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" needed"}}} -{"type":"assistant/chunk","seq":32,"time":1783860676705,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":33,"time":1783860676728,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" justified"}}} -{"type":"assistant/chunk","seq":34,"time":1783860676787,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" as"}}} -{"type":"assistant/chunk","seq":35,"time":1783860676788,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" instructed"}}} -{"type":"assistant/chunk","seq":36,"time":1783860676816,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":27,"time":1783860676583,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":28,"time":1783860676611,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" no"}}} +{"type":"assistant/chunk","seq":29,"time":1783860676639,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" prior"}}} +{"type":"assistant/chunk","seq":30,"time":1783860676640,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} +{"type":"assistant/chunk","seq":31,"time":1783860676672,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" needed"}}} +{"type":"assistant/chunk","seq":32,"time":1783860676673,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":33,"time":1783860676705,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" justified"}}} +{"type":"assistant/chunk","seq":34,"time":1783860676728,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" as"}}} +{"type":"assistant/chunk","seq":35,"time":1783860676787,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" instructed"}}} +{"type":"assistant/chunk","seq":36,"time":1783860676788,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} {"type":"assistant/chunk","seq":37,"time":1783860676816,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":38,"time":1783860676816,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":""}}} {"type":"assistant/chunk","seq":39,"time":1783860676816,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"{"}}} -{"type":"assistant/chunk","seq":40,"time":1783860676845,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":40,"time":1783860676816,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":41,"time":1783860676845,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"command"}}} -{"type":"assistant/chunk","seq":42,"time":1783860676846,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":42,"time":1783860676845,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":43,"time":1783860676846,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":": "}}} -{"type":"assistant/chunk","seq":44,"time":1783860676878,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":44,"time":1783860676846,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":45,"time":1783860676878,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"printf"}}} {"type":"assistant/chunk","seq":46,"time":1783860676878,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" '"}}} {"type":"assistant/chunk","seq":47,"time":1783860676878,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"es"}}} {"type":"assistant/chunk","seq":48,"time":1783860676878,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"cal"}}} -{"type":"assistant/chunk","seq":49,"time":1783860676879,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"ated"}}} -{"type":"assistant/chunk","seq":50,"time":1783860676909,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\\\\n"}}} -{"type":"assistant/chunk","seq":51,"time":1783860676911,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"'"}}} -{"type":"assistant/chunk","seq":52,"time":1783860676912,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" >"}}} +{"type":"assistant/chunk","seq":49,"time":1783860676878,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"ated"}}} +{"type":"assistant/chunk","seq":50,"time":1783860676879,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\\\\n"}}} +{"type":"assistant/chunk","seq":51,"time":1783860676909,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"'"}}} +{"type":"assistant/chunk","seq":52,"time":1783860676911,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" >"}}} {"type":"assistant/chunk","seq":53,"time":1783860676912,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" /"}}} {"type":"assistant/chunk","seq":54,"time":1783860676912,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"tmp"}}} -{"type":"assistant/chunk","seq":55,"time":1783860676939,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"/d"}}} -{"type":"assistant/chunk","seq":56,"time":1783860676941,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"sh"}}} +{"type":"assistant/chunk","seq":55,"time":1783860676912,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"/d"}}} +{"type":"assistant/chunk","seq":56,"time":1783860676939,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"sh"}}} {"type":"assistant/chunk","seq":57,"time":1783860676941,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"-es"}}} {"type":"assistant/chunk","seq":58,"time":1783860676941,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"cal"}}} {"type":"assistant/chunk","seq":59,"time":1783860676941,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"ated"}}} -{"type":"assistant/chunk","seq":60,"time":1783860676967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":".txt"}}} -{"type":"assistant/chunk","seq":61,"time":1783860676969,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" &&"}}} +{"type":"assistant/chunk","seq":60,"time":1783860676941,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":61,"time":1783860676967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" &&"}}} {"type":"assistant/chunk","seq":62,"time":1783860676969,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" cat"}}} -{"type":"assistant/chunk","seq":63,"time":1783860676970,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" /"}}} +{"type":"assistant/chunk","seq":63,"time":1783860676969,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" /"}}} {"type":"assistant/chunk","seq":64,"time":1783860676970,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"tmp"}}} {"type":"assistant/chunk","seq":65,"time":1783860676970,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"/d"}}} -{"type":"assistant/chunk","seq":66,"time":1783860677003,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"sh"}}} +{"type":"assistant/chunk","seq":66,"time":1783860676970,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"sh"}}} {"type":"assistant/chunk","seq":67,"time":1783860677003,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"-es"}}} {"type":"assistant/chunk","seq":68,"time":1783860677003,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"cal"}}} {"type":"assistant/chunk","seq":69,"time":1783860677003,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"ated"}}} -{"type":"assistant/chunk","seq":70,"time":1783860677004,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":70,"time":1783860677003,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":".txt"}}} {"type":"assistant/chunk","seq":71,"time":1783860677004,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" &&"}}} -{"type":"assistant/chunk","seq":72,"time":1783860677026,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" rm"}}} +{"type":"assistant/chunk","seq":72,"time":1783860677004,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" rm"}}} {"type":"assistant/chunk","seq":73,"time":1783860677026,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" /"}}} {"type":"assistant/chunk","seq":74,"time":1783860677026,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"tmp"}}} {"type":"assistant/chunk","seq":75,"time":1783860677026,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"/d"}}} {"type":"assistant/chunk","seq":76,"time":1783860677026,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"sh"}}} {"type":"assistant/chunk","seq":77,"time":1783860677026,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"-es"}}} -{"type":"assistant/chunk","seq":78,"time":1783860677055,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"cal"}}} -{"type":"assistant/chunk","seq":79,"time":1783860677085,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"ated"}}} -{"type":"assistant/chunk","seq":80,"time":1783860677087,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":78,"time":1783860677026,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"cal"}}} +{"type":"assistant/chunk","seq":79,"time":1783860677055,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"ated"}}} +{"type":"assistant/chunk","seq":80,"time":1783860677085,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":".txt"}}} {"type":"assistant/chunk","seq":81,"time":1783860677087,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":82,"time":1783860677087,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":", "}}} {"type":"assistant/chunk","seq":83,"time":1783860677087,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":84,"time":1783860677115,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":84,"time":1783860677087,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"description"}}} {"type":"assistant/chunk","seq":85,"time":1783860677115,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":86,"time":1783860677116,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":86,"time":1783860677115,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":87,"time":1783860677116,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":88,"time":1783860677146,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"Write"}}} -{"type":"assistant/chunk","seq":89,"time":1783860677147,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" file"}}} -{"type":"assistant/chunk","seq":90,"time":1783860677148,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" outside"}}} -{"type":"assistant/chunk","seq":91,"time":1783860677174,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" workspace"}}} -{"type":"assistant/chunk","seq":92,"time":1783860677202,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" and"}}} +{"type":"assistant/chunk","seq":88,"time":1783860677116,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"Write"}}} +{"type":"assistant/chunk","seq":89,"time":1783860677146,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" file"}}} +{"type":"assistant/chunk","seq":90,"time":1783860677147,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" outside"}}} +{"type":"assistant/chunk","seq":91,"time":1783860677148,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" workspace"}}} +{"type":"assistant/chunk","seq":92,"time":1783860677174,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" and"}}} {"type":"assistant/chunk","seq":93,"time":1783860677202,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" verify"}}} {"type":"assistant/chunk","seq":94,"time":1783860677202,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":95,"time":1783860677202,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":", "}}} {"type":"assistant/chunk","seq":96,"time":1783860677202,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":97,"time":1783860677238,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"sand"}}} +{"type":"assistant/chunk","seq":97,"time":1783860677202,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"sand"}}} {"type":"assistant/chunk","seq":98,"time":1783860677238,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"box"}}} {"type":"assistant/chunk","seq":99,"time":1783860677238,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"_per"}}} {"type":"assistant/chunk","seq":100,"time":1783860677238,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"missions"}}} -{"type":"assistant/chunk","seq":101,"time":1783860677275,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":101,"time":1783860677238,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":102,"time":1783860677275,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":": "}}} -{"type":"assistant/chunk","seq":103,"time":1783860677276,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":103,"time":1783860677275,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":104,"time":1783860677276,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"danger"}}} -{"type":"assistant/chunk","seq":105,"time":1783860677292,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"-full"}}} -{"type":"assistant/chunk","seq":106,"time":1783860677293,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"-access"}}} -{"type":"assistant/chunk","seq":107,"time":1783860677320,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":108,"time":1783860677321,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":105,"time":1783860677276,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"-full"}}} +{"type":"assistant/chunk","seq":106,"time":1783860677292,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"-access"}}} +{"type":"assistant/chunk","seq":107,"time":1783860677293,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":108,"time":1783860677320,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":", "}}} {"type":"assistant/chunk","seq":109,"time":1783860677321,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":110,"time":1783860677321,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"just"}}} -{"type":"assistant/chunk","seq":111,"time":1783860677349,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"ification"}}} +{"type":"assistant/chunk","seq":111,"time":1783860677321,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"ification"}}} {"type":"assistant/chunk","seq":112,"time":1783860677349,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":113,"time":1783860677349,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":114,"time":1783860677349,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":115,"time":1783860677349,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"the"}}} {"type":"assistant/chunk","seq":116,"time":1783860677349,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" user"}}} -{"type":"assistant/chunk","seq":117,"time":1783860677388,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" asked"}}} +{"type":"assistant/chunk","seq":117,"time":1783860677349,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" asked"}}} {"type":"assistant/chunk","seq":118,"time":1783860677388,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" to"}}} {"type":"assistant/chunk","seq":119,"time":1783860677388,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" write"}}} {"type":"assistant/chunk","seq":120,"time":1783860677388,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" a"}}} -{"type":"assistant/chunk","seq":121,"time":1783860677492,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" file"}}} -{"type":"assistant/chunk","seq":122,"time":1783860677493,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" outside"}}} +{"type":"assistant/chunk","seq":121,"time":1783860677388,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" file"}}} +{"type":"assistant/chunk","seq":122,"time":1783860677492,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" outside"}}} {"type":"assistant/chunk","seq":123,"time":1783860677493,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" the"}}} {"type":"assistant/chunk","seq":124,"time":1783860677493,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":" workspace"}}} {"type":"assistant/chunk","seq":125,"time":1783860677493,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":126,"time":1783860677493,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","argumentsDelta":"}"}}} {"type":"assistant/chunk","seq":127,"time":1783860677493,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run a command with sandbox_permissions set to danger-full-access, no prior run needed, justified as instructed."}}}} -{"type":"assistant/chunk","seq":128,"time":1783962245385,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write file outside workspace and verify\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}}}} +{"type":"assistant/chunk","seq":128,"time":1783860677493,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write file outside workspace and verify\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}}}} {"type":"assistant/chunk","seq":129,"time":1783962245385,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1501,"outputTokens":174,"cacheReadTokens":0,"reasoningTokens":28}}}} {"type":"assistant/chunk","seq":130,"time":1783962245385,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":131,"time":1783962245385,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a command with sandbox_permissions set to danger-full-access, no prior run needed, justified as instructed."},{"type":"tool-call","id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write file outside workspace and verify\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":1501,"outputTokens":174,"cacheReadTokens":0,"reasoningTokens":28}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130],"surfaceOp":"append"} -{"type":"tool/call","seq":132,"time":1783962245385,"data":{"turn":1,"step":1,"callId":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write file outside workspace and verify\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}} -{"type":"approval/asked","seq":133,"time":1783962245386,"data":{"id":"efa76e4a-8e3e-4be0-9608-a68def7354b9","toolName":"bash","callId":"call_00_d0sAHpJ9mYOJi0z7KNy30441","reason":"escalate sandbox to danger-full-access: the user asked to write a file outside the workspace"}} -{"type":"approval/decided","seq":134,"time":1783962245387,"data":{"id":"efa76e4a-8e3e-4be0-9608-a68def7354b9","outcome":"allowed-once"}} -{"type":"tool/result","seq":135,"time":1783962245399,"data":{"turn":1,"step":1,"callId":"call_00_d0sAHpJ9mYOJi0z7KNy30441","content":[{"type":"text","text":"escalated\n"}],"isError":false},"sourceEventSeqs":[132],"surfaceOp":"append"} -{"type":"step/end","seq":136,"time":1783962245400,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":137,"time":1783962245400,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":138,"time":1783860678811,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":139,"time":1783860678811,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":140,"time":1783860678833,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} -{"type":"assistant/chunk","seq":141,"time":1783860678834,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" succeeded"}}} -{"type":"assistant/chunk","seq":142,"time":1783860678834,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" —"}}} -{"type":"assistant/chunk","seq":143,"time":1783860678868,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} -{"type":"assistant/chunk","seq":144,"time":1783860678868,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wrote"}}} -{"type":"assistant/chunk","seq":145,"time":1783860678868,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":146,"time":1783860678904,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} -{"type":"assistant/chunk","seq":147,"time":1783860678905,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":148,"time":1783860678926,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" read"}}} -{"type":"assistant/chunk","seq":149,"time":1783860678926,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} -{"type":"assistant/chunk","seq":150,"time":1783860678926,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" back"}}} -{"type":"assistant/chunk","seq":151,"time":1783860678975,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" ("}}} -{"type":"assistant/chunk","seq":152,"time":1783860678976,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"output"}}} -{"type":"assistant/chunk","seq":153,"time":1783860678976,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":154,"time":1783860678976,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"es"}}} -{"type":"assistant/chunk","seq":155,"time":1783860678976,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"cal"}}} -{"type":"assistant/chunk","seq":156,"time":1783860678976,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ated"}}} -{"type":"assistant/chunk","seq":157,"time":1783860678999,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"),"}}} -{"type":"assistant/chunk","seq":158,"time":1783860679001,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":159,"time":1783860679002,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" removed"}}} -{"type":"assistant/chunk","seq":160,"time":1783860679002,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} -{"type":"assistant/chunk","seq":161,"time":1783860679002,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":162,"time":1783860679016,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} -{"type":"assistant/chunk","seq":163,"time":1783860679017,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":164,"time":1783860679017,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} -{"type":"assistant/chunk","seq":165,"time":1783860679017,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":166,"time":1783860679046,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":167,"time":1783860679047,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":168,"time":1783860679048,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":169,"time":1783860679048,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":170,"time":1783860679079,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} -{"type":"assistant/chunk","seq":171,"time":1783860679079,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} -{"type":"assistant/chunk","seq":172,"time":1783860679103,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" D"}}} -{"type":"assistant/chunk","seq":173,"time":1783860679136,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} -{"type":"assistant/chunk","seq":174,"time":1783860679136,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" after"}}} -{"type":"assistant/chunk","seq":175,"time":1783860679136,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":176,"time":1783860679136,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" result"}}} -{"type":"assistant/chunk","seq":177,"time":1783860679136,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":178,"time":1783860679137,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":179,"time":1783860679137,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} -{"type":"assistant/chunk","seq":180,"time":1783860679137,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} -{"type":"assistant/chunk","seq":181,"time":1783860679137,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The command succeeded — it wrote the file, read it back (output \"escalated\"), and removed it. The user asked me to reply with the single word DONE after the result."}}}} -{"type":"assistant/chunk","seq":182,"time":1783962245402,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} -{"type":"assistant/chunk","seq":183,"time":1783962245402,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":27,"outputTokens":42,"cacheReadTokens":1664,"reasoningTokens":39}}}} -{"type":"assistant/chunk","seq":184,"time":1783962245402,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":185,"time":1783962245402,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The command succeeded — it wrote the file, read it back (output \"escalated\"), and removed it. The user asked me to reply with the single word DONE after the result."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":27,"outputTokens":42,"cacheReadTokens":1664,"reasoningTokens":39}},"sourceEventSeqs":[138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184],"surfaceOp":"append"} -{"type":"step/end","seq":186,"time":1783962245402,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":187,"time":1783962245402,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"assistant/message","seq":131,"time":1784903374599,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a command with sandbox_permissions set to danger-full-access, no prior run needed, justified as instructed."},{"type":"tool-call","id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write file outside workspace and verify\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":1501,"outputTokens":174,"cacheReadTokens":0,"reasoningTokens":28}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130],"surfaceOp":"append"} +{"type":"tool/call","seq":132,"time":1784903374599,"data":{"turn":1,"step":1,"callId":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write file outside workspace and verify\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}} +{"type":"approval/asked","seq":133,"time":1784903374607,"data":{"id":"f3896ad0-5828-4b45-970f-293e5c10f057","toolName":"bash","callId":"call_00_d0sAHpJ9mYOJi0z7KNy30441","reason":"escalate sandbox to danger-full-access: the user asked to write a file outside the workspace"}} +{"type":"approval/decided","seq":134,"time":1784903374608,"data":{"id":"f3896ad0-5828-4b45-970f-293e5c10f057","outcome":"allowed-once"}} +{"type":"tool/result","seq":135,"time":1784903374625,"data":{"turn":1,"step":1,"callId":"call_00_d0sAHpJ9mYOJi0z7KNy30441","content":[{"type":"text","text":"escalated\n"}],"isError":false},"sourceEventSeqs":[132],"surfaceOp":"append"} +{"type":"step/end","seq":136,"time":1784903374625,"data":{"turn":1,"step":1}} +{"type":"user/message","seq":137,"time":1784903374633,"data":{"content":[{"type":"text","text":"The approval policy changed from \"never\" to \"ask\" (changed by the operator/config)."}],"source":{"kind":"plugin","plugin":"user-approval"}},"surfaceOp":"append"} +{"type":"step/start","seq":138,"time":1783962245400,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":139,"time":1783860678811,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":140,"time":1783860678811,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":141,"time":1783860678833,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} +{"type":"assistant/chunk","seq":142,"time":1783860678834,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" succeeded"}}} +{"type":"assistant/chunk","seq":143,"time":1783860678834,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" —"}}} +{"type":"assistant/chunk","seq":144,"time":1783860678868,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":145,"time":1783860678868,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wrote"}}} +{"type":"assistant/chunk","seq":146,"time":1783860678868,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":147,"time":1783860678904,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":148,"time":1783860678905,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":149,"time":1783860678926,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" read"}}} +{"type":"assistant/chunk","seq":150,"time":1783860678926,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":151,"time":1783860678926,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" back"}}} +{"type":"assistant/chunk","seq":152,"time":1783860678975,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" ("}}} +{"type":"assistant/chunk","seq":153,"time":1783860678976,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"output"}}} +{"type":"assistant/chunk","seq":154,"time":1783860678976,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":155,"time":1783860678976,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"es"}}} +{"type":"assistant/chunk","seq":156,"time":1783860678976,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"cal"}}} +{"type":"assistant/chunk","seq":157,"time":1783860678976,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ated"}}} +{"type":"assistant/chunk","seq":158,"time":1783860678999,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"),"}}} +{"type":"assistant/chunk","seq":159,"time":1783860679001,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":160,"time":1783860679002,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" removed"}}} +{"type":"assistant/chunk","seq":161,"time":1783860679002,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":162,"time":1783860679002,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":163,"time":1783860679016,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} +{"type":"assistant/chunk","seq":164,"time":1783860679017,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":165,"time":1783860679017,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} +{"type":"assistant/chunk","seq":166,"time":1783860679017,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":167,"time":1783860679046,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":168,"time":1783860679047,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":169,"time":1783860679048,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":170,"time":1783860679048,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":171,"time":1783860679079,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":172,"time":1783860679079,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":173,"time":1783860679103,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" D"}}} +{"type":"assistant/chunk","seq":174,"time":1783860679136,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":175,"time":1783860679136,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" after"}}} +{"type":"assistant/chunk","seq":176,"time":1783860679136,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":177,"time":1783860679136,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" result"}}} +{"type":"assistant/chunk","seq":178,"time":1783860679136,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":179,"time":1783860679137,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":180,"time":1783860679137,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} +{"type":"assistant/chunk","seq":181,"time":1783860679137,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":182,"time":1783860679137,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The command succeeded — it wrote the file, read it back (output \"escalated\"), and removed it. The user asked me to reply with the single word DONE after the result."}}}} +{"type":"assistant/chunk","seq":183,"time":1783962245402,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":184,"time":1783962245402,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":27,"outputTokens":42,"cacheReadTokens":1664,"reasoningTokens":39}}}} +{"type":"assistant/chunk","seq":185,"time":1783962245402,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":186,"time":1783962245402,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The command succeeded — it wrote the file, read it back (output \"escalated\"), and removed it. The user asked me to reply with the single word DONE after the result."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":27,"outputTokens":42,"cacheReadTokens":1664,"reasoningTokens":39}},"sourceEventSeqs":[139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185],"surfaceOp":"append"} +{"type":"step/end","seq":187,"time":1783962245402,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":188,"time":1783962245402,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl b/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl index eb01e7443e..f1674483ca 100644 --- a/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl +++ b/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl @@ -1,216 +1,217 @@ {"type":"session","version":0,"id":"d692fe7f-7079-4ee4-8b06-f44fd026d4ea","createdAt":1783860679475,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-Hn29Od","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783860679476,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"permission/preset","seq":1,"time":1783962246267,"data":{"preset":"workspace-write"}} -{"type":"sandbox/mode","seq":2,"time":1784518117237,"data":{"mode":"workspace-write"}} -{"type":"approval/policy","seq":3,"time":1783962246267,"data":{"policy":"ask"}} -{"type":"user/message","seq":4,"time":1783962246267,"data":{"content":[{"type":"text","text":"The sandbox already denied writing /tmp/dsh-escalated.txt earlier (it is outside this workspace). Retry it now exactly once: one single bash call with the command printf 'escalated\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt, with sandbox_permissions set to danger-full-access and the justification 'the user asked to write a file outside the workspace'. Do not run it without sandbox_permissions first. I will reject the permission prompt; after the rejection, do not retry and do not work around it — explain in one short sentence and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"session/title","seq":5,"time":1783962246267,"data":{"title":"The sandbox already denied writing","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":6,"time":1783962246269,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":7,"time":1783962246269,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"assistant/chunk","seq":8,"time":1783860680779,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":9,"time":1783860680782,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"user/message","seq":1,"time":1784903375699,"data":{"content":[{"type":"text","text":"The sandbox already denied writing /tmp/dsh-escalated.txt earlier (it is outside this workspace). Retry it now exactly once: one single bash call with the command printf 'escalated\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt, with sandbox_permissions set to danger-full-access and the justification 'the user asked to write a file outside the workspace'. Do not run it without sandbox_permissions first. I will reject the permission prompt; after the rejection, do not retry and do not work around it — explain in one short sentence and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"session/title","seq":2,"time":1784903375699,"data":{"title":"The sandbox already denied writing","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"permission/preset","seq":3,"time":1784903375719,"data":{"preset":"workspace-write"}} +{"type":"sandbox/mode","seq":4,"time":1784903375719,"data":{"mode":"workspace-write"}} +{"type":"approval/policy","seq":5,"time":1784903375719,"data":{"policy":"ask"}} +{"type":"step/start","seq":6,"time":1784903375720,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":7,"time":1784903375721,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":8,"time":1784903375729,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":9,"time":1783860680779,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} {"type":"assistant/chunk","seq":10,"time":1783860680782,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} {"type":"assistant/chunk","seq":11,"time":1783860680782,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} -{"type":"assistant/chunk","seq":12,"time":1783860680830,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":13,"time":1783860680831,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":12,"time":1783860680782,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":13,"time":1783860680830,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} {"type":"assistant/chunk","seq":14,"time":1783860680831,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} -{"type":"assistant/chunk","seq":15,"time":1783860680859,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":15,"time":1783860680831,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} {"type":"assistant/chunk","seq":16,"time":1783860680859,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" specific"}}} -{"type":"assistant/chunk","seq":17,"time":1783860680868,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} -{"type":"assistant/chunk","seq":18,"time":1783860680871,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":17,"time":1783860680859,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} +{"type":"assistant/chunk","seq":18,"time":1783860680868,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} {"type":"assistant/chunk","seq":19,"time":1783860680871,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" `"}}} -{"type":"assistant/chunk","seq":20,"time":1783860680872,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"sand"}}} +{"type":"assistant/chunk","seq":20,"time":1783860680871,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"sand"}}} {"type":"assistant/chunk","seq":21,"time":1783860680872,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"box"}}} -{"type":"assistant/chunk","seq":22,"time":1783860680902,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_per"}}} -{"type":"assistant/chunk","seq":23,"time":1783860680903,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"missions"}}} +{"type":"assistant/chunk","seq":22,"time":1783860680872,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_per"}}} +{"type":"assistant/chunk","seq":23,"time":1783860680902,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"missions"}}} {"type":"assistant/chunk","seq":24,"time":1783860680903,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"`"}}} {"type":"assistant/chunk","seq":25,"time":1783860680903,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" set"}}} {"type":"assistant/chunk","seq":26,"time":1783860680903,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} {"type":"assistant/chunk","seq":27,"time":1783860680903,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" `"}}} -{"type":"assistant/chunk","seq":28,"time":1783860680937,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"danger"}}} -{"type":"assistant/chunk","seq":29,"time":1783860680938,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-full"}}} +{"type":"assistant/chunk","seq":28,"time":1783860680903,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"danger"}}} +{"type":"assistant/chunk","seq":29,"time":1783860680937,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-full"}}} {"type":"assistant/chunk","seq":30,"time":1783860680938,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-access"}}} -{"type":"assistant/chunk","seq":31,"time":1783860680956,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"`"}}} -{"type":"assistant/chunk","seq":32,"time":1783860680958,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":31,"time":1783860680938,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":32,"time":1783860680956,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} {"type":"assistant/chunk","seq":33,"time":1783860680958,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} {"type":"assistant/chunk","seq":34,"time":1783860680958,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" specific"}}} -{"type":"assistant/chunk","seq":35,"time":1783860680985,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" justification"}}} +{"type":"assistant/chunk","seq":35,"time":1783860680958,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" justification"}}} {"type":"assistant/chunk","seq":36,"time":1783860680985,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":37,"time":1783860681022,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" They"}}} -{"type":"assistant/chunk","seq":38,"time":1783860681024,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" explicitly"}}} +{"type":"assistant/chunk","seq":37,"time":1783860680985,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" They"}}} +{"type":"assistant/chunk","seq":38,"time":1783860681022,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" explicitly"}}} {"type":"assistant/chunk","seq":39,"time":1783860681024,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" said"}}} {"type":"assistant/chunk","seq":40,"time":1783860681024,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" NOT"}}} {"type":"assistant/chunk","seq":41,"time":1783860681024,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":42,"time":1783860681043,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} -{"type":"assistant/chunk","seq":43,"time":1783860681091,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":42,"time":1783860681024,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} +{"type":"assistant/chunk","seq":43,"time":1783860681043,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} {"type":"assistant/chunk","seq":44,"time":1783860681091,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" without"}}} {"type":"assistant/chunk","seq":45,"time":1783860681091,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sand"}}} {"type":"assistant/chunk","seq":46,"time":1783860681091,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"box"}}} {"type":"assistant/chunk","seq":47,"time":1783860681091,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_per"}}} {"type":"assistant/chunk","seq":48,"time":1783860681091,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"missions"}}} -{"type":"assistant/chunk","seq":49,"time":1783860681107,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" first"}}} +{"type":"assistant/chunk","seq":49,"time":1783860681091,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" first"}}} {"type":"assistant/chunk","seq":50,"time":1783860681107,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":51,"time":1783860681108,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} -{"type":"assistant/chunk","seq":52,"time":1783860681138,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":51,"time":1783860681107,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":52,"time":1783860681108,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} {"type":"assistant/chunk","seq":53,"time":1783860681138,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" do"}}} -{"type":"assistant/chunk","seq":54,"time":1783860681251,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":54,"time":1783860681138,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} {"type":"assistant/chunk","seq":55,"time":1783860681251,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} {"type":"assistant/chunk","seq":56,"time":1783860681251,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} {"type":"assistant/chunk","seq":57,"time":1783860681251,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":58,"time":1783860681252,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":58,"time":1783860681251,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":""}}} {"type":"assistant/chunk","seq":59,"time":1783860681252,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"{"}}} {"type":"assistant/chunk","seq":60,"time":1783860681252,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":61,"time":1783860681252,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"command"}}} -{"type":"assistant/chunk","seq":62,"time":1783860681280,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":63,"time":1783860681281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":62,"time":1783860681252,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":63,"time":1783860681280,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":64,"time":1783860681281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":65,"time":1783860681324,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"printf"}}} +{"type":"assistant/chunk","seq":65,"time":1783860681281,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"printf"}}} {"type":"assistant/chunk","seq":66,"time":1783860681324,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" '"}}} {"type":"assistant/chunk","seq":67,"time":1783860681324,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"es"}}} {"type":"assistant/chunk","seq":68,"time":1783860681324,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"cal"}}} {"type":"assistant/chunk","seq":69,"time":1783860681324,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"ated"}}} {"type":"assistant/chunk","seq":70,"time":1783860681324,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\\\\n"}}} -{"type":"assistant/chunk","seq":71,"time":1783860681342,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"'"}}} -{"type":"assistant/chunk","seq":72,"time":1783860681343,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" >"}}} +{"type":"assistant/chunk","seq":71,"time":1783860681324,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"'"}}} +{"type":"assistant/chunk","seq":72,"time":1783860681342,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" >"}}} {"type":"assistant/chunk","seq":73,"time":1783860681343,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" /"}}} {"type":"assistant/chunk","seq":74,"time":1783860681343,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"tmp"}}} -{"type":"assistant/chunk","seq":75,"time":1783860681368,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"/d"}}} -{"type":"assistant/chunk","seq":76,"time":1783860681371,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"sh"}}} +{"type":"assistant/chunk","seq":75,"time":1783860681343,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"/d"}}} +{"type":"assistant/chunk","seq":76,"time":1783860681368,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"sh"}}} {"type":"assistant/chunk","seq":77,"time":1783860681371,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"-es"}}} {"type":"assistant/chunk","seq":78,"time":1783860681371,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"cal"}}} {"type":"assistant/chunk","seq":79,"time":1783860681371,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"ated"}}} -{"type":"assistant/chunk","seq":80,"time":1783860681400,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":80,"time":1783860681371,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":".txt"}}} {"type":"assistant/chunk","seq":81,"time":1783860681400,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" &&"}}} {"type":"assistant/chunk","seq":82,"time":1783860681400,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" cat"}}} {"type":"assistant/chunk","seq":83,"time":1783860681400,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" /"}}} -{"type":"assistant/chunk","seq":84,"time":1783860681401,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"tmp"}}} -{"type":"assistant/chunk","seq":85,"time":1783860681402,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"/d"}}} -{"type":"assistant/chunk","seq":86,"time":1783860681432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"sh"}}} +{"type":"assistant/chunk","seq":84,"time":1783860681400,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"tmp"}}} +{"type":"assistant/chunk","seq":85,"time":1783860681401,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"/d"}}} +{"type":"assistant/chunk","seq":86,"time":1783860681402,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"sh"}}} {"type":"assistant/chunk","seq":87,"time":1783860681432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"-es"}}} {"type":"assistant/chunk","seq":88,"time":1783860681432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"cal"}}} {"type":"assistant/chunk","seq":89,"time":1783860681432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"ated"}}} {"type":"assistant/chunk","seq":90,"time":1783860681432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":".txt"}}} {"type":"assistant/chunk","seq":91,"time":1783860681432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" &&"}}} -{"type":"assistant/chunk","seq":92,"time":1783860681456,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" rm"}}} +{"type":"assistant/chunk","seq":92,"time":1783860681432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" rm"}}} {"type":"assistant/chunk","seq":93,"time":1783860681456,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" /"}}} {"type":"assistant/chunk","seq":94,"time":1783860681456,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"tmp"}}} {"type":"assistant/chunk","seq":95,"time":1783860681456,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"/d"}}} {"type":"assistant/chunk","seq":96,"time":1783860681456,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"sh"}}} {"type":"assistant/chunk","seq":97,"time":1783860681456,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"-es"}}} -{"type":"assistant/chunk","seq":98,"time":1783860681485,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"cal"}}} -{"type":"assistant/chunk","seq":99,"time":1783860681519,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"ated"}}} -{"type":"assistant/chunk","seq":100,"time":1783860681521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":98,"time":1783860681456,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"cal"}}} +{"type":"assistant/chunk","seq":99,"time":1783860681485,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"ated"}}} +{"type":"assistant/chunk","seq":100,"time":1783860681519,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":".txt"}}} {"type":"assistant/chunk","seq":101,"time":1783860681521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":102,"time":1783860681521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":", "}}} {"type":"assistant/chunk","seq":103,"time":1783860681521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":104,"time":1783860681542,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"description"}}} -{"type":"assistant/chunk","seq":105,"time":1783860681545,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":104,"time":1783860681521,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":105,"time":1783860681542,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":106,"time":1783860681545,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":": "}}} -{"type":"assistant/chunk","seq":107,"time":1783860681573,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":107,"time":1783860681545,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":108,"time":1783860681573,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"Write"}}} -{"type":"assistant/chunk","seq":109,"time":1783860681574,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" to"}}} -{"type":"assistant/chunk","seq":110,"time":1783860681605,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" /"}}} -{"type":"assistant/chunk","seq":111,"time":1783860681608,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"tmp"}}} -{"type":"assistant/chunk","seq":112,"time":1783860681630,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" and"}}} -{"type":"assistant/chunk","seq":113,"time":1783860681632,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" verify"}}} -{"type":"assistant/chunk","seq":114,"time":1783860681661,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":","}}} +{"type":"assistant/chunk","seq":109,"time":1783860681573,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" to"}}} +{"type":"assistant/chunk","seq":110,"time":1783860681574,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" /"}}} +{"type":"assistant/chunk","seq":111,"time":1783860681605,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"tmp"}}} +{"type":"assistant/chunk","seq":112,"time":1783860681608,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" and"}}} +{"type":"assistant/chunk","seq":113,"time":1783860681630,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" verify"}}} +{"type":"assistant/chunk","seq":114,"time":1783860681632,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":","}}} {"type":"assistant/chunk","seq":115,"time":1783860681661,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" then"}}} -{"type":"assistant/chunk","seq":116,"time":1783860681693,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" clean"}}} +{"type":"assistant/chunk","seq":116,"time":1783860681661,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" clean"}}} {"type":"assistant/chunk","seq":117,"time":1783860681693,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" up"}}} {"type":"assistant/chunk","seq":118,"time":1783860681693,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":119,"time":1783860681693,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":", "}}} -{"type":"assistant/chunk","seq":120,"time":1783860681725,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":120,"time":1783860681693,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":121,"time":1783860681725,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"sand"}}} -{"type":"assistant/chunk","seq":122,"time":1783860681726,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"box"}}} +{"type":"assistant/chunk","seq":122,"time":1783860681725,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"box"}}} {"type":"assistant/chunk","seq":123,"time":1783860681726,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"_per"}}} -{"type":"assistant/chunk","seq":124,"time":1783860681751,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"missions"}}} -{"type":"assistant/chunk","seq":125,"time":1783860681753,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":126,"time":1783860681754,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":124,"time":1783860681726,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"missions"}}} +{"type":"assistant/chunk","seq":125,"time":1783860681751,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":126,"time":1783860681753,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":127,"time":1783860681754,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":128,"time":1783860681754,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"danger"}}} -{"type":"assistant/chunk","seq":129,"time":1783860681813,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"-full"}}} +{"type":"assistant/chunk","seq":129,"time":1783860681754,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"-full"}}} {"type":"assistant/chunk","seq":130,"time":1783860681813,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"-access"}}} {"type":"assistant/chunk","seq":131,"time":1783860681813,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":132,"time":1783860681813,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":", "}}} {"type":"assistant/chunk","seq":133,"time":1783860681813,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":134,"time":1783860681815,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"just"}}} -{"type":"assistant/chunk","seq":135,"time":1783860681840,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"ification"}}} -{"type":"assistant/chunk","seq":136,"time":1783860681842,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":134,"time":1783860681813,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"just"}}} +{"type":"assistant/chunk","seq":135,"time":1783860681815,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"ification"}}} +{"type":"assistant/chunk","seq":136,"time":1783860681840,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":137,"time":1783860681842,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":138,"time":1783860681842,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":139,"time":1783860681842,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"the"}}} -{"type":"assistant/chunk","seq":140,"time":1783860681870,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" user"}}} +{"type":"assistant/chunk","seq":140,"time":1783860681842,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" user"}}} {"type":"assistant/chunk","seq":141,"time":1783860681870,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" asked"}}} {"type":"assistant/chunk","seq":142,"time":1783860681870,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" to"}}} {"type":"assistant/chunk","seq":143,"time":1783860681870,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" write"}}} {"type":"assistant/chunk","seq":144,"time":1783860681870,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" a"}}} -{"type":"assistant/chunk","seq":145,"time":1783860681872,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" file"}}} -{"type":"assistant/chunk","seq":146,"time":1783860681901,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" outside"}}} -{"type":"assistant/chunk","seq":147,"time":1783860681903,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" the"}}} -{"type":"assistant/chunk","seq":148,"time":1783860681967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" workspace"}}} +{"type":"assistant/chunk","seq":145,"time":1783860681870,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" file"}}} +{"type":"assistant/chunk","seq":146,"time":1783860681872,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" outside"}}} +{"type":"assistant/chunk","seq":147,"time":1783860681901,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":148,"time":1783860681903,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":" workspace"}}} {"type":"assistant/chunk","seq":149,"time":1783860681967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":150,"time":1783860681967,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","argumentsDelta":"}"}}} {"type":"assistant/chunk","seq":151,"time":1783860681967,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run a specific command with `sandbox_permissions` set to `danger-full-access` and a specific justification. They explicitly said NOT to run it without sandbox_permissions first. Let me do exactly that."}}}} -{"type":"assistant/chunk","seq":152,"time":1783962246274,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write to /tmp and verify, then clean up\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}}}} +{"type":"assistant/chunk","seq":152,"time":1783860681967,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write to /tmp and verify, then clean up\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}}}} {"type":"assistant/chunk","seq":153,"time":1783962246274,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1509,"outputTokens":198,"cacheReadTokens":0,"reasoningTokens":48}}}} {"type":"assistant/chunk","seq":154,"time":1783962246274,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":155,"time":1783962246274,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a specific command with `sandbox_permissions` set to `danger-full-access` and a specific justification. They explicitly said NOT to run it without sandbox_permissions first. Let me do exactly that."},{"type":"tool-call","id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write to /tmp and verify, then clean up\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":1509,"outputTokens":198,"cacheReadTokens":0,"reasoningTokens":48}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154],"surfaceOp":"append"} -{"type":"tool/call","seq":156,"time":1783962246274,"data":{"turn":1,"step":1,"callId":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write to /tmp and verify, then clean up\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}} -{"type":"approval/asked","seq":157,"time":1783962246275,"data":{"id":"e5cc84ef-8704-4cd9-8e47-897a0a51beb6","toolName":"bash","callId":"call_00_WB1vnPomi8yr6MlcFKTj7912","reason":"escalate sandbox to danger-full-access: the user asked to write a file outside the workspace"}} -{"type":"approval/decided","seq":158,"time":1783962246275,"data":{"id":"e5cc84ef-8704-4cd9-8e47-897a0a51beb6","outcome":"rejected"}} -{"type":"tool/result","seq":159,"time":1783962246275,"data":{"turn":1,"step":1,"callId":"call_00_WB1vnPomi8yr6MlcFKTj7912","content":[{"type":"text","text":"Error: the user rejected escalating this command to \"danger-full-access\""}],"isError":true},"sourceEventSeqs":[156],"surfaceOp":"append"} -{"type":"step/end","seq":160,"time":1783962246276,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":161,"time":1783962246276,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":162,"time":1783860683140,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":163,"time":1783860683141,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":164,"time":1783860683141,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":165,"time":1783860683170,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" rejected"}}} -{"type":"assistant/chunk","seq":166,"time":1783860683170,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":167,"time":1783860683170,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" escalation"}}} -{"type":"assistant/chunk","seq":168,"time":1783860683205,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":169,"time":1783860683208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" As"}}} -{"type":"assistant/chunk","seq":170,"time":1783860683208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instructed"}}} -{"type":"assistant/chunk","seq":171,"time":1783860683208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":172,"time":1783860683208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} -{"type":"assistant/chunk","seq":173,"time":1783860683230,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" will"}}} -{"type":"assistant/chunk","seq":174,"time":1783860683232,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} -{"type":"assistant/chunk","seq":175,"time":1783860683233,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" ret"}}} -{"type":"assistant/chunk","seq":176,"time":1783860683260,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ry"}}} -{"type":"assistant/chunk","seq":177,"time":1783860683260,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":178,"time":1783860683260,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" will"}}} -{"type":"assistant/chunk","seq":179,"time":1783860683260,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} -{"type":"assistant/chunk","seq":180,"time":1783860683260,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" work"}}} -{"type":"assistant/chunk","seq":181,"time":1783860683294,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" around"}}} -{"type":"assistant/chunk","seq":182,"time":1783860683296,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} -{"type":"assistant/chunk","seq":183,"time":1783860683296,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" —"}}} -{"type":"assistant/chunk","seq":184,"time":1783860683317,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} -{"type":"assistant/chunk","seq":185,"time":1783860683317,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" explain"}}} -{"type":"assistant/chunk","seq":186,"time":1783860683317,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} -{"type":"assistant/chunk","seq":187,"time":1783860683318,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" one"}}} -{"type":"assistant/chunk","seq":188,"time":1783860683318,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" short"}}} -{"type":"assistant/chunk","seq":189,"time":1783860683318,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" sentence"}}} -{"type":"assistant/chunk","seq":190,"time":1783860683347,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":191,"time":1783860683347,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} -{"type":"assistant/chunk","seq":192,"time":1783860683349,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":193,"time":1783860683349,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":194,"time":1783860683375,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"The"}}} -{"type":"assistant/chunk","seq":195,"time":1783860683376,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" user"}}} -{"type":"assistant/chunk","seq":196,"time":1783860683409,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" rejected"}}} -{"type":"assistant/chunk","seq":197,"time":1783860683410,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" the"}}} -{"type":"assistant/chunk","seq":198,"time":1783860683410,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" permission"}}} -{"type":"assistant/chunk","seq":199,"time":1783860683435,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" escalation"}}} -{"type":"assistant/chunk","seq":200,"time":1783860683437,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} -{"type":"assistant/chunk","seq":201,"time":1783860683437,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" so"}}} -{"type":"assistant/chunk","seq":202,"time":1783860683462,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" this"}}} -{"type":"assistant/chunk","seq":203,"time":1783860683464,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" command"}}} -{"type":"assistant/chunk","seq":204,"time":1783860683464,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" cannot"}}} -{"type":"assistant/chunk","seq":205,"time":1783860683506,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" be"}}} -{"type":"assistant/chunk","seq":206,"time":1783860683506,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" run"}}} -{"type":"assistant/chunk","seq":207,"time":1783860683506,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"."}}} -{"type":"assistant/chunk","seq":208,"time":1783860683506,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user rejected the escalation. As instructed, I will not retry and will not work around it — just explain in one short sentence and stop."}}}} -{"type":"assistant/chunk","seq":209,"time":1783962246279,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The user rejected the permission escalation, so this command cannot be run."}}}} -{"type":"assistant/chunk","seq":210,"time":1783962246279,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":69,"outputTokens":45,"cacheReadTokens":1664,"reasoningTokens":30}}}} -{"type":"assistant/chunk","seq":211,"time":1783962246279,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":212,"time":1783962246279,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The user rejected the escalation. As instructed, I will not retry and will not work around it — just explain in one short sentence and stop."},{"type":"text","text":"The user rejected the permission escalation, so this command cannot be run."}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":69,"outputTokens":45,"cacheReadTokens":1664,"reasoningTokens":30}},"sourceEventSeqs":[162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211],"surfaceOp":"append"} -{"type":"step/end","seq":213,"time":1783962246279,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":214,"time":1783962246279,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"assistant/message","seq":155,"time":1784903375735,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a specific command with `sandbox_permissions` set to `danger-full-access` and a specific justification. They explicitly said NOT to run it without sandbox_permissions first. Let me do exactly that."},{"type":"tool-call","id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write to /tmp and verify, then clean up\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":1509,"outputTokens":198,"cacheReadTokens":0,"reasoningTokens":48}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154],"surfaceOp":"append"} +{"type":"tool/call","seq":156,"time":1784903375736,"data":{"turn":1,"step":1,"callId":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write to /tmp and verify, then clean up\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}} +{"type":"approval/asked","seq":157,"time":1784903375744,"data":{"id":"08bd507d-4e24-43f8-8944-efe11fa3fd01","toolName":"bash","callId":"call_00_WB1vnPomi8yr6MlcFKTj7912","reason":"escalate sandbox to danger-full-access: the user asked to write a file outside the workspace"}} +{"type":"approval/decided","seq":158,"time":1784903375745,"data":{"id":"08bd507d-4e24-43f8-8944-efe11fa3fd01","outcome":"rejected"}} +{"type":"tool/result","seq":159,"time":1784903375745,"data":{"turn":1,"step":1,"callId":"call_00_WB1vnPomi8yr6MlcFKTj7912","content":[{"type":"text","text":"Error: the user rejected escalating this command to \"danger-full-access\""}],"isError":true},"sourceEventSeqs":[156],"surfaceOp":"append"} +{"type":"step/end","seq":160,"time":1784903375746,"data":{"turn":1,"step":1}} +{"type":"user/message","seq":161,"time":1784903375751,"data":{"content":[{"type":"text","text":"The approval policy changed from \"never\" to \"ask\" (changed by the operator/config)."}],"source":{"kind":"plugin","plugin":"user-approval"}},"surfaceOp":"append"} +{"type":"step/start","seq":162,"time":1783962246276,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":163,"time":1783860683140,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":164,"time":1783860683141,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":165,"time":1783860683141,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":166,"time":1783860683170,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" rejected"}}} +{"type":"assistant/chunk","seq":167,"time":1783860683170,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":168,"time":1783860683170,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" escalation"}}} +{"type":"assistant/chunk","seq":169,"time":1783860683205,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":170,"time":1783860683208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" As"}}} +{"type":"assistant/chunk","seq":171,"time":1783860683208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instructed"}}} +{"type":"assistant/chunk","seq":172,"time":1783860683208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":173,"time":1783860683208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":174,"time":1783860683230,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" will"}}} +{"type":"assistant/chunk","seq":175,"time":1783860683232,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} +{"type":"assistant/chunk","seq":176,"time":1783860683233,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" ret"}}} +{"type":"assistant/chunk","seq":177,"time":1783860683260,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ry"}}} +{"type":"assistant/chunk","seq":178,"time":1783860683260,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":179,"time":1783860683260,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" will"}}} +{"type":"assistant/chunk","seq":180,"time":1783860683260,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} +{"type":"assistant/chunk","seq":181,"time":1783860683260,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" work"}}} +{"type":"assistant/chunk","seq":182,"time":1783860683294,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" around"}}} +{"type":"assistant/chunk","seq":183,"time":1783860683296,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":184,"time":1783860683296,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" —"}}} +{"type":"assistant/chunk","seq":185,"time":1783860683317,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} +{"type":"assistant/chunk","seq":186,"time":1783860683317,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" explain"}}} +{"type":"assistant/chunk","seq":187,"time":1783860683317,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} +{"type":"assistant/chunk","seq":188,"time":1783860683318,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" one"}}} +{"type":"assistant/chunk","seq":189,"time":1783860683318,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" short"}}} +{"type":"assistant/chunk","seq":190,"time":1783860683318,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" sentence"}}} +{"type":"assistant/chunk","seq":191,"time":1783860683347,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":192,"time":1783860683347,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} +{"type":"assistant/chunk","seq":193,"time":1783860683349,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":194,"time":1783860683349,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":195,"time":1783860683375,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"The"}}} +{"type":"assistant/chunk","seq":196,"time":1783860683376,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" user"}}} +{"type":"assistant/chunk","seq":197,"time":1783860683409,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" rejected"}}} +{"type":"assistant/chunk","seq":198,"time":1783860683410,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":199,"time":1783860683410,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" permission"}}} +{"type":"assistant/chunk","seq":200,"time":1783860683435,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" escalation"}}} +{"type":"assistant/chunk","seq":201,"time":1783860683437,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":202,"time":1783860683437,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" so"}}} +{"type":"assistant/chunk","seq":203,"time":1783860683462,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" this"}}} +{"type":"assistant/chunk","seq":204,"time":1783860683464,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" command"}}} +{"type":"assistant/chunk","seq":205,"time":1783860683464,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" cannot"}}} +{"type":"assistant/chunk","seq":206,"time":1783860683506,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" be"}}} +{"type":"assistant/chunk","seq":207,"time":1783860683506,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":" run"}}} +{"type":"assistant/chunk","seq":208,"time":1783860683506,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"."}}} +{"type":"assistant/chunk","seq":209,"time":1783860683506,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user rejected the escalation. As instructed, I will not retry and will not work around it — just explain in one short sentence and stop."}}}} +{"type":"assistant/chunk","seq":210,"time":1783962246279,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The user rejected the permission escalation, so this command cannot be run."}}}} +{"type":"assistant/chunk","seq":211,"time":1783962246279,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":69,"outputTokens":45,"cacheReadTokens":1664,"reasoningTokens":30}}}} +{"type":"assistant/chunk","seq":212,"time":1783962246279,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":213,"time":1783962246279,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The user rejected the escalation. As instructed, I will not retry and will not work around it — just explain in one short sentence and stop."},{"type":"text","text":"The user rejected the permission escalation, so this command cannot be run."}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":69,"outputTokens":45,"cacheReadTokens":1664,"reasoningTokens":30}},"sourceEventSeqs":[163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212],"surfaceOp":"append"} +{"type":"step/end","seq":214,"time":1783962246279,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":215,"time":1783962246279,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl b/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl index d0bd2d5bef..bcdd4c7b99 100644 --- a/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl @@ -130,7 +130,7 @@ {"type":"assistant/chunk","seq":128,"time":1783352087469,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":129,"time":1783352087469,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"Now I need to replace \"DEBUG\" with \"RELEASE\" using the edit tool."},{"type":"tool-call","id":"call_00_vOytneZ0XpsLslEEJAxR6398","name":"edit","arguments":"{\"file_path\": \"config.txt\", \"old_string\": \"DEBUG\", \"new_string\": \"RELEASE\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":241,"outputTokens":98,"cacheReadTokens":2816,"reasoningTokens":18}},"sourceEventSeqs":[74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128],"surfaceOp":"append"} {"type":"tool/call","seq":130,"time":1783352087469,"data":{"turn":1,"step":2,"callId":"call_00_vOytneZ0XpsLslEEJAxR6398","name":"edit","arguments":"{\"file_path\": \"config.txt\", \"old_string\": \"DEBUG\", \"new_string\": \"RELEASE\"}"}} -{"type":"tool/result","seq":131,"time":1783352087476,"data":{"turn":1,"step":2,"callId":"call_00_vOytneZ0XpsLslEEJAxR6398","content":[{"type":"text","text":"The file /tmp/acp-snap-cwd-0BxHdV/config.txt has been updated successfully."}],"isError":false,"meta":{"diffs":[{"path":"config.txt","oldText":"mode=DEBUG\nlevel=info","newText":"mode=RELEASE\nlevel=info"}]}},"sourceEventSeqs":[130],"surfaceOp":"append"} +{"type":"tool/result","seq":131,"time":1783352087476,"data":{"turn":1,"step":2,"callId":"call_00_vOytneZ0XpsLslEEJAxR6398","content":[{"type":"text","text":"The file /private/tmp/acp-snap-cwd-0BxHdV/config.txt has been updated successfully."}],"isError":false,"meta":{"diffs":[{"path":"config.txt","oldText":"mode=DEBUG\nlevel=info","newText":"mode=RELEASE\nlevel=info"}]}},"sourceEventSeqs":[130],"surfaceOp":"append"} {"type":"step/end","seq":132,"time":1783352087477,"data":{"turn":1,"step":2}} {"type":"step/start","seq":133,"time":1783352087477,"data":{"turn":1,"step":3}} {"type":"assistant/chunk","seq":134,"time":1783352088286,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} diff --git a/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl b/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl index 918b3ab4d5..c176ceed3a 100644 --- a/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl @@ -1,128 +1,129 @@ {"type":"session","version":0,"id":"977a4820-f609-4b48-9039-adcdd921c5fe","createdAt":1784045702340,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-vmEGzd","delegationDepth":0} {"type":"turn/start","seq":0,"time":1784045702342,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"permission/preset","seq":1,"time":1784045702343,"data":{"preset":"workspace-write"}} -{"type":"sandbox/mode","seq":2,"time":1784045702343,"data":{"mode":"workspace-write"}} -{"type":"approval/policy","seq":3,"time":1784045702343,"data":{"policy":"ask"}} -{"type":"user/message","seq":4,"time":1784045702343,"data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create escalated.md in the current directory containing exactly the single line: escalated. An equivalent write was denied earlier, so make this one single write call with sandbox_permissions set to danger-full-access and the justification 'the user asked to escalate this write'. Do not call write without sandbox_permissions first. I will approve the permission prompt. After the result, reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"session/title","seq":5,"time":1784045702343,"data":{"title":"Use the write tool (NOT","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":6,"time":1784045702345,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":7,"time":1784045702345,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"assistant/chunk","seq":8,"time":1784045703046,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"user/message","seq":1,"time":1784903376886,"data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create escalated.md in the current directory containing exactly the single line: escalated. An equivalent write was denied earlier, so make this one single write call with sandbox_permissions set to danger-full-access and the justification 'the user asked to escalate this write'. Do not call write without sandbox_permissions first. I will approve the permission prompt. After the result, reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"session/title","seq":2,"time":1784903376886,"data":{"title":"Use the write tool (NOT","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"permission/preset","seq":3,"time":1784903376905,"data":{"preset":"workspace-write"}} +{"type":"sandbox/mode","seq":4,"time":1784903376905,"data":{"mode":"workspace-write"}} +{"type":"approval/policy","seq":5,"time":1784903376905,"data":{"policy":"ask"}} +{"type":"step/start","seq":6,"time":1784903376906,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":7,"time":1784903376906,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":8,"time":1784903376916,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} {"type":"assistant/chunk","seq":9,"time":1784045703046,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":10,"time":1784045703162,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":11,"time":1784045703172,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":10,"time":1784045703046,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":11,"time":1784045703162,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} {"type":"assistant/chunk","seq":12,"time":1784045703172,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":13,"time":1784045703173,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":13,"time":1784045703172,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} {"type":"assistant/chunk","seq":14,"time":1784045703173,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" create"}}} {"type":"assistant/chunk","seq":15,"time":1784045703173,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} {"type":"assistant/chunk","seq":16,"time":1784045703173,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} -{"type":"assistant/chunk","seq":17,"time":1784045703199,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" using"}}} -{"type":"assistant/chunk","seq":18,"time":1784045703225,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":19,"time":1784045703251,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" write"}}} -{"type":"assistant/chunk","seq":20,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tool"}}} +{"type":"assistant/chunk","seq":17,"time":1784045703173,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" using"}}} +{"type":"assistant/chunk","seq":18,"time":1784045703199,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":19,"time":1784045703225,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" write"}}} +{"type":"assistant/chunk","seq":20,"time":1784045703251,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tool"}}} {"type":"assistant/chunk","seq":21,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} {"type":"assistant/chunk","seq":22,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sand"}}} {"type":"assistant/chunk","seq":23,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"box"}}} {"type":"assistant/chunk","seq":24,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_per"}}} -{"type":"assistant/chunk","seq":25,"time":1784045703277,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"missions"}}} -{"type":"assistant/chunk","seq":26,"time":1784045703278,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":25,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"missions"}}} +{"type":"assistant/chunk","seq":26,"time":1784045703277,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} {"type":"assistant/chunk","seq":27,"time":1784045703278,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} {"type":"assistant/chunk","seq":28,"time":1784045703278,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} {"type":"assistant/chunk","seq":29,"time":1784045703278,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" do"}}} -{"type":"assistant/chunk","seq":30,"time":1784045703304,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":30,"time":1784045703278,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} {"type":"assistant/chunk","seq":31,"time":1784045703304,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":32,"time":1784045703356,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":32,"time":1784045703304,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":33,"time":1784045703356,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":""}}} -{"type":"assistant/chunk","seq":34,"time":1784045703381,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":34,"time":1784045703356,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"{"}}} {"type":"assistant/chunk","seq":35,"time":1784045703381,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":36,"time":1784045703381,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"file"}}} -{"type":"assistant/chunk","seq":37,"time":1784045703405,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"_path"}}} -{"type":"assistant/chunk","seq":38,"time":1784045703406,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":37,"time":1784045703381,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"_path"}}} +{"type":"assistant/chunk","seq":38,"time":1784045703405,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":39,"time":1784045703406,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":40,"time":1784045703406,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":41,"time":1784045703431,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"es"}}} -{"type":"assistant/chunk","seq":42,"time":1784045703432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"cal"}}} +{"type":"assistant/chunk","seq":41,"time":1784045703406,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"es"}}} +{"type":"assistant/chunk","seq":42,"time":1784045703431,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"cal"}}} {"type":"assistant/chunk","seq":43,"time":1784045703432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"ated"}}} {"type":"assistant/chunk","seq":44,"time":1784045703432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":".md"}}} {"type":"assistant/chunk","seq":45,"time":1784045703432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":46,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":46,"time":1784045703432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":", "}}} {"type":"assistant/chunk","seq":47,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":48,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"content"}}} {"type":"assistant/chunk","seq":49,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":50,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":": "}}} -{"type":"assistant/chunk","seq":51,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":51,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":52,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"es"}}} {"type":"assistant/chunk","seq":53,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"cal"}}} {"type":"assistant/chunk","seq":54,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"ated"}}} {"type":"assistant/chunk","seq":55,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":56,"time":1784045703565,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":", "}}} -{"type":"assistant/chunk","seq":57,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":56,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":57,"time":1784045703565,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":58,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"sand"}}} {"type":"assistant/chunk","seq":59,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"box"}}} {"type":"assistant/chunk","seq":60,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"_per"}}} {"type":"assistant/chunk","seq":61,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"missions"}}} {"type":"assistant/chunk","seq":62,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":63,"time":1784045703591,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":63,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":64,"time":1784045703591,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":65,"time":1784045703591,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"danger"}}} {"type":"assistant/chunk","seq":66,"time":1784045703591,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"-full"}}} -{"type":"assistant/chunk","seq":67,"time":1784045703617,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"-access"}}} -{"type":"assistant/chunk","seq":68,"time":1784045703618,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":69,"time":1784045703644,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":", "}}} -{"type":"assistant/chunk","seq":70,"time":1784045703645,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":67,"time":1784045703591,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"-access"}}} +{"type":"assistant/chunk","seq":68,"time":1784045703617,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":69,"time":1784045703618,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":70,"time":1784045703644,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":71,"time":1784045703645,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"just"}}} {"type":"assistant/chunk","seq":72,"time":1784045703645,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"ification"}}} -{"type":"assistant/chunk","seq":73,"time":1784045703669,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":73,"time":1784045703645,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":74,"time":1784045703669,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":75,"time":1784045703669,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":76,"time":1784045703669,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"the"}}} -{"type":"assistant/chunk","seq":77,"time":1784045703696,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" user"}}} +{"type":"assistant/chunk","seq":77,"time":1784045703669,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" user"}}} {"type":"assistant/chunk","seq":78,"time":1784045703696,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" asked"}}} {"type":"assistant/chunk","seq":79,"time":1784045703696,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" to"}}} {"type":"assistant/chunk","seq":80,"time":1784045703696,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" escalate"}}} -{"type":"assistant/chunk","seq":81,"time":1784045703724,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" this"}}} +{"type":"assistant/chunk","seq":81,"time":1784045703696,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" this"}}} {"type":"assistant/chunk","seq":82,"time":1784045703724,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" write"}}} {"type":"assistant/chunk","seq":83,"time":1784045703724,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":84,"time":1784045703749,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"}"}}} -{"type":"assistant/chunk","seq":85,"time":1784045703776,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to create a file using the write tool with sandbox_permissions. Let me do that."}}}} +{"type":"assistant/chunk","seq":84,"time":1784045703724,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":85,"time":1784045703749,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to create a file using the write tool with sandbox_permissions. Let me do that."}}}} {"type":"assistant/chunk","seq":86,"time":1784045703776,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","arguments":"{\"file_path\": \"escalated.md\", \"content\": \"escalated\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to escalate this write\"}"}}}} {"type":"assistant/chunk","seq":87,"time":1784045703776,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3871,"outputTokens":132,"cacheReadTokens":0,"reasoningTokens":23}}}} {"type":"assistant/chunk","seq":88,"time":1784045703776,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":89,"time":1784045703780,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to create a file using the write tool with sandbox_permissions. Let me do that."},{"type":"tool-call","id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","arguments":"{\"file_path\": \"escalated.md\", \"content\": \"escalated\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to escalate this write\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3871,"outputTokens":132,"cacheReadTokens":0,"reasoningTokens":23}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88],"surfaceOp":"append"} -{"type":"tool/call","seq":90,"time":1784045703780,"data":{"turn":1,"step":1,"callId":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","arguments":"{\"file_path\": \"escalated.md\", \"content\": \"escalated\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to escalate this write\"}"}} -{"type":"approval/asked","seq":91,"time":1784045703782,"data":{"id":"d7250dff-fc08-4163-9b51-f43f08d6f36f","toolName":"write","callId":"call_00_Fnymmavpr4klMDy4Fdej3227","reason":"escalate sandbox to danger-full-access: the user asked to escalate this write"}} -{"type":"approval/decided","seq":92,"time":1784045703786,"data":{"id":"d7250dff-fc08-4163-9b51-f43f08d6f36f","outcome":"allowed-once"}} -{"type":"tool/result","seq":93,"time":1784045703798,"data":{"turn":1,"step":1,"callId":"call_00_Fnymmavpr4klMDy4Fdej3227","content":[{"type":"text","text":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-vmEGzd/escalated.md\nfile\n\nCreated file\n"}],"isError":false,"meta":{"diffs":[]}},"sourceEventSeqs":[90],"surfaceOp":"append"} -{"type":"step/end","seq":94,"time":1784045703798,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":95,"time":1784045703799,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":96,"time":1784045704512,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":97,"time":1784045704512,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":98,"time":1784045704620,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} -{"type":"assistant/chunk","seq":99,"time":1784045704645,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" was"}}} -{"type":"assistant/chunk","seq":100,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" created"}}} -{"type":"assistant/chunk","seq":101,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" successfully"}}} -{"type":"assistant/chunk","seq":102,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":103,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} -{"type":"assistant/chunk","seq":104,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":105,"time":1784045704672,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} -{"type":"assistant/chunk","seq":106,"time":1784045704673,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":107,"time":1784045704673,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":108,"time":1784045704673,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":109,"time":1784045704699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":110,"time":1784045704699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} -{"type":"assistant/chunk","seq":111,"time":1784045704699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":112,"time":1784045704726,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} -{"type":"assistant/chunk","seq":113,"time":1784045704726,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} -{"type":"assistant/chunk","seq":114,"time":1784045704726,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" D"}}} -{"type":"assistant/chunk","seq":115,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} -{"type":"assistant/chunk","seq":116,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":117,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":118,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} -{"type":"assistant/chunk","seq":119,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} -{"type":"assistant/chunk","seq":120,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The file was created successfully. The user asked me to reply with exactly the single word DONE."}}}} -{"type":"assistant/chunk","seq":121,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} -{"type":"assistant/chunk","seq":122,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":107,"outputTokens":23,"cacheReadTokens":3968,"reasoningTokens":20}}}} -{"type":"assistant/chunk","seq":123,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":124,"time":1784045704755,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The file was created successfully. The user asked me to reply with exactly the single word DONE."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":107,"outputTokens":23,"cacheReadTokens":3968,"reasoningTokens":20}},"sourceEventSeqs":[96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123],"surfaceOp":"append"} -{"type":"step/end","seq":125,"time":1784045704755,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":126,"time":1784045704756,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"assistant/message","seq":89,"time":1784903376921,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to create a file using the write tool with sandbox_permissions. Let me do that."},{"type":"tool-call","id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","arguments":"{\"file_path\": \"escalated.md\", \"content\": \"escalated\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to escalate this write\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3871,"outputTokens":132,"cacheReadTokens":0,"reasoningTokens":23}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88],"surfaceOp":"append"} +{"type":"tool/call","seq":90,"time":1784903376921,"data":{"turn":1,"step":1,"callId":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","arguments":"{\"file_path\": \"escalated.md\", \"content\": \"escalated\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to escalate this write\"}"}} +{"type":"approval/asked","seq":91,"time":1784903376928,"data":{"id":"0f35b4c3-4e40-4152-ab64-df6465411a00","toolName":"write","callId":"call_00_Fnymmavpr4klMDy4Fdej3227","reason":"escalate sandbox to danger-full-access: the user asked to escalate this write"}} +{"type":"approval/decided","seq":92,"time":1784903376929,"data":{"id":"0f35b4c3-4e40-4152-ab64-df6465411a00","outcome":"allowed-once"}} +{"type":"tool/result","seq":93,"time":1784903376941,"data":{"turn":1,"step":1,"callId":"call_00_Fnymmavpr4klMDy4Fdej3227","content":[{"type":"text","text":"/private/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-vmEGzd/escalated.md\nfile\n\nCreated file\n"}],"isError":false,"meta":{"diffs":[]}},"sourceEventSeqs":[90],"surfaceOp":"append"} +{"type":"step/end","seq":94,"time":1784903376941,"data":{"turn":1,"step":1}} +{"type":"user/message","seq":95,"time":1784903376949,"data":{"content":[{"type":"text","text":"The approval policy changed from \"never\" to \"ask\" (changed by the operator/config)."}],"source":{"kind":"plugin","plugin":"user-approval"}},"surfaceOp":"append"} +{"type":"step/start","seq":96,"time":1784045703799,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":97,"time":1784045704512,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":98,"time":1784045704512,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":99,"time":1784045704620,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":100,"time":1784045704645,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" was"}}} +{"type":"assistant/chunk","seq":101,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" created"}}} +{"type":"assistant/chunk","seq":102,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" successfully"}}} +{"type":"assistant/chunk","seq":103,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":104,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} +{"type":"assistant/chunk","seq":105,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":106,"time":1784045704672,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} +{"type":"assistant/chunk","seq":107,"time":1784045704673,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":108,"time":1784045704673,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":109,"time":1784045704673,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":110,"time":1784045704699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":111,"time":1784045704699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":112,"time":1784045704699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":113,"time":1784045704726,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":114,"time":1784045704726,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":115,"time":1784045704726,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" D"}}} +{"type":"assistant/chunk","seq":116,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":117,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":118,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":119,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} +{"type":"assistant/chunk","seq":120,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":121,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The file was created successfully. The user asked me to reply with exactly the single word DONE."}}}} +{"type":"assistant/chunk","seq":122,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":123,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":107,"outputTokens":23,"cacheReadTokens":3968,"reasoningTokens":20}}}} +{"type":"assistant/chunk","seq":124,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":125,"time":1784045704755,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The file was created successfully. The user asked me to reply with exactly the single word DONE."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":107,"outputTokens":23,"cacheReadTokens":3968,"reasoningTokens":20}},"sourceEventSeqs":[97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124],"surfaceOp":"append"} +{"type":"step/end","seq":126,"time":1784045704755,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":127,"time":1784045704756,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl b/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl index 8f97feff04..25c57ee964 100644 --- a/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl @@ -78,7 +78,7 @@ {"type":"assistant/chunk","seq":76,"time":1783611703969,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":77,"time":1783611703972,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to use the edit tool to replace \"blue\" with \"green\" in settings.txt without reading the file first, and then reply with just \"DONE\"."},{"type":"tool-call","id":"call_00_x0zlnXl5JOxLrAYL9y7P0119","name":"edit","arguments":"{\"file_path\": \"settings.txt\", \"old_string\": \"blue\", \"new_string\": \"green\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3132,"outputTokens":115,"cacheReadTokens":0,"reasoningTokens":36}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76],"surfaceOp":"append"} {"type":"tool/call","seq":78,"time":1783611703972,"data":{"turn":1,"step":1,"callId":"call_00_x0zlnXl5JOxLrAYL9y7P0119","name":"edit","arguments":"{\"file_path\": \"settings.txt\", \"old_string\": \"blue\", \"new_string\": \"green\"}"}} -{"type":"tool/result","seq":79,"time":1783611703978,"data":{"turn":1,"step":1,"callId":"call_00_x0zlnXl5JOxLrAYL9y7P0119","content":[{"type":"text","text":"Error: edit requires reading \"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-QzoqnB/settings.txt\" first"}],"isError":true,"error":{"name":"FsError","code":"FS_NOT_OBSERVED"}},"sourceEventSeqs":[78],"surfaceOp":"append"} +{"type":"tool/result","seq":79,"time":1783611703978,"data":{"turn":1,"step":1,"callId":"call_00_x0zlnXl5JOxLrAYL9y7P0119","content":[{"type":"text","text":"Error: edit requires reading \"/private/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-QzoqnB/settings.txt\" first"}],"isError":true,"error":{"name":"FsError","code":"FS_NOT_OBSERVED"}},"sourceEventSeqs":[78],"surfaceOp":"append"} {"type":"step/end","seq":80,"time":1783611703978,"data":{"turn":1,"step":1}} {"type":"step/start","seq":81,"time":1783611703978,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":82,"time":1783611704825,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} @@ -226,7 +226,7 @@ {"type":"assistant/chunk","seq":224,"time":1783611707096,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":225,"time":1783611707097,"data":{"turn":1,"step":3,"content":[{"type":"reasoning","text":"The file contains \"color: blue\". I need to replace \"blue\" with \"green\". The edit tool said it requires reading first — now I've read it, so the edit should work."},{"type":"tool-call","id":"call_00_GVknJu2tksKkP4lALCwh0926","name":"edit","arguments":"{\"file_path\": \"settings.txt\", \"old_string\": \"blue\", \"new_string\": \"green\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":281,"outputTokens":119,"cacheReadTokens":3200,"reasoningTokens":40}},"sourceEventSeqs":[149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224],"surfaceOp":"append"} {"type":"tool/call","seq":226,"time":1783611707097,"data":{"turn":1,"step":3,"callId":"call_00_GVknJu2tksKkP4lALCwh0926","name":"edit","arguments":"{\"file_path\": \"settings.txt\", \"old_string\": \"blue\", \"new_string\": \"green\"}"}} -{"type":"tool/result","seq":227,"time":1783611707114,"data":{"turn":1,"step":3,"callId":"call_00_GVknJu2tksKkP4lALCwh0926","content":[{"type":"text","text":"The file /var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-QzoqnB/settings.txt has been updated successfully."}],"isError":false,"meta":{"diffs":[{"path":"settings.txt","oldText":"color: blue","newText":"color: green"}]}},"sourceEventSeqs":[226],"surfaceOp":"append"} +{"type":"tool/result","seq":227,"time":1783611707114,"data":{"turn":1,"step":3,"callId":"call_00_GVknJu2tksKkP4lALCwh0926","content":[{"type":"text","text":"The file /private/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-QzoqnB/settings.txt has been updated successfully."}],"isError":false,"meta":{"diffs":[{"path":"settings.txt","oldText":"color: blue","newText":"color: green"}]}},"sourceEventSeqs":[226],"surfaceOp":"append"} {"type":"step/end","seq":228,"time":1783611707114,"data":{"turn":1,"step":3}} {"type":"step/start","seq":229,"time":1783611707114,"data":{"turn":1,"step":4}} {"type":"assistant/chunk","seq":230,"time":1783611707747,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} diff --git a/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl b/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl index 5a3fc5696b..f3db3493b5 100644 --- a/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl @@ -115,7 +115,7 @@ {"type":"assistant/chunk","seq":113,"time":1783352094988,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":114,"time":1783352094988,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The file contains \"original contents\". Now I'll replace it with \"replaced\"."},{"type":"tool-call","id":"call_00_N23EvXjDo4c8enyWpIUq4043","name":"write","arguments":"{\"file_path\": \"data.txt\", \"content\": \"replaced\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":228,"outputTokens":79,"cacheReadTokens":2816,"reasoningTokens":17}},"sourceEventSeqs":[70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113],"surfaceOp":"append"} {"type":"tool/call","seq":115,"time":1783352094988,"data":{"turn":1,"step":2,"callId":"call_00_N23EvXjDo4c8enyWpIUq4043","name":"write","arguments":"{\"file_path\": \"data.txt\", \"content\": \"replaced\"}"}} -{"type":"tool/result","seq":116,"time":1783352094995,"data":{"turn":1,"step":2,"callId":"call_00_N23EvXjDo4c8enyWpIUq4043","content":[{"type":"text","text":"/tmp/acp-snap-cwd-hH2sGY/data.txt\nfile\n\nUpdated file\n"}],"isError":false,"meta":{"diffs":[{"path":"data.txt","oldText":"original contents","newText":"replaced"}]}},"sourceEventSeqs":[115],"surfaceOp":"append"} +{"type":"tool/result","seq":116,"time":1783352094995,"data":{"turn":1,"step":2,"callId":"call_00_N23EvXjDo4c8enyWpIUq4043","content":[{"type":"text","text":"/private/tmp/acp-snap-cwd-hH2sGY/data.txt\nfile\n\nUpdated file\n"}],"isError":false,"meta":{"diffs":[{"path":"data.txt","oldText":"original contents","newText":"replaced"}]}},"sourceEventSeqs":[115],"surfaceOp":"append"} {"type":"step/end","seq":117,"time":1783352094995,"data":{"turn":1,"step":2}} {"type":"step/start","seq":118,"time":1783352094995,"data":{"turn":1,"step":3}} {"type":"assistant/chunk","seq":119,"time":1783352096090,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} diff --git a/examples/acp-agent/tests/snapshots/fs-write/session.jsonl b/examples/acp-agent/tests/snapshots/fs-write/session.jsonl index dbaaf8a8b6..46de5fa221 100644 --- a/examples/acp-agent/tests/snapshots/fs-write/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-write/session.jsonl @@ -63,7 +63,7 @@ {"type":"assistant/chunk","seq":61,"time":1783352079886,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":62,"time":1783352079888,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to create a file named notes.txt with the content \"hello world\" using the write tool, then reply with \"DONE\"."},{"type":"tool-call","id":"call_00_APMUCJJm9lrTSlVbg6dB0185","name":"write","arguments":"{\"file_path\": \"notes.txt\", \"content\": \"hello world\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":2891,"outputTokens":92,"cacheReadTokens":0,"reasoningTokens":30}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61],"surfaceOp":"append"} {"type":"tool/call","seq":63,"time":1783352079888,"data":{"turn":1,"step":1,"callId":"call_00_APMUCJJm9lrTSlVbg6dB0185","name":"write","arguments":"{\"file_path\": \"notes.txt\", \"content\": \"hello world\"}"}} -{"type":"tool/result","seq":64,"time":1783352079897,"data":{"turn":1,"step":1,"callId":"call_00_APMUCJJm9lrTSlVbg6dB0185","content":[{"type":"text","text":"/tmp/acp-snap-cwd-sNvn5N/notes.txt\nfile\n\nCreated file\n"}],"isError":false,"meta":{"diffs":[]}},"sourceEventSeqs":[63],"surfaceOp":"append"} +{"type":"tool/result","seq":64,"time":1783352079897,"data":{"turn":1,"step":1,"callId":"call_00_APMUCJJm9lrTSlVbg6dB0185","content":[{"type":"text","text":"/private/tmp/acp-snap-cwd-sNvn5N/notes.txt\nfile\n\nCreated file\n"}],"isError":false,"meta":{"diffs":[]}},"sourceEventSeqs":[63],"surfaceOp":"append"} {"type":"step/end","seq":65,"time":1783352079898,"data":{"turn":1,"step":1}} {"type":"step/start","seq":66,"time":1783352079899,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":67,"time":1783352080825,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl index 888f2f5c13..b5387abd53 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl @@ -56,8 +56,8 @@ {"type":"tool/call","seq":54,"time":1783352172557,"data":{"turn":1,"step":1,"callId":"call_00_6k0oGSliVHxGSgqBmMEO4311","name":"bash","arguments":"{\"command\": \"echo HELLO\", \"description\": \"Echo HELLO\"}"}} {"type":"hook/invoked","seq":55,"time":1783352172558,"data":{"turn":1,"point":"PreToolUse","dialect":"claude","handlerId":"claude:PreToolUse:1","matcher":"bash"}} {"type":"hook/result","seq":56,"time":1783352172573,"data":{"turn":1,"point":"PreToolUse","handlerId":"claude:PreToolUse:1","decision":"ask","exitCode":0,"durationMs":14.113374999999905}} -{"type":"approval/asked","seq":57,"time":1783962235813,"data":{"id":"97616288-1a5e-4110-a75d-7616a24adcc4","toolName":"bash","callId":"call_00_6k0oGSliVHxGSgqBmMEO4311","reason":"bash requires manual approval in this session"}} -{"type":"approval/decided","seq":58,"time":1783962235813,"data":{"id":"97616288-1a5e-4110-a75d-7616a24adcc4","outcome":"rejected"}} +{"type":"approval/asked","seq":57,"time":1783962235813,"data":{"id":"484fd014-76cc-49fa-ad83-71cd60d8b91f","toolName":"bash","callId":"call_00_6k0oGSliVHxGSgqBmMEO4311","reason":"bash requires manual approval in this session"}} +{"type":"approval/decided","seq":58,"time":1783962235813,"data":{"id":"484fd014-76cc-49fa-ad83-71cd60d8b91f","outcome":"rejected"}} {"type":"tool/result","seq":59,"time":1783962235814,"data":{"turn":1,"step":1,"callId":"call_00_6k0oGSliVHxGSgqBmMEO4311","content":[{"type":"text","text":"Error: the user rejected tool \"bash\""}],"isError":true},"sourceEventSeqs":[54],"surfaceOp":"append"} {"type":"step/end","seq":60,"time":1783962235814,"data":{"turn":1,"step":1}} {"type":"step/start","seq":61,"time":1783962235814,"data":{"turn":1,"step":2}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/session.jsonl index dd6745b770..3d6badf9da 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/session.jsonl @@ -1,6 +1 @@ {"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} -{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"hook/invoked","seq":1,"time":0,"data":{"turn":1,"point":"UserPromptSubmit","dialect":"claude","handlerId":"claude:UserPromptSubmit:1"}} -{"type":"hook/result","seq":2,"time":0,"data":{"turn":1,"point":"UserPromptSubmit","handlerId":"claude:UserPromptSubmit:1","decision":"block","exitCode":2,"stderrSummary":"blocked by policy hook","durationMs":0}} -{"type":"prompt/blocked","seq":3,"time":0,"data":{"content":[{"type":"text","text":"Delete everything in the repo."}],"source":{"kind":"user"},"reason":"blocked by policy hook"}} -{"type":"turn/end","seq":4,"time":0,"data":{"turn":1,"reason":{"kind":"rejected","reason":"blocked by policy hook"}}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl index 921bc270d9..275876edc2 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl @@ -1,39 +1,37 @@ {"type":"session","version":0,"id":"d03c3a83-1238-4e2e-ad9a-b86a61840a40","createdAt":1783352160541,"cwd":"/tmp/acp-snap-cwd-QUDqlk","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352160545,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"hook/invoked","seq":1,"time":1783352160546,"data":{"turn":1,"point":"UserPromptSubmit","dialect":"claude","handlerId":"claude:UserPromptSubmit:1"}} -{"type":"hook/result","seq":2,"time":1783352160564,"data":{"turn":1,"point":"UserPromptSubmit","handlerId":"claude:UserPromptSubmit:1","decision":"pass","exitCode":0,"durationMs":17.45639600000004}} -{"type":"user/message","seq":3,"time":1783352160564,"data":{"content":[{"type":"text","text":"What is my favorite color? Reply with just the color and stop. Do not use any tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"user/message","seq":4,"time":1783352160564,"data":{"content":[{"type":"text","text":"The user has previously stated their favorite color is teal."}],"source":{"kind":"plugin","plugin":"hooks-claude"}},"surfaceOp":"append"} -{"type":"session/title","seq":5,"time":1783352160564,"data":{"title":"What is my favorite color?","messageSeqs":[3],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":6,"time":1783352160565,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":7,"time":1783352160566,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"assistant/chunk","seq":8,"time":1783352161228,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":9,"time":1783352161229,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":10,"time":1783352161335,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":11,"time":1783352161363,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} -{"type":"assistant/chunk","seq":12,"time":1783352161363,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" favorite"}}} -{"type":"assistant/chunk","seq":13,"time":1783352161392,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" color"}}} -{"type":"assistant/chunk","seq":14,"time":1783352161392,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} -{"type":"assistant/chunk","seq":15,"time":1783352161392,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" te"}}} -{"type":"assistant/chunk","seq":16,"time":1783352161393,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"al"}}} -{"type":"assistant/chunk","seq":17,"time":1783352161393,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":18,"time":1783352161420,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" as"}}} -{"type":"assistant/chunk","seq":19,"time":1783352161421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stated"}}} -{"type":"assistant/chunk","seq":20,"time":1783352161421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} -{"type":"assistant/chunk","seq":21,"time":1783352161421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":22,"time":1783352161449,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" context"}}} -{"type":"assistant/chunk","seq":23,"time":1783352161449,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" provided"}}} -{"type":"assistant/chunk","seq":24,"time":1783352161449,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" by"}}} -{"type":"assistant/chunk","seq":25,"time":1783352161477,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":26,"time":1783352161478,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" plugin"}}} -{"type":"assistant/chunk","seq":27,"time":1783352161478,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":28,"time":1783352161478,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":29,"time":1783352161478,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"te"}}} -{"type":"assistant/chunk","seq":30,"time":1783352161511,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"al"}}} -{"type":"assistant/chunk","seq":31,"time":1783352161512,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user's favorite color is teal, as stated in the context provided by the plugin."}}}} -{"type":"assistant/chunk","seq":32,"time":1783352161512,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"teal"}}}} -{"type":"assistant/chunk","seq":33,"time":1783352161512,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2892,"outputTokens":22,"cacheReadTokens":0,"reasoningTokens":19}}}} -{"type":"assistant/chunk","seq":34,"time":1783352161512,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":35,"time":1783352161515,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user's favorite color is teal, as stated in the context provided by the plugin."},{"type":"text","text":"teal"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":2892,"outputTokens":22,"cacheReadTokens":0,"reasoningTokens":19}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34],"surfaceOp":"append"} -{"type":"step/end","seq":36,"time":1783352161516,"data":{"turn":1,"step":1}} -{"type":"turn/end","seq":37,"time":1783352161516,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"user/message","seq":1,"time":1784902776029,"data":{"content":[{"type":"text","text":"What is my favorite color? Reply with just the color and stop. Do not use any tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"user/message","seq":2,"time":1784902776029,"data":{"content":[{"type":"text","text":"The user has previously stated their favorite color is teal."}],"source":{"kind":"plugin","plugin":"hooks-claude"}},"surfaceOp":"append"} +{"type":"session/title","seq":3,"time":1784902776029,"data":{"title":"What is my favorite color?","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"step/start","seq":4,"time":1784902776052,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":5,"time":1784902776052,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":6,"time":1784902776061,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":7,"time":1784902776062,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":8,"time":1784902776062,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":9,"time":1783352161228,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} +{"type":"assistant/chunk","seq":10,"time":1783352161229,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" favorite"}}} +{"type":"assistant/chunk","seq":11,"time":1783352161335,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" color"}}} +{"type":"assistant/chunk","seq":12,"time":1783352161363,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} +{"type":"assistant/chunk","seq":13,"time":1783352161363,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" te"}}} +{"type":"assistant/chunk","seq":14,"time":1783352161392,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"al"}}} +{"type":"assistant/chunk","seq":15,"time":1783352161392,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":16,"time":1783352161392,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" as"}}} +{"type":"assistant/chunk","seq":17,"time":1783352161393,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stated"}}} +{"type":"assistant/chunk","seq":18,"time":1783352161393,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} +{"type":"assistant/chunk","seq":19,"time":1783352161420,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":20,"time":1783352161421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" context"}}} +{"type":"assistant/chunk","seq":21,"time":1783352161421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" provided"}}} +{"type":"assistant/chunk","seq":22,"time":1783352161421,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" by"}}} +{"type":"assistant/chunk","seq":23,"time":1783352161449,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":24,"time":1783352161449,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" plugin"}}} +{"type":"assistant/chunk","seq":25,"time":1783352161449,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":26,"time":1783352161477,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":27,"time":1783352161478,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"te"}}} +{"type":"assistant/chunk","seq":28,"time":1783352161478,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"al"}}} +{"type":"assistant/chunk","seq":29,"time":1783352161478,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user's favorite color is teal, as stated in the context provided by the plugin."}}}} +{"type":"assistant/chunk","seq":30,"time":1783352161478,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"teal"}}}} +{"type":"assistant/chunk","seq":31,"time":1783352161511,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2892,"outputTokens":22,"cacheReadTokens":0,"reasoningTokens":19}}}} +{"type":"assistant/chunk","seq":32,"time":1783352161512,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":33,"time":1784902776064,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user's favorite color is teal, as stated in the context provided by the plugin."},{"type":"text","text":"teal"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":2892,"outputTokens":22,"cacheReadTokens":0,"reasoningTokens":19}},"sourceEventSeqs":[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32],"surfaceOp":"append"} +{"type":"step/end","seq":34,"time":1784902776064,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":35,"time":1784902776065,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/session.jsonl index 126a761309..3d6badf9da 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/session.jsonl @@ -1,6 +1 @@ {"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} -{"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"hook/invoked","seq":1,"time":0,"data":{"turn":1,"point":"UserPromptSubmit","dialect":"codex","handlerId":"codex:UserPromptSubmit:1"}} -{"type":"hook/result","seq":2,"time":0,"data":{"turn":1,"point":"UserPromptSubmit","handlerId":"codex:UserPromptSubmit:1","decision":"block","exitCode":2,"stderrSummary":"blocked by codex policy hook","durationMs":0}} -{"type":"prompt/blocked","seq":3,"time":0,"data":{"content":[{"type":"text","text":"Delete everything in the repo."}],"source":{"kind":"user"},"reason":"blocked by codex policy hook"}} -{"type":"turn/end","seq":4,"time":0,"data":{"turn":1,"reason":{"kind":"rejected","reason":"blocked by codex policy hook"}}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl index ac319964f3..5521362a47 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl @@ -1,58 +1,56 @@ {"type":"session","version":0,"id":"0bebc0f4-a089-4fde-9b6e-db9532cfd4de","createdAt":1783352209682,"cwd":"/tmp/acp-snap-cwd-aopaZV","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352209686,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"hook/invoked","seq":1,"time":1783352209687,"data":{"turn":1,"point":"UserPromptSubmit","dialect":"codex","handlerId":"codex:UserPromptSubmit:1"}} -{"type":"hook/result","seq":2,"time":1783352209706,"data":{"turn":1,"point":"UserPromptSubmit","handlerId":"codex:UserPromptSubmit:1","decision":"pass","exitCode":0,"durationMs":19.49695100000008}} -{"type":"user/message","seq":3,"time":1783352209707,"data":{"content":[{"type":"text","text":"What is my favorite color? Reply with just the color and stop. Do not use any tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"user/message","seq":4,"time":1783352209707,"data":{"content":[{"type":"text","text":"The user has previously stated their favorite color is teal."}],"source":{"kind":"plugin","plugin":"hooks-codex"}},"surfaceOp":"append"} -{"type":"session/title","seq":5,"time":1783352209707,"data":{"title":"What is my favorite color?","messageSeqs":[3],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":6,"time":1783352209709,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":7,"time":1783352209710,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"assistant/chunk","seq":8,"time":1783352210353,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":9,"time":1783352210353,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":10,"time":1783352210470,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":11,"time":1783352210501,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} -{"type":"assistant/chunk","seq":12,"time":1783352210527,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" about"}}} -{"type":"assistant/chunk","seq":13,"time":1783352210555,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" their"}}} -{"type":"assistant/chunk","seq":14,"time":1783352210556,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" favorite"}}} -{"type":"assistant/chunk","seq":15,"time":1783352210556,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" color"}}} -{"type":"assistant/chunk","seq":16,"time":1783352210556,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":17,"time":1783352210556,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":18,"time":1783352210585,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":19,"time":1783352210585,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" context"}}} -{"type":"assistant/chunk","seq":20,"time":1783352210585,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tells"}}} -{"type":"assistant/chunk","seq":21,"time":1783352210612,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":22,"time":1783352210613,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" they"}}} -{"type":"assistant/chunk","seq":23,"time":1783352210613,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" previously"}}} -{"type":"assistant/chunk","seq":24,"time":1783352210640,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stated"}}} -{"type":"assistant/chunk","seq":25,"time":1783352210641,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} -{"type":"assistant/chunk","seq":26,"time":1783352210668,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} -{"type":"assistant/chunk","seq":27,"time":1783352210668,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" te"}}} -{"type":"assistant/chunk","seq":28,"time":1783352210669,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"al"}}} -{"type":"assistant/chunk","seq":29,"time":1783352210669,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":30,"time":1783352210669,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" They"}}} -{"type":"assistant/chunk","seq":31,"time":1783352210697,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} -{"type":"assistant/chunk","seq":32,"time":1783352210697,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":33,"time":1783352210697,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":34,"time":1783352210697,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":35,"time":1783352210726,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":36,"time":1783352210726,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} -{"type":"assistant/chunk","seq":37,"time":1783352210726,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":38,"time":1783352210727,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" color"}}} -{"type":"assistant/chunk","seq":39,"time":1783352210727,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":40,"time":1783352210727,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} -{"type":"assistant/chunk","seq":41,"time":1783352210754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":42,"time":1783352210754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" without"}}} -{"type":"assistant/chunk","seq":43,"time":1783352210754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" using"}}} -{"type":"assistant/chunk","seq":44,"time":1783352210754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" any"}}} -{"type":"assistant/chunk","seq":45,"time":1783352210754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tools"}}} -{"type":"assistant/chunk","seq":46,"time":1783352210755,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":47,"time":1783352210787,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":48,"time":1783352210787,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"te"}}} -{"type":"assistant/chunk","seq":49,"time":1783352210787,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"al"}}} -{"type":"assistant/chunk","seq":50,"time":1783352210788,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked about their favorite color, and the context tells me they previously stated it's teal. They asked me to reply with just the color and stop, without using any tools."}}}} -{"type":"assistant/chunk","seq":51,"time":1783352210788,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"teal"}}}} -{"type":"assistant/chunk","seq":52,"time":1783352210788,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2891,"outputTokens":41,"cacheReadTokens":0,"reasoningTokens":38}}}} -{"type":"assistant/chunk","seq":53,"time":1783352210788,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":54,"time":1783352210790,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user asked about their favorite color, and the context tells me they previously stated it's teal. They asked me to reply with just the color and stop, without using any tools."},{"type":"text","text":"teal"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":2891,"outputTokens":41,"cacheReadTokens":0,"reasoningTokens":38}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53],"surfaceOp":"append"} -{"type":"step/end","seq":55,"time":1783352210790,"data":{"turn":1,"step":1}} -{"type":"turn/end","seq":56,"time":1783352210790,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"user/message","seq":1,"time":1784902777073,"data":{"content":[{"type":"text","text":"What is my favorite color? Reply with just the color and stop. Do not use any tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"user/message","seq":2,"time":1784902777073,"data":{"content":[{"type":"text","text":"The user has previously stated their favorite color is teal."}],"source":{"kind":"plugin","plugin":"hooks-codex"}},"surfaceOp":"append"} +{"type":"session/title","seq":3,"time":1784902777073,"data":{"title":"What is my favorite color?","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"step/start","seq":4,"time":1784902777093,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":5,"time":1784902777094,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":6,"time":1784902777101,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":7,"time":1784902777101,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":8,"time":1784902777101,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":9,"time":1783352210353,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} +{"type":"assistant/chunk","seq":10,"time":1783352210353,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" about"}}} +{"type":"assistant/chunk","seq":11,"time":1783352210470,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" their"}}} +{"type":"assistant/chunk","seq":12,"time":1783352210501,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" favorite"}}} +{"type":"assistant/chunk","seq":13,"time":1783352210527,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" color"}}} +{"type":"assistant/chunk","seq":14,"time":1783352210555,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":15,"time":1783352210556,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":16,"time":1783352210556,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":17,"time":1783352210556,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" context"}}} +{"type":"assistant/chunk","seq":18,"time":1783352210556,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tells"}}} +{"type":"assistant/chunk","seq":19,"time":1783352210585,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":20,"time":1783352210585,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" they"}}} +{"type":"assistant/chunk","seq":21,"time":1783352210585,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" previously"}}} +{"type":"assistant/chunk","seq":22,"time":1783352210612,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stated"}}} +{"type":"assistant/chunk","seq":23,"time":1783352210613,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":24,"time":1783352210613,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} +{"type":"assistant/chunk","seq":25,"time":1783352210640,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" te"}}} +{"type":"assistant/chunk","seq":26,"time":1783352210641,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"al"}}} +{"type":"assistant/chunk","seq":27,"time":1783352210668,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":28,"time":1783352210668,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" They"}}} +{"type":"assistant/chunk","seq":29,"time":1783352210669,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} +{"type":"assistant/chunk","seq":30,"time":1783352210669,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":31,"time":1783352210669,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":32,"time":1783352210697,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":33,"time":1783352210697,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":34,"time":1783352210697,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} +{"type":"assistant/chunk","seq":35,"time":1783352210697,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":36,"time":1783352210726,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" color"}}} +{"type":"assistant/chunk","seq":37,"time":1783352210726,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":38,"time":1783352210726,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} +{"type":"assistant/chunk","seq":39,"time":1783352210727,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":40,"time":1783352210727,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" without"}}} +{"type":"assistant/chunk","seq":41,"time":1783352210727,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" using"}}} +{"type":"assistant/chunk","seq":42,"time":1783352210754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" any"}}} +{"type":"assistant/chunk","seq":43,"time":1783352210754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tools"}}} +{"type":"assistant/chunk","seq":44,"time":1783352210754,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":45,"time":1783352210754,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":46,"time":1783352210754,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"te"}}} +{"type":"assistant/chunk","seq":47,"time":1783352210755,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"al"}}} +{"type":"assistant/chunk","seq":48,"time":1783352210787,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked about their favorite color, and the context tells me they previously stated it's teal. They asked me to reply with just the color and stop, without using any tools."}}}} +{"type":"assistant/chunk","seq":49,"time":1783352210787,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"teal"}}}} +{"type":"assistant/chunk","seq":50,"time":1783352210787,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2891,"outputTokens":41,"cacheReadTokens":0,"reasoningTokens":38}}}} +{"type":"assistant/chunk","seq":51,"time":1783352210788,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":52,"time":1784902777103,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user asked about their favorite color, and the context tells me they previously stated it's teal. They asked me to reply with just the color and stop, without using any tools."},{"type":"text","text":"teal"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":2891,"outputTokens":41,"cacheReadTokens":0,"reasoningTokens":38}},"sourceEventSeqs":[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51],"surfaceOp":"append"} +{"type":"step/end","seq":53,"time":1784902777103,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":54,"time":1784902777103,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/permission-switching/session.jsonl b/examples/acp-agent/tests/snapshots/permission-switching/session.jsonl index 381458957c..cda402f60b 100644 --- a/examples/acp-agent/tests/snapshots/permission-switching/session.jsonl +++ b/examples/acp-agent/tests/snapshots/permission-switching/session.jsonl @@ -1,239 +1,240 @@ {"type":"session","version":0,"id":"df041acb-2f14-4d5f-b6e2-2fb6b9eb6427","createdAt":1783860666204,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-4oJKT4","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783860666206,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"permission/preset","seq":1,"time":1783962244578,"data":{"preset":"workspace-write"}} -{"type":"sandbox/mode","seq":2,"time":1784518115721,"data":{"mode":"workspace-write"}} -{"type":"approval/policy","seq":3,"time":1783962244578,"data":{"policy":"ask"}} -{"type":"user/message","seq":4,"time":1783962244578,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly this one command in a single call: printf 'before\\n' > out.txt && cat out.txt. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"session/title","seq":5,"time":1783962244578,"data":{"title":"Use the bash tool to","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":6,"time":1783962244579,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":7,"time":1783962244580,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"assistant/chunk","seq":8,"time":1783860667444,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":9,"time":1783860667445,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"user/message","seq":1,"time":1784903373364,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly this one command in a single call: printf 'before\\n' > out.txt && cat out.txt. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"session/title","seq":2,"time":1784903373364,"data":{"title":"Use the bash tool to","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"permission/preset","seq":3,"time":1784903373381,"data":{"preset":"workspace-write"}} +{"type":"sandbox/mode","seq":4,"time":1784903373381,"data":{"mode":"workspace-write"}} +{"type":"approval/policy","seq":5,"time":1784903373381,"data":{"policy":"ask"}} +{"type":"step/start","seq":6,"time":1784903373382,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":7,"time":1784903373382,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":8,"time":1784903373390,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":9,"time":1783860667444,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} {"type":"assistant/chunk","seq":10,"time":1783860667445,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":11,"time":1783860667446,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":11,"time":1783860667445,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} {"type":"assistant/chunk","seq":12,"time":1783860667446,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} {"type":"assistant/chunk","seq":13,"time":1783860667446,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":14,"time":1783860667478,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} +{"type":"assistant/chunk","seq":14,"time":1783860667446,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} {"type":"assistant/chunk","seq":15,"time":1783860667478,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} {"type":"assistant/chunk","seq":16,"time":1783860667478,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" specific"}}} -{"type":"assistant/chunk","seq":17,"time":1783860667479,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} -{"type":"assistant/chunk","seq":18,"time":1783860667501,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":19,"time":1783860667502,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} +{"type":"assistant/chunk","seq":17,"time":1783860667478,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} +{"type":"assistant/chunk","seq":18,"time":1783860667479,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":19,"time":1783860667501,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} {"type":"assistant/chunk","seq":20,"time":1783860667502,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} {"type":"assistant/chunk","seq":21,"time":1783860667502,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} {"type":"assistant/chunk","seq":22,"time":1783860667502,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":23,"time":1783860667595,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"D"}}} -{"type":"assistant/chunk","seq":24,"time":1783860667596,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} -{"type":"assistant/chunk","seq":25,"time":1783860667623,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} -{"type":"assistant/chunk","seq":26,"time":1783860667624,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":23,"time":1783860667502,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"D"}}} +{"type":"assistant/chunk","seq":24,"time":1783860667595,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":25,"time":1783860667596,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":26,"time":1783860667623,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":27,"time":1783860667624,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":""}}} {"type":"assistant/chunk","seq":28,"time":1783860667624,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"{"}}} {"type":"assistant/chunk","seq":29,"time":1783860667624,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":30,"time":1783860667656,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"command"}}} -{"type":"assistant/chunk","seq":31,"time":1783860667657,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":30,"time":1783860667624,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":31,"time":1783860667656,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":32,"time":1783860667657,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":33,"time":1783860667657,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":34,"time":1783860667657,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"printf"}}} -{"type":"assistant/chunk","seq":35,"time":1783860667686,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" '"}}} -{"type":"assistant/chunk","seq":36,"time":1783860667687,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"before"}}} +{"type":"assistant/chunk","seq":35,"time":1783860667657,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" '"}}} +{"type":"assistant/chunk","seq":36,"time":1783860667686,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"before"}}} {"type":"assistant/chunk","seq":37,"time":1783860667687,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"\\\\n"}}} {"type":"assistant/chunk","seq":38,"time":1783860667687,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"'"}}} {"type":"assistant/chunk","seq":39,"time":1783860667687,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" >"}}} {"type":"assistant/chunk","seq":40,"time":1783860667687,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" out"}}} -{"type":"assistant/chunk","seq":41,"time":1783860667710,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":41,"time":1783860667687,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":".txt"}}} {"type":"assistant/chunk","seq":42,"time":1783860667710,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" &&"}}} -{"type":"assistant/chunk","seq":43,"time":1783860667711,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" cat"}}} -{"type":"assistant/chunk","seq":44,"time":1783860667738,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" out"}}} -{"type":"assistant/chunk","seq":45,"time":1783860667739,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":43,"time":1783860667710,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" cat"}}} +{"type":"assistant/chunk","seq":44,"time":1783860667711,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" out"}}} +{"type":"assistant/chunk","seq":45,"time":1783860667738,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":".txt"}}} {"type":"assistant/chunk","seq":46,"time":1783860667739,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":47,"time":1783860667776,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":47,"time":1783860667739,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":", "}}} {"type":"assistant/chunk","seq":48,"time":1783860667776,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":49,"time":1783860667776,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"description"}}} {"type":"assistant/chunk","seq":50,"time":1783860667776,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":51,"time":1783860667796,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":51,"time":1783860667776,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":52,"time":1783860667796,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":53,"time":1783860667834,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"Write"}}} -{"type":"assistant/chunk","seq":54,"time":1783860667835,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" and"}}} -{"type":"assistant/chunk","seq":55,"time":1783860667863,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" then"}}} +{"type":"assistant/chunk","seq":53,"time":1783860667796,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"Write"}}} +{"type":"assistant/chunk","seq":54,"time":1783860667834,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" and"}}} +{"type":"assistant/chunk","seq":55,"time":1783860667835,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" then"}}} {"type":"assistant/chunk","seq":56,"time":1783860667863,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" read"}}} -{"type":"assistant/chunk","seq":57,"time":1783860667889,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" out"}}} -{"type":"assistant/chunk","seq":58,"time":1783860667918,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":57,"time":1783860667863,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":" out"}}} +{"type":"assistant/chunk","seq":58,"time":1783860667889,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":".txt"}}} {"type":"assistant/chunk","seq":59,"time":1783860667918,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":60,"time":1783860667918,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","argumentsDelta":"}"}}} {"type":"assistant/chunk","seq":61,"time":1783860667918,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run a specific command and then reply with \"DONE\"."}}}} -{"type":"assistant/chunk","seq":62,"time":1783962244582,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","arguments":"{\"command\": \"printf 'before\\\\n' > out.txt && cat out.txt\", \"description\": \"Write and then read out.txt\"}"}}}} +{"type":"assistant/chunk","seq":62,"time":1783860667918,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","arguments":"{\"command\": \"printf 'before\\\\n' > out.txt && cat out.txt\", \"description\": \"Write and then read out.txt\"}"}}}} {"type":"assistant/chunk","seq":63,"time":1783962244582,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1411,"outputTokens":92,"cacheReadTokens":0,"reasoningTokens":17}}}} {"type":"assistant/chunk","seq":64,"time":1783962244582,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":65,"time":1783962244582,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a specific command and then reply with \"DONE\"."},{"type":"tool-call","id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","arguments":"{\"command\": \"printf 'before\\\\n' > out.txt && cat out.txt\", \"description\": \"Write and then read out.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":1411,"outputTokens":92,"cacheReadTokens":0,"reasoningTokens":17}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64],"surfaceOp":"append"} -{"type":"tool/call","seq":66,"time":1783962244582,"data":{"turn":1,"step":1,"callId":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","arguments":"{\"command\": \"printf 'before\\\\n' > out.txt && cat out.txt\", \"description\": \"Write and then read out.txt\"}"}} -{"type":"tool/result","seq":67,"time":1783962244599,"data":{"turn":1,"step":1,"callId":"call_00_E1vtulcKU1LKUgLahxdR3767","content":[{"type":"text","text":"before\n"}],"isError":false},"sourceEventSeqs":[66],"surfaceOp":"append"} -{"type":"step/end","seq":68,"time":1783962244599,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":69,"time":1783962244600,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":70,"time":1783860669145,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":71,"time":1783860669172,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":72,"time":1783860669174,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} -{"type":"assistant/chunk","seq":73,"time":1783860669209,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" ran"}}} -{"type":"assistant/chunk","seq":74,"time":1783860669210,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" successfully"}}} -{"type":"assistant/chunk","seq":75,"time":1783860669235,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":76,"time":1783860669235,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" output"}}} -{"type":"assistant/chunk","seq":77,"time":1783860669236,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":78,"time":1783860669262,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"before"}}} -{"type":"assistant/chunk","seq":79,"time":1783860669264,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} -{"type":"assistant/chunk","seq":80,"time":1783860669264,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Now"}}} -{"type":"assistant/chunk","seq":81,"time":1783860669264,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} -{"type":"assistant/chunk","seq":82,"time":1783860669264,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} -{"type":"assistant/chunk","seq":83,"time":1783860669292,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":84,"time":1783860669292,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":85,"time":1783860669322,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":86,"time":1783860669323,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} -{"type":"assistant/chunk","seq":87,"time":1783860669356,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":88,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} -{"type":"assistant/chunk","seq":89,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" D"}}} -{"type":"assistant/chunk","seq":90,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} -{"type":"assistant/chunk","seq":91,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":92,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":93,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} -{"type":"assistant/chunk","seq":94,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} -{"type":"assistant/chunk","seq":95,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The command ran successfully, output \"before\". Now I need to reply with just the word DONE."}}}} -{"type":"assistant/chunk","seq":96,"time":1783962244601,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} -{"type":"assistant/chunk","seq":97,"time":1783962244601,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":109,"outputTokens":24,"cacheReadTokens":1408,"reasoningTokens":21}}}} -{"type":"assistant/chunk","seq":98,"time":1783962244601,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":99,"time":1783962244601,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The command ran successfully, output \"before\". Now I need to reply with just the word DONE."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":109,"outputTokens":24,"cacheReadTokens":1408,"reasoningTokens":21}},"sourceEventSeqs":[70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98],"surfaceOp":"append"} -{"type":"step/end","seq":100,"time":1783962244601,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":101,"time":1783962244601,"data":{"turn":1,"reason":{"kind":"completed"}}} -{"type":"turn/start","seq":102,"time":1783962244623,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"permission/preset","seq":103,"time":1783962244624,"data":{"preset":"danger-full-access"}} -{"type":"sandbox/mode","seq":104,"time":1784518115842,"data":{"mode":"danger-full-access"}} -{"type":"approval/policy","seq":105,"time":1783962244624,"data":{"policy":"never"}} -{"type":"user/message","seq":106,"time":1783962244624,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: cat out.txt. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"user/message","seq":107,"time":1783962244624,"data":{"content":[{"type":"text","text":"The approval policy changed from \"ask\" to \"never\" (changed by the user)."}],"source":{"kind":"plugin","plugin":"user-approval"}},"surfaceOp":"append"} -{"type":"step/start","seq":108,"time":1783962244624,"data":{"turn":2,"step":1}} -{"type":"request/header","seq":109,"time":1784000791271,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"change"}} -{"type":"assistant/chunk","seq":110,"time":1783860671025,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":111,"time":1783860671026,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/message","seq":65,"time":1784903373393,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a specific command and then reply with \"DONE\"."},{"type":"tool-call","id":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","arguments":"{\"command\": \"printf 'before\\\\n' > out.txt && cat out.txt\", \"description\": \"Write and then read out.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":1411,"outputTokens":92,"cacheReadTokens":0,"reasoningTokens":17}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64],"surfaceOp":"append"} +{"type":"tool/call","seq":66,"time":1784903373393,"data":{"turn":1,"step":1,"callId":"call_00_E1vtulcKU1LKUgLahxdR3767","name":"bash","arguments":"{\"command\": \"printf 'before\\\\n' > out.txt && cat out.txt\", \"description\": \"Write and then read out.txt\"}"}} +{"type":"tool/result","seq":67,"time":1784903373416,"data":{"turn":1,"step":1,"callId":"call_00_E1vtulcKU1LKUgLahxdR3767","content":[{"type":"text","text":"before\n"}],"isError":false},"sourceEventSeqs":[66],"surfaceOp":"append"} +{"type":"step/end","seq":68,"time":1784903373417,"data":{"turn":1,"step":1}} +{"type":"user/message","seq":69,"time":1784903373424,"data":{"content":[{"type":"text","text":"The approval policy changed from \"never\" to \"ask\" (changed by the operator/config)."}],"source":{"kind":"plugin","plugin":"user-approval"}},"surfaceOp":"append"} +{"type":"step/start","seq":70,"time":1783962244600,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":71,"time":1783860669145,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":72,"time":1783860669172,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":73,"time":1783860669174,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" command"}}} +{"type":"assistant/chunk","seq":74,"time":1783860669209,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" ran"}}} +{"type":"assistant/chunk","seq":75,"time":1783860669210,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" successfully"}}} +{"type":"assistant/chunk","seq":76,"time":1783860669235,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":77,"time":1783860669235,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" output"}}} +{"type":"assistant/chunk","seq":78,"time":1783860669236,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":79,"time":1783860669262,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"before"}}} +{"type":"assistant/chunk","seq":80,"time":1783860669264,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":81,"time":1783860669264,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Now"}}} +{"type":"assistant/chunk","seq":82,"time":1783860669264,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":83,"time":1783860669264,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} +{"type":"assistant/chunk","seq":84,"time":1783860669292,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":85,"time":1783860669292,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":86,"time":1783860669322,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":87,"time":1783860669323,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} +{"type":"assistant/chunk","seq":88,"time":1783860669356,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":89,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":90,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" D"}}} +{"type":"assistant/chunk","seq":91,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":92,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":93,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":94,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} +{"type":"assistant/chunk","seq":95,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":96,"time":1783860669357,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The command ran successfully, output \"before\". Now I need to reply with just the word DONE."}}}} +{"type":"assistant/chunk","seq":97,"time":1783962244601,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":98,"time":1783962244601,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":109,"outputTokens":24,"cacheReadTokens":1408,"reasoningTokens":21}}}} +{"type":"assistant/chunk","seq":99,"time":1783962244601,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":100,"time":1783962244601,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The command ran successfully, output \"before\". Now I need to reply with just the word DONE."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":109,"outputTokens":24,"cacheReadTokens":1408,"reasoningTokens":21}},"sourceEventSeqs":[71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99],"surfaceOp":"append"} +{"type":"step/end","seq":101,"time":1783962244601,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":102,"time":1783962244601,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":103,"time":1783962244623,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":104,"time":1784903373437,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: cat out.txt. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"permission/preset","seq":105,"time":1784903373442,"data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","seq":106,"time":1784903373442,"data":{"mode":"danger-full-access"}} +{"type":"approval/policy","seq":107,"time":1784903373442,"data":{"policy":"never"}} +{"type":"step/start","seq":108,"time":1784903373442,"data":{"turn":2,"step":1}} +{"type":"request/header","seq":109,"time":1784903373443,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"change"}} +{"type":"assistant/chunk","seq":110,"time":1784903373450,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":111,"time":1783860671025,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} {"type":"assistant/chunk","seq":112,"time":1783860671026,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} {"type":"assistant/chunk","seq":113,"time":1783860671026,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} {"type":"assistant/chunk","seq":114,"time":1783860671026,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":115,"time":1783860671079,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":116,"time":1783860671080,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} +{"type":"assistant/chunk","seq":115,"time":1783860671026,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":116,"time":1783860671079,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" run"}}} {"type":"assistant/chunk","seq":117,"time":1783860671080,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" `"}}} {"type":"assistant/chunk","seq":118,"time":1783860671080,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"cat"}}} {"type":"assistant/chunk","seq":119,"time":1783860671080,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" out"}}} {"type":"assistant/chunk","seq":120,"time":1783860671080,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".txt"}}} -{"type":"assistant/chunk","seq":121,"time":1783860671097,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"`"}}} -{"type":"assistant/chunk","seq":122,"time":1783860671101,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":121,"time":1783860671080,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":122,"time":1783860671097,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} {"type":"assistant/chunk","seq":123,"time":1783860671101,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} -{"type":"assistant/chunk","seq":124,"time":1783860671102,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":124,"time":1783860671101,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} {"type":"assistant/chunk","seq":125,"time":1783860671102,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} {"type":"assistant/chunk","seq":126,"time":1783860671102,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":127,"time":1783860671175,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"D"}}} +{"type":"assistant/chunk","seq":127,"time":1783860671102,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"D"}}} {"type":"assistant/chunk","seq":128,"time":1783860671175,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} -{"type":"assistant/chunk","seq":129,"time":1783860671211,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} -{"type":"assistant/chunk","seq":130,"time":1783860671212,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":129,"time":1783860671175,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":130,"time":1783860671211,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":131,"time":1783860671212,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":""}}} -{"type":"assistant/chunk","seq":132,"time":1783860671228,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"{"}}} -{"type":"assistant/chunk","seq":133,"time":1783860671229,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":132,"time":1783860671212,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":133,"time":1783860671228,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":134,"time":1783860671229,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"description"}}} {"type":"assistant/chunk","seq":135,"time":1783860671229,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":136,"time":1783860671261,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":": "}}} -{"type":"assistant/chunk","seq":137,"time":1783860671262,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":138,"time":1783860671301,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"Read"}}} -{"type":"assistant/chunk","seq":139,"time":1783860671316,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":" out"}}} +{"type":"assistant/chunk","seq":136,"time":1783860671229,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":137,"time":1783860671261,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":138,"time":1783860671262,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"Read"}}} +{"type":"assistant/chunk","seq":139,"time":1783860671301,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":" out"}}} {"type":"assistant/chunk","seq":140,"time":1783860671316,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":".txt"}}} {"type":"assistant/chunk","seq":141,"time":1783860671316,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":142,"time":1783860671316,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":", "}}} {"type":"assistant/chunk","seq":143,"time":1783860671316,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":144,"time":1783860671350,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"command"}}} -{"type":"assistant/chunk","seq":145,"time":1783860671351,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":144,"time":1783860671316,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":145,"time":1783860671350,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} {"type":"assistant/chunk","seq":146,"time":1783860671351,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":": "}}} {"type":"assistant/chunk","seq":147,"time":1783860671351,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":148,"time":1783860671388,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"cat"}}} +{"type":"assistant/chunk","seq":148,"time":1783860671351,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"cat"}}} {"type":"assistant/chunk","seq":149,"time":1783860671388,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":" out"}}} -{"type":"assistant/chunk","seq":150,"time":1783860671435,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":150,"time":1783860671388,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":".txt"}}} {"type":"assistant/chunk","seq":151,"time":1783860671435,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"\""}}} -{"type":"assistant/chunk","seq":152,"time":1783860671436,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":152,"time":1783860671435,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","argumentsDelta":"}"}}} {"type":"assistant/chunk","seq":153,"time":1783860671436,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to run `cat out.txt` and then reply with \"DONE\"."}}}} -{"type":"assistant/chunk","seq":154,"time":1783962244626,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","arguments":"{\"description\": \"Read out.txt\", \"command\": \"cat out.txt\"}"}}}} +{"type":"assistant/chunk","seq":154,"time":1783860671436,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","arguments":"{\"description\": \"Read out.txt\", \"command\": \"cat out.txt\"}"}}}} {"type":"assistant/chunk","seq":155,"time":1783962244626,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1608,"outputTokens":82,"cacheReadTokens":0,"reasoningTokens":19}}}} {"type":"assistant/chunk","seq":156,"time":1783962244626,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":157,"time":1783962244626,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run `cat out.txt` and then reply with \"DONE\"."},{"type":"tool-call","id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","arguments":"{\"description\": \"Read out.txt\", \"command\": \"cat out.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":1608,"outputTokens":82,"cacheReadTokens":0,"reasoningTokens":19}},"sourceEventSeqs":[110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156],"surfaceOp":"append"} -{"type":"tool/call","seq":158,"time":1783962244626,"data":{"turn":2,"step":1,"callId":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","arguments":"{\"description\": \"Read out.txt\", \"command\": \"cat out.txt\"}"}} -{"type":"tool/result","seq":159,"time":1783962244631,"data":{"turn":2,"step":1,"callId":"call_00_7Jb7FWHNjIBVML49dEJl1990","content":[{"type":"text","text":"before\n"}],"isError":false},"sourceEventSeqs":[158],"surfaceOp":"append"} -{"type":"step/end","seq":160,"time":1783962244631,"data":{"turn":2,"step":1}} -{"type":"step/start","seq":161,"time":1783962244631,"data":{"turn":2,"step":2}} -{"type":"assistant/chunk","seq":162,"time":1783860673229,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} -{"type":"assistant/chunk","seq":163,"time":1783860673229,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":0,"text":"D"}}} -{"type":"assistant/chunk","seq":164,"time":1783860673229,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":0,"text":"ONE"}}} -{"type":"assistant/chunk","seq":165,"time":1783962244632,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}} -{"type":"assistant/chunk","seq":166,"time":1783962244632,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":40,"outputTokens":3,"cacheReadTokens":1664,"reasoningTokens":0}}}} -{"type":"assistant/chunk","seq":167,"time":1783962244632,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":168,"time":1783962244632,"data":{"turn":2,"step":2,"content":[{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":40,"outputTokens":3,"cacheReadTokens":1664,"reasoningTokens":0}},"sourceEventSeqs":[162,163,164,165,166,167],"surfaceOp":"append"} -{"type":"step/end","seq":169,"time":1783962244632,"data":{"turn":2,"step":2}} -{"type":"turn/end","seq":170,"time":1783962244632,"data":{"turn":2,"reason":{"kind":"completed"}}} -{"type":"turn/start","seq":171,"time":1783962244637,"data":{"turn":3,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"user/message","seq":172,"time":1783962244637,"data":{"content":[{"type":"text","text":"Without using any tools, state your current approval policy in one short sentence and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"step/start","seq":173,"time":1783962244637,"data":{"turn":3,"step":1}} -{"type":"assistant/chunk","seq":174,"time":1783860674433,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":175,"time":1783860674435,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":176,"time":1783860674435,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":177,"time":1783860674465,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} -{"type":"assistant/chunk","seq":178,"time":1783860674465,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":179,"time":1783860674499,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":180,"time":1783860674500,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" state"}}} -{"type":"assistant/chunk","seq":181,"time":1783860674500,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":182,"time":1783860674500,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" current"}}} -{"type":"assistant/chunk","seq":183,"time":1783860674500,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" approval"}}} -{"type":"assistant/chunk","seq":184,"time":1783860674525,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" policy"}}} -{"type":"assistant/chunk","seq":185,"time":1783860674526,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" without"}}} -{"type":"assistant/chunk","seq":186,"time":1783860674526,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" using"}}} -{"type":"assistant/chunk","seq":187,"time":1783860674526,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" any"}}} -{"type":"assistant/chunk","seq":188,"time":1783860674526,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tools"}}} -{"type":"assistant/chunk","seq":189,"time":1783860674550,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":190,"time":1783860674550,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" From"}}} -{"type":"assistant/chunk","seq":191,"time":1783860674581,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":192,"time":1783860674582,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" context"}}} -{"type":"assistant/chunk","seq":193,"time":1783860674582,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" provided"}}} -{"type":"assistant/chunk","seq":194,"time":1783860674610,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":195,"time":1783860674611,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":196,"time":1783860674611,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" approval"}}} -{"type":"assistant/chunk","seq":197,"time":1783860674611,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" policy"}}} -{"type":"assistant/chunk","seq":198,"time":1783860674638,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" changed"}}} -{"type":"assistant/chunk","seq":199,"time":1783860674638,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" from"}}} -{"type":"assistant/chunk","seq":200,"time":1783860674638,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":201,"time":1783860674638,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ask"}}} -{"type":"assistant/chunk","seq":202,"time":1783860674638,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} -{"type":"assistant/chunk","seq":203,"time":1783860674640,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":204,"time":1783860674698,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":205,"time":1783860674698,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"never"}}} -{"type":"assistant/chunk","seq":206,"time":1783860674700,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} -{"type":"assistant/chunk","seq":207,"time":1783860674726,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":208,"time":1783860674726,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":"My"}}} -{"type":"assistant/chunk","seq":209,"time":1783860674727,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" approval"}}} -{"type":"assistant/chunk","seq":210,"time":1783860674727,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" policy"}}} -{"type":"assistant/chunk","seq":211,"time":1783860674754,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" is"}}} -{"type":"assistant/chunk","seq":212,"time":1783860674757,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" set"}}} -{"type":"assistant/chunk","seq":213,"time":1783860674757,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" to"}}} -{"type":"assistant/chunk","seq":214,"time":1783860674757,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" \""}}} -{"type":"assistant/chunk","seq":215,"time":1783860674757,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":"never"}}} -{"type":"assistant/chunk","seq":216,"time":1783860674786,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":"\""}}} -{"type":"assistant/chunk","seq":217,"time":1783860674817,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" —"}}} -{"type":"assistant/chunk","seq":218,"time":1783860674846,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" no"}}} -{"type":"assistant/chunk","seq":219,"time":1783860674875,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" actions"}}} -{"type":"assistant/chunk","seq":220,"time":1783860674879,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" requiring"}}} -{"type":"assistant/chunk","seq":221,"time":1783860674880,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" approval"}}} -{"type":"assistant/chunk","seq":222,"time":1783860674904,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" are"}}} -{"type":"assistant/chunk","seq":223,"time":1783860674906,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" permitted"}}} -{"type":"assistant/chunk","seq":224,"time":1783860674906,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":","}}} -{"type":"assistant/chunk","seq":225,"time":1783860674939,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" and"}}} -{"type":"assistant/chunk","seq":226,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" they"}}} -{"type":"assistant/chunk","seq":227,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" are"}}} -{"type":"assistant/chunk","seq":228,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" rejected"}}} -{"type":"assistant/chunk","seq":229,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" automatically"}}} -{"type":"assistant/chunk","seq":230,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":"."}}} -{"type":"assistant/chunk","seq":231,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to state the current approval policy without using any tools. From the context provided, the approval policy changed from \"ask\" to \"never\"."}}}} -{"type":"assistant/chunk","seq":232,"time":1783962244639,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"My approval policy is set to \"never\" — no actions requiring approval are permitted, and they are rejected automatically."}}}} -{"type":"assistant/chunk","seq":233,"time":1783962244639,"data":{"turn":3,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":64,"outputTokens":56,"cacheReadTokens":1664,"reasoningTokens":32}}}} -{"type":"assistant/chunk","seq":234,"time":1783962244639,"data":{"turn":3,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":235,"time":1783962244640,"data":{"turn":3,"step":1,"content":[{"type":"reasoning","text":"The user wants me to state the current approval policy without using any tools. From the context provided, the approval policy changed from \"ask\" to \"never\"."},{"type":"text","text":"My approval policy is set to \"never\" — no actions requiring approval are permitted, and they are rejected automatically."}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":64,"outputTokens":56,"cacheReadTokens":1664,"reasoningTokens":32}},"sourceEventSeqs":[174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234],"surfaceOp":"append"} -{"type":"step/end","seq":236,"time":1783962244640,"data":{"turn":3,"step":1}} -{"type":"turn/end","seq":237,"time":1783962244640,"data":{"turn":3,"reason":{"kind":"completed"}}} +{"type":"assistant/message","seq":157,"time":1784903373452,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run `cat out.txt` and then reply with \"DONE\"."},{"type":"tool-call","id":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","arguments":"{\"description\": \"Read out.txt\", \"command\": \"cat out.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":1608,"outputTokens":82,"cacheReadTokens":0,"reasoningTokens":19}},"sourceEventSeqs":[110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156],"surfaceOp":"append"} +{"type":"tool/call","seq":158,"time":1784903373452,"data":{"turn":2,"step":1,"callId":"call_00_7Jb7FWHNjIBVML49dEJl1990","name":"bash","arguments":"{\"description\": \"Read out.txt\", \"command\": \"cat out.txt\"}"}} +{"type":"tool/result","seq":159,"time":1784903373464,"data":{"turn":2,"step":1,"callId":"call_00_7Jb7FWHNjIBVML49dEJl1990","content":[{"type":"text","text":"before\n"}],"isError":false},"sourceEventSeqs":[158],"surfaceOp":"append"} +{"type":"step/end","seq":160,"time":1784903373464,"data":{"turn":2,"step":1}} +{"type":"user/message","seq":161,"time":1784903373470,"data":{"content":[{"type":"text","text":"The approval policy changed from \"ask\" to \"never\" (changed by the operator/config)."}],"source":{"kind":"plugin","plugin":"user-approval"}},"surfaceOp":"append"} +{"type":"step/start","seq":162,"time":1783962244631,"data":{"turn":2,"step":2}} +{"type":"assistant/chunk","seq":163,"time":1783860673229,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":164,"time":1783860673229,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":0,"text":"D"}}} +{"type":"assistant/chunk","seq":165,"time":1783860673229,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":166,"time":1783962244632,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":167,"time":1783962244632,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":40,"outputTokens":3,"cacheReadTokens":1664,"reasoningTokens":0}}}} +{"type":"assistant/chunk","seq":168,"time":1783962244632,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":169,"time":1783962244632,"data":{"turn":2,"step":2,"content":[{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":40,"outputTokens":3,"cacheReadTokens":1664,"reasoningTokens":0}},"sourceEventSeqs":[163,164,165,166,167,168],"surfaceOp":"append"} +{"type":"step/end","seq":170,"time":1783962244632,"data":{"turn":2,"step":2}} +{"type":"turn/end","seq":171,"time":1783962244632,"data":{"turn":2,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":172,"time":1783962244637,"data":{"turn":3,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":173,"time":1783962244637,"data":{"content":[{"type":"text","text":"Without using any tools, state your current approval policy in one short sentence and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":174,"time":1783962244637,"data":{"turn":3,"step":1}} +{"type":"assistant/chunk","seq":175,"time":1783860674433,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":176,"time":1783860674435,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":177,"time":1783860674435,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":178,"time":1783860674465,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":179,"time":1783860674465,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":180,"time":1783860674499,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":181,"time":1783860674500,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" state"}}} +{"type":"assistant/chunk","seq":182,"time":1783860674500,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":183,"time":1783860674500,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" current"}}} +{"type":"assistant/chunk","seq":184,"time":1783860674500,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" approval"}}} +{"type":"assistant/chunk","seq":185,"time":1783860674525,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" policy"}}} +{"type":"assistant/chunk","seq":186,"time":1783860674526,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" without"}}} +{"type":"assistant/chunk","seq":187,"time":1783860674526,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" using"}}} +{"type":"assistant/chunk","seq":188,"time":1783860674526,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" any"}}} +{"type":"assistant/chunk","seq":189,"time":1783860674526,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tools"}}} +{"type":"assistant/chunk","seq":190,"time":1783860674550,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":191,"time":1783860674550,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" From"}}} +{"type":"assistant/chunk","seq":192,"time":1783860674581,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":193,"time":1783860674582,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" context"}}} +{"type":"assistant/chunk","seq":194,"time":1783860674582,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" provided"}}} +{"type":"assistant/chunk","seq":195,"time":1783860674610,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":196,"time":1783860674611,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":197,"time":1783860674611,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" approval"}}} +{"type":"assistant/chunk","seq":198,"time":1783860674611,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" policy"}}} +{"type":"assistant/chunk","seq":199,"time":1783860674638,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" changed"}}} +{"type":"assistant/chunk","seq":200,"time":1783860674638,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" from"}}} +{"type":"assistant/chunk","seq":201,"time":1783860674638,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":202,"time":1783860674638,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ask"}}} +{"type":"assistant/chunk","seq":203,"time":1783860674638,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":204,"time":1783860674640,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":205,"time":1783860674698,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":206,"time":1783860674698,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"never"}}} +{"type":"assistant/chunk","seq":207,"time":1783860674700,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":208,"time":1783860674726,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":209,"time":1783860674726,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":"My"}}} +{"type":"assistant/chunk","seq":210,"time":1783860674727,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" approval"}}} +{"type":"assistant/chunk","seq":211,"time":1783860674727,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" policy"}}} +{"type":"assistant/chunk","seq":212,"time":1783860674754,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" is"}}} +{"type":"assistant/chunk","seq":213,"time":1783860674757,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" set"}}} +{"type":"assistant/chunk","seq":214,"time":1783860674757,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" to"}}} +{"type":"assistant/chunk","seq":215,"time":1783860674757,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" \""}}} +{"type":"assistant/chunk","seq":216,"time":1783860674757,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":"never"}}} +{"type":"assistant/chunk","seq":217,"time":1783860674786,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":"\""}}} +{"type":"assistant/chunk","seq":218,"time":1783860674817,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" —"}}} +{"type":"assistant/chunk","seq":219,"time":1783860674846,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" no"}}} +{"type":"assistant/chunk","seq":220,"time":1783860674875,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" actions"}}} +{"type":"assistant/chunk","seq":221,"time":1783860674879,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" requiring"}}} +{"type":"assistant/chunk","seq":222,"time":1783860674880,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" approval"}}} +{"type":"assistant/chunk","seq":223,"time":1783860674904,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" are"}}} +{"type":"assistant/chunk","seq":224,"time":1783860674906,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" permitted"}}} +{"type":"assistant/chunk","seq":225,"time":1783860674906,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":226,"time":1783860674939,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" and"}}} +{"type":"assistant/chunk","seq":227,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" they"}}} +{"type":"assistant/chunk","seq":228,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" are"}}} +{"type":"assistant/chunk","seq":229,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" rejected"}}} +{"type":"assistant/chunk","seq":230,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":" automatically"}}} +{"type":"assistant/chunk","seq":231,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"text-delta","index":1,"text":"."}}} +{"type":"assistant/chunk","seq":232,"time":1783860674940,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to state the current approval policy without using any tools. From the context provided, the approval policy changed from \"ask\" to \"never\"."}}}} +{"type":"assistant/chunk","seq":233,"time":1783962244639,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"My approval policy is set to \"never\" — no actions requiring approval are permitted, and they are rejected automatically."}}}} +{"type":"assistant/chunk","seq":234,"time":1783962244639,"data":{"turn":3,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":64,"outputTokens":56,"cacheReadTokens":1664,"reasoningTokens":32}}}} +{"type":"assistant/chunk","seq":235,"time":1783962244639,"data":{"turn":3,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":236,"time":1783962244640,"data":{"turn":3,"step":1,"content":[{"type":"reasoning","text":"The user wants me to state the current approval policy without using any tools. From the context provided, the approval policy changed from \"ask\" to \"never\"."},{"type":"text","text":"My approval policy is set to \"never\" — no actions requiring approval are permitted, and they are rejected automatically."}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":64,"outputTokens":56,"cacheReadTokens":1664,"reasoningTokens":32}},"sourceEventSeqs":[175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235],"surfaceOp":"append"} +{"type":"step/end","seq":237,"time":1783962244640,"data":{"turn":3,"step":1}} +{"type":"turn/end","seq":238,"time":1783962244640,"data":{"turn":3,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl b/examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl index cad0f9cfb4..d5e4d5418b 100644 --- a/examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl +++ b/examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl @@ -1,8 +1,8 @@ {"type":"session","version":0,"id":"50138298-1385-449e-b252-146acb0571d0","createdAt":1784525384931,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-DTr6Ra","delegationDepth":0} {"type":"turn/start","seq":0,"time":1784525384935,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"plan/mode","seq":1,"time":1784525384935,"data":{"active":true}} -{"type":"user/message","seq":2,"time":1784525384935,"data":{"content":[{"type":"text","text":"Read the file notes.txt (use the relative path notes.txt exactly, never an absolute path), then present a short plan titled '# Fix the greeting typo' via exit_plan_mode, exactly once. If the review does not approve, summarize the reviewer's feedback in plain text and end your reply - do not present the plan again."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"session/title","seq":3,"time":1784525384935,"data":{"title":"Read the file notes.txt (use","messageSeqs":[2],"source":{"kind":"fallback"}}} +{"type":"user/message","seq":1,"time":1784903314522,"data":{"content":[{"type":"text","text":"Read the file notes.txt (use the relative path notes.txt exactly, never an absolute path), then present a short plan titled '# Fix the greeting typo' via exit_plan_mode, exactly once. If the review does not approve, summarize the reviewer's feedback in plain text and end your reply - do not present the plan again."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"plan/mode","seq":2,"time":1784903314522,"data":{"active":true}} +{"type":"session/title","seq":3,"time":1784525384935,"data":{"title":"Read the file notes.txt (use","messageSeqs":[1],"source":{"kind":"fallback"}}} {"type":"step/start","seq":4,"time":1784525384938,"data":{"turn":1,"step":1}} {"type":"request/header","seq":5,"time":1784525384938,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"assistant/chunk","seq":6,"time":1784525385351,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} diff --git a/examples/acp-agent/tests/snapshots/plan-mode/session.jsonl b/examples/acp-agent/tests/snapshots/plan-mode/session.jsonl index 71a1189702..a61b8ef0a7 100644 --- a/examples/acp-agent/tests/snapshots/plan-mode/session.jsonl +++ b/examples/acp-agent/tests/snapshots/plan-mode/session.jsonl @@ -1,8 +1,8 @@ {"type":"session","version":0,"id":"5fa52100-2da3-4be1-ac1d-6c2e1fba01c6","createdAt":1784525374896,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-tia05W","delegationDepth":0} {"type":"turn/start","seq":0,"time":1784525374902,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"plan/mode","seq":1,"time":1784525374902,"data":{"active":true}} -{"type":"user/message","seq":2,"time":1784525374902,"data":{"content":[{"type":"text","text":"The typo is on line 2 of notes.txt itself; the workspace contains no other file. Inspect it with the bash tool: run exactly `cat notes.txt` (relative path, no other command). Then present a one-step plan titled '# Fix the greeting typo' via exit_plan_mode: the single step is editing line 2 of notes.txt to say hello world. Do not edit anything before the review approves. After it approves, apply exactly that one edit to notes.txt with the edit tool and stop — no other commands, do not look for any other file."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"session/title","seq":3,"time":1784525374902,"data":{"title":"The typo is on line","messageSeqs":[2],"source":{"kind":"fallback"}}} +{"type":"user/message","seq":1,"time":1784903313084,"data":{"content":[{"type":"text","text":"The typo is on line 2 of notes.txt itself; the workspace contains no other file. Inspect it with the bash tool: run exactly `cat notes.txt` (relative path, no other command). Then present a one-step plan titled '# Fix the greeting typo' via exit_plan_mode: the single step is editing line 2 of notes.txt to say hello world. Do not edit anything before the review approves. After it approves, apply exactly that one edit to notes.txt with the edit tool and stop — no other commands, do not look for any other file."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"plan/mode","seq":2,"time":1784903313085,"data":{"active":true}} +{"type":"session/title","seq":3,"time":1784525374902,"data":{"title":"The typo is on line","messageSeqs":[1],"source":{"kind":"fallback"}}} {"type":"step/start","seq":4,"time":1784525374904,"data":{"turn":1,"step":1}} {"type":"request/header","seq":5,"time":1784525374905,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"assistant/chunk","seq":6,"time":1784525375472,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} @@ -324,7 +324,7 @@ {"type":"assistant/chunk","seq":322,"time":1784525379646,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":323,"time":1784525379647,"data":{"turn":1,"step":3,"content":[{"type":"reasoning","text":"The plan is approved. Now I need to apply exactly that one edit: change line 2 of notes.txt to say \"hello world\". I'll use the edit tool to replace the current line 2 content."},{"type":"tool-call","id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","arguments":"{\"file_path\": \"notes.txt\", \"old_string\": \"- the greeting message still says \\\"helo wrld\\\"\", \"new_string\": \"hello world\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3269,"outputTokens":134,"cacheReadTokens":0,"reasoningTokens":43}},"sourceEventSeqs":[232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322],"surfaceOp":"append"} {"type":"tool/call","seq":324,"time":1784525379647,"data":{"turn":1,"step":3,"callId":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","arguments":"{\"file_path\": \"notes.txt\", \"old_string\": \"- the greeting message still says \\\"helo wrld\\\"\", \"new_string\": \"hello world\"}"}} -{"type":"tool/result","seq":325,"time":1784525379652,"data":{"turn":1,"step":3,"callId":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","content":[{"type":"text","text":"Error: edit requires reading \"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-tia05W/notes.txt\" first"}],"isError":true,"error":{"name":"FsError","code":"FS_NOT_OBSERVED"}},"sourceEventSeqs":[324],"surfaceOp":"append"} +{"type":"tool/result","seq":325,"time":1784525379652,"data":{"turn":1,"step":3,"callId":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","content":[{"type":"text","text":"Error: edit requires reading \"/private/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-tia05W/notes.txt\" first"}],"isError":true,"error":{"name":"FsError","code":"FS_NOT_OBSERVED"}},"sourceEventSeqs":[324],"surfaceOp":"append"} {"type":"step/end","seq":326,"time":1784525379652,"data":{"turn":1,"step":3}} {"type":"step/start","seq":327,"time":1784525379655,"data":{"turn":1,"step":4}} {"type":"assistant/chunk","seq":328,"time":1784525380205,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} @@ -428,7 +428,7 @@ {"type":"assistant/chunk","seq":426,"time":1784525381745,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":427,"time":1784525381746,"data":{"turn":1,"step":5,"content":[{"type":"reasoning","text":"Now I can apply the edit."},{"type":"tool-call","id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","arguments":"{\"file_path\": \"notes.txt\", \"old_string\": \"- the greeting message still says \\\"helo wrld\\\"\", \"new_string\": \"hello world\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":169,"outputTokens":98,"cacheReadTokens":3456,"reasoningTokens":7}},"sourceEventSeqs":[372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426],"surfaceOp":"append"} {"type":"tool/call","seq":428,"time":1784525381746,"data":{"turn":1,"step":5,"callId":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","arguments":"{\"file_path\": \"notes.txt\", \"old_string\": \"- the greeting message still says \\\"helo wrld\\\"\", \"new_string\": \"hello world\"}"}} -{"type":"tool/result","seq":429,"time":1784525381766,"data":{"turn":1,"step":5,"callId":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","content":[{"type":"text","text":"The file /var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-tia05W/notes.txt has been updated successfully."}],"isError":false,"meta":{"diffs":[{"path":"notes.txt","oldText":"project notes\n- the greeting message still says \"helo wrld\"","newText":"project notes\nhello world"}]}},"sourceEventSeqs":[428],"surfaceOp":"append"} +{"type":"tool/result","seq":429,"time":1784525381766,"data":{"turn":1,"step":5,"callId":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","content":[{"type":"text","text":"The file /private/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-tia05W/notes.txt has been updated successfully."}],"isError":false,"meta":{"diffs":[{"path":"notes.txt","oldText":"project notes\n- the greeting message still says \"helo wrld\"","newText":"project notes\nhello world"}]}},"sourceEventSeqs":[428],"surfaceOp":"append"} {"type":"step/end","seq":430,"time":1784525381766,"data":{"turn":1,"step":5}} {"type":"step/start","seq":431,"time":1784525381768,"data":{"turn":1,"step":6}} {"type":"assistant/chunk","seq":432,"time":1784525382334,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} diff --git a/examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl b/examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl index e92adbafb9..63816266b9 100644 --- a/examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl +++ b/examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl @@ -1,27 +1,28 @@ {"type":"session","version":0,"id":"00000000-0000-0000-0000-000000000000","createdAt":0,"cwd":"/Users/cty/acp-snap-cwd-MABAjO","delegationDepth":0} {"type":"turn/start","seq":0,"time":1784567324138,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"permission/preset","seq":1,"time":1784567324138,"data":{"preset":"workspace-write"}} -{"type":"sandbox/mode","seq":2,"time":1784567324138,"data":{"mode":"workspace-write"}} -{"type":"approval/policy","seq":3,"time":1784567324138,"data":{"policy":"ask"}} -{"type":"user/message","seq":4,"time":1784567324138,"data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create session-root.txt in the current directory containing exactly: session root. Then reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"session/title","seq":5,"time":1784567324138,"data":{"title":"Use the write tool (NOT","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":6,"time":1784567324142,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":7,"time":1784567324142,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"assistant/chunk","seq":8,"time":1784567324143,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"user/message","seq":1,"time":1784903378009,"data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create session-root.txt in the current directory containing exactly: session root. Then reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"session/title","seq":2,"time":1784903378009,"data":{"title":"Use the write tool (NOT","messageSeqs":[1],"source":{"kind":"fallback"}}} +{"type":"permission/preset","seq":3,"time":1784903378027,"data":{"preset":"workspace-write"}} +{"type":"sandbox/mode","seq":4,"time":1784903378027,"data":{"mode":"workspace-write"}} +{"type":"approval/policy","seq":5,"time":1784903378027,"data":{"policy":"ask"}} +{"type":"step/start","seq":6,"time":1784903378028,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":7,"time":1784903378028,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":8,"time":1784903378035,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":9,"time":1784567324143,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_session_root","name":"write","argumentsDelta":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}}} {"type":"assistant/chunk","seq":10,"time":1784567324143,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}}}} {"type":"assistant/chunk","seq":11,"time":1784567324143,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} {"type":"assistant/chunk","seq":12,"time":1784567324143,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":13,"time":1784567324144,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"} -{"type":"tool/call","seq":14,"time":1784567324145,"data":{"turn":1,"step":1,"callId":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}} -{"type":"tool/result","seq":15,"time":1784567324155,"data":{"turn":1,"step":1,"callId":"call_session_root","content":[{"type":"text","text":"/Users/cty/acp-snap-cwd-MABAjO/session-root.txt\nfile\n\nCreated file\n"}],"isError":false,"meta":{"diffs":[]}},"sourceEventSeqs":[14],"surfaceOp":"append"} -{"type":"step/end","seq":16,"time":1784567324157,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":17,"time":1784567324157,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":18,"time":1784567324157,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} -{"type":"assistant/chunk","seq":19,"time":1784567324157,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}} -{"type":"assistant/chunk","seq":20,"time":1784567324158,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}} -{"type":"assistant/chunk","seq":21,"time":1784567324158,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}} -{"type":"assistant/chunk","seq":22,"time":1784567324158,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":23,"time":1784567324158,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"} -{"type":"step/end","seq":24,"time":1784567324158,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":25,"time":1784567324158,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"assistant/message","seq":13,"time":1784903378036,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"} +{"type":"tool/call","seq":14,"time":1784903378036,"data":{"turn":1,"step":1,"callId":"call_session_root","name":"write","arguments":"{\"file_path\":\"session-root.txt\",\"content\":\"session root\"}"}} +{"type":"tool/result","seq":15,"time":1784903378050,"data":{"turn":1,"step":1,"callId":"call_session_root","content":[{"type":"text","text":"/Users/cty/acp-snap-cwd-MABAjO/session-root.txt\nfile\n\nCreated file\n"}],"isError":false,"meta":{"diffs":[]}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"step/end","seq":16,"time":1784903378050,"data":{"turn":1,"step":1}} +{"type":"user/message","seq":17,"time":1784903378057,"data":{"content":[{"type":"text","text":"The approval policy changed from \"never\" to \"ask\" (changed by the operator/config)."}],"source":{"kind":"plugin","plugin":"user-approval"}},"surfaceOp":"append"} +{"type":"step/start","seq":18,"time":1784567324157,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":19,"time":1784567324157,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":20,"time":1784567324157,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}} +{"type":"assistant/chunk","seq":21,"time":1784567324158,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":22,"time":1784567324158,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}} +{"type":"assistant/chunk","seq":23,"time":1784567324158,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":24,"time":1784567324158,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"} +{"type":"step/end","seq":25,"time":1784567324158,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":26,"time":1784567324158,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl index 0881d11f46..36f5202825 100644 --- a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl +++ b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl @@ -2,29 +2,30 @@ {"type":"turn/start","seq":0,"time":1783654655602,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783654655603,"data":{"content":[{"type":"text","text":"Load the snapshot-skill skill with the skill tool, then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"session/title","seq":2,"time":1783654655603,"data":{"title":"Load the snapshot-skill skill with","messageSeqs":[1],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":3,"time":1783654655608,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":4,"time":1783654655608,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":[{"role":"user","content":[{"type":"text","text":"\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n\n- `snapshot-skill`: Exercise project skill discovery and loading in snapshot tests.\n\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\n"}]}]},"reason":"initial"}} -{"type":"assistant/chunk","seq":5,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":6,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Load the requested skill."}}} -{"type":"assistant/chunk","seq":7,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":8,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_skill_load","name":"skill","argumentsDelta":"{\"name\":\"snapshot-skill\"}"}}} -{"type":"assistant/chunk","seq":9,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Load the requested skill."}}}} -{"type":"assistant/chunk","seq":10,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"snapshot-skill\"}"}}}} -{"type":"assistant/chunk","seq":11,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}}}} -{"type":"assistant/chunk","seq":12,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":13,"time":1783654655609,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"Load the requested skill."},{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"snapshot-skill\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}},"sourceEventSeqs":[5,6,7,8,9,10,11,12],"surfaceOp":"append"} -{"type":"tool/call","seq":14,"time":1783654655609,"data":{"turn":1,"step":1,"callId":"call_skill_load","name":"skill","arguments":"{\"name\":\"snapshot-skill\"}"}} -{"type":"tool/result","seq":15,"time":1783654655610,"data":{"turn":1,"step":1,"callId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: /var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-DhYwNW/.dsh/skills/snapshot-skill\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\nFollow these snapshot-only instructions.\nResolve referenced resources relative to this skill directory.\n\n"}],"isError":false},"sourceEventSeqs":[14],"surfaceOp":"append"} -{"type":"step/end","seq":16,"time":1783654655610,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":17,"time":1783654655610,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":18,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":19,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The skill is loaded."}}} -{"type":"assistant/chunk","seq":20,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":21,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"DONE"}}} -{"type":"assistant/chunk","seq":22,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The skill is loaded."}}}} -{"type":"assistant/chunk","seq":23,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} -{"type":"assistant/chunk","seq":24,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":180,"outputTokens":10,"cacheReadTokens":0,"reasoningTokens":4}}}} -{"type":"assistant/chunk","seq":25,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":26,"time":1783654655611,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The skill is loaded."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":180,"outputTokens":10,"cacheReadTokens":0,"reasoningTokens":4}},"sourceEventSeqs":[18,19,20,21,22,23,24,25],"surfaceOp":"append"} -{"type":"step/end","seq":27,"time":1783654655611,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":28,"time":1783654655611,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"user/message","seq":3,"time":1784903324926,"data":{"content":[{"type":"text","text":"\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n\n- `snapshot-skill`: Exercise project skill discovery and loading in snapshot tests.\n\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\n"}],"source":{"kind":"plugin","plugin":"dsh-tool-skill"}},"surfaceOp":"append"} +{"type":"step/start","seq":4,"time":1784903324927,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":5,"time":1784903324928,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":6,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":7,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Load the requested skill."}}} +{"type":"assistant/chunk","seq":8,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":9,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_skill_load","name":"skill","argumentsDelta":"{\"name\":\"snapshot-skill\"}"}}} +{"type":"assistant/chunk","seq":10,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Load the requested skill."}}}} +{"type":"assistant/chunk","seq":11,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"snapshot-skill\"}"}}}} +{"type":"assistant/chunk","seq":12,"time":1783654655609,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}}}} +{"type":"assistant/chunk","seq":13,"time":1784903324935,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":14,"time":1784903324935,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"Load the requested skill."},{"type":"tool-call","id":"call_skill_load","name":"skill","arguments":"{\"name\":\"snapshot-skill\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":100,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":5}},"sourceEventSeqs":[6,7,8,9,10,11,12,13],"surfaceOp":"append"} +{"type":"tool/call","seq":15,"time":1784903324936,"data":{"turn":1,"step":1,"callId":"call_skill_load","name":"skill","arguments":"{\"name\":\"snapshot-skill\"}"}} +{"type":"tool/result","seq":16,"time":1784903324944,"data":{"turn":1,"step":1,"callId":"call_skill_load","content":[{"type":"text","text":"\n\nBase directory for this skill: /var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-DhYwNW/.dsh/skills/snapshot-skill\nResolve relative paths mentioned by this skill against the base directory before using them. Load referenced resources only as needed.\n\n\n\nFollow these snapshot-only instructions.\nResolve referenced resources relative to this skill directory.\n\n"}],"isError":false},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"step/end","seq":17,"time":1784903324944,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":18,"time":1784903324952,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":19,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":20,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The skill is loaded."}}} +{"type":"assistant/chunk","seq":21,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":22,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"DONE"}}} +{"type":"assistant/chunk","seq":23,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The skill is loaded."}}}} +{"type":"assistant/chunk","seq":24,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":25,"time":1783654655611,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":180,"outputTokens":10,"cacheReadTokens":0,"reasoningTokens":4}}}} +{"type":"assistant/chunk","seq":26,"time":1784903324956,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":27,"time":1784903324956,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The skill is loaded."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":180,"outputTokens":10,"cacheReadTokens":0,"reasoningTokens":4}},"sourceEventSeqs":[19,20,21,22,23,24,25,26],"surfaceOp":"append"} +{"type":"step/end","seq":28,"time":1784903324956,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":29,"time":1784903324956,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl index f0af629997..d7a85fd022 100644 --- a/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl @@ -40,51 +40,50 @@ {"type":"turn/start","seq":38,"time":1783352137162,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":39,"time":1783352137163,"data":{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":40,"time":1783352137163,"data":{"turn":2,"step":1}} -{"type":"request/header","seq":41,"time":1783352137163,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"resume"}} -{"type":"assistant/chunk","seq":42,"time":1783352137783,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":43,"time":1783352137783,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":44,"time":1783352137961,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":45,"time":1783352137989,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} -{"type":"assistant/chunk","seq":46,"time":1783352138020,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":47,"time":1783352138046,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":48,"time":1783352138046,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" remember"}}} -{"type":"assistant/chunk","seq":49,"time":1783352138046,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":50,"time":1783352138046,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" project"}}} -{"type":"assistant/chunk","seq":51,"time":1783352138074,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" cod"}}} -{"type":"assistant/chunk","seq":52,"time":1783352138075,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ew"}}} -{"type":"assistant/chunk","seq":53,"time":1783352138075,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ord"}}} -{"type":"assistant/chunk","seq":54,"time":1783352138075,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":55,"time":1783352138075,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"M"}}} -{"type":"assistant/chunk","seq":56,"time":1783352138075,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ARM"}}} -{"type":"assistant/chunk","seq":57,"time":1783352138103,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"AL"}}} -{"type":"assistant/chunk","seq":58,"time":1783352138103,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ADE"}}} -{"type":"assistant/chunk","seq":59,"time":1783352138103,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} -{"type":"assistant/chunk","seq":60,"time":1783352138103,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":61,"time":1783352138103,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" now"}}} -{"type":"assistant/chunk","seq":62,"time":1783352138131,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" they"}}} -{"type":"assistant/chunk","seq":63,"time":1783352138159,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"'re"}}} -{"type":"assistant/chunk","seq":64,"time":1783352138160,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asking"}}} -{"type":"assistant/chunk","seq":65,"time":1783352138160,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" what"}}} -{"type":"assistant/chunk","seq":66,"time":1783352138160,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} -{"type":"assistant/chunk","seq":67,"time":1783352138188,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} -{"type":"assistant/chunk","seq":68,"time":1783352138188,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":69,"time":1783352138188,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} -{"type":"assistant/chunk","seq":70,"time":1783352138217,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" should"}}} -{"type":"assistant/chunk","seq":71,"time":1783352138217,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} -{"type":"assistant/chunk","seq":72,"time":1783352138217,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":73,"time":1783352138245,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":74,"time":1783352138246,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} -{"type":"assistant/chunk","seq":75,"time":1783352138274,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} -{"type":"assistant/chunk","seq":76,"time":1783352138275,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":77,"time":1783352138275,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":78,"time":1783352138275,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"M"}}} -{"type":"assistant/chunk","seq":79,"time":1783352138275,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ARM"}}} -{"type":"assistant/chunk","seq":80,"time":1783352138275,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"AL"}}} -{"type":"assistant/chunk","seq":81,"time":1783352138305,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ADE"}}} -{"type":"assistant/chunk","seq":82,"time":1783352138307,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked me to remember the project codeword \"MARMALADE\" and now they're asking what it is. I should just reply with that word."}}}} -{"type":"assistant/chunk","seq":83,"time":1783352138307,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"MARMALADE"}}}} -{"type":"assistant/chunk","seq":84,"time":1783352138307,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":97,"outputTokens":39,"cacheReadTokens":2816,"reasoningTokens":34}}}} -{"type":"assistant/chunk","seq":85,"time":1783352138307,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":86,"time":1783352138308,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"The user asked me to remember the project codeword \"MARMALADE\" and now they're asking what it is. I should just reply with that word."},{"type":"text","text":"MARMALADE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":97,"outputTokens":39,"cacheReadTokens":2816,"reasoningTokens":34}},"sourceEventSeqs":[42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85],"surfaceOp":"append"} -{"type":"step/end","seq":87,"time":1783352138308,"data":{"turn":2,"step":1}} -{"type":"turn/end","seq":88,"time":1783352138308,"data":{"turn":2,"reason":{"kind":"completed"}}} +{"type":"assistant/chunk","seq":41,"time":1784903345813,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":42,"time":1783352137783,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":43,"time":1783352137783,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":44,"time":1783352137961,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} +{"type":"assistant/chunk","seq":45,"time":1783352137989,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":46,"time":1783352138020,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":47,"time":1783352138046,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" remember"}}} +{"type":"assistant/chunk","seq":48,"time":1783352138046,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":49,"time":1783352138046,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" project"}}} +{"type":"assistant/chunk","seq":50,"time":1783352138046,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" cod"}}} +{"type":"assistant/chunk","seq":51,"time":1783352138074,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ew"}}} +{"type":"assistant/chunk","seq":52,"time":1783352138075,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ord"}}} +{"type":"assistant/chunk","seq":53,"time":1783352138075,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":54,"time":1783352138075,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"M"}}} +{"type":"assistant/chunk","seq":55,"time":1783352138075,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ARM"}}} +{"type":"assistant/chunk","seq":56,"time":1783352138075,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"AL"}}} +{"type":"assistant/chunk","seq":57,"time":1783352138103,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ADE"}}} +{"type":"assistant/chunk","seq":58,"time":1783352138103,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":59,"time":1783352138103,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":60,"time":1783352138103,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" now"}}} +{"type":"assistant/chunk","seq":61,"time":1783352138103,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" they"}}} +{"type":"assistant/chunk","seq":62,"time":1783352138131,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"'re"}}} +{"type":"assistant/chunk","seq":63,"time":1783352138159,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asking"}}} +{"type":"assistant/chunk","seq":64,"time":1783352138160,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" what"}}} +{"type":"assistant/chunk","seq":65,"time":1783352138160,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":66,"time":1783352138160,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} +{"type":"assistant/chunk","seq":67,"time":1783352138188,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":68,"time":1783352138188,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":69,"time":1783352138188,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" should"}}} +{"type":"assistant/chunk","seq":70,"time":1783352138217,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} +{"type":"assistant/chunk","seq":71,"time":1783352138217,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":72,"time":1783352138217,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":73,"time":1783352138245,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":74,"time":1783352138246,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":75,"time":1783352138274,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":76,"time":1783352138275,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":77,"time":1783352138275,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"M"}}} +{"type":"assistant/chunk","seq":78,"time":1783352138275,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ARM"}}} +{"type":"assistant/chunk","seq":79,"time":1783352138275,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"AL"}}} +{"type":"assistant/chunk","seq":80,"time":1783352138275,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ADE"}}} +{"type":"assistant/chunk","seq":81,"time":1783352138305,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user asked me to remember the project codeword \"MARMALADE\" and now they're asking what it is. I should just reply with that word."}}}} +{"type":"assistant/chunk","seq":82,"time":1783352138307,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"MARMALADE"}}}} +{"type":"assistant/chunk","seq":83,"time":1783352138307,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":97,"outputTokens":39,"cacheReadTokens":2816,"reasoningTokens":34}}}} +{"type":"assistant/chunk","seq":84,"time":1783352138307,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":85,"time":1784903345814,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"The user asked me to remember the project codeword \"MARMALADE\" and now they're asking what it is. I should just reply with that word."},{"type":"text","text":"MARMALADE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":97,"outputTokens":39,"cacheReadTokens":2816,"reasoningTokens":34}},"sourceEventSeqs":[41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84],"surfaceOp":"append"} +{"type":"step/end","seq":86,"time":1784903345814,"data":{"turn":2,"step":1}} +{"type":"turn/end","seq":87,"time":1784903345814,"data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl index 9b017173fe..fe2b9c1d1f 100644 --- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl @@ -34,47 +34,46 @@ {"type":"turn/start","seq":32,"time":1783352147508,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":33,"time":1783352147509,"data":{"content":[{"type":"text","text":"What is the project codeword mentioned earlier in this conversation? Reply with exactly that one word and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":34,"time":1783352147509,"data":{"turn":2,"step":1}} -{"type":"request/header","seq":35,"time":1783352147509,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"resume"}} -{"type":"assistant/chunk","seq":36,"time":1783352147925,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":37,"time":1783352147925,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":38,"time":1783352148019,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":39,"time":1783352148048,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} -{"type":"assistant/chunk","seq":40,"time":1783352148049,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asking"}}} -{"type":"assistant/chunk","seq":41,"time":1783352148049,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":42,"time":1783352148076,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":43,"time":1783352148076,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" recall"}}} -{"type":"assistant/chunk","seq":44,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":45,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" project"}}} -{"type":"assistant/chunk","seq":46,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" cod"}}} -{"type":"assistant/chunk","seq":47,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ew"}}} -{"type":"assistant/chunk","seq":48,"time":1783352148106,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ord"}}} -{"type":"assistant/chunk","seq":49,"time":1783352148106,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} -{"type":"assistant/chunk","seq":50,"time":1783352148106,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" was"}}} -{"type":"assistant/chunk","seq":51,"time":1783352148106,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" mentioned"}}} -{"type":"assistant/chunk","seq":52,"time":1783352148141,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" earlier"}}} -{"type":"assistant/chunk","seq":53,"time":1783352148141,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} -{"type":"assistant/chunk","seq":54,"time":1783352148141,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":55,"time":1783352148141,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" conversation"}}} -{"type":"assistant/chunk","seq":56,"time":1783352148141,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":57,"time":1783352148167,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} -{"type":"assistant/chunk","seq":58,"time":1783352148196,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" was"}}} -{"type":"assistant/chunk","seq":59,"time":1783352148227,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" told"}}} -{"type":"assistant/chunk","seq":60,"time":1783352148227,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":61,"time":1783352148257,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" remember"}}} -{"type":"assistant/chunk","seq":62,"time":1783352148257,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} -{"type":"assistant/chunk","seq":63,"time":1783352148257,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} -{"type":"assistant/chunk","seq":64,"time":1783352148284,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" SA"}}} -{"type":"assistant/chunk","seq":65,"time":1783352148285,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"FF"}}} -{"type":"assistant/chunk","seq":66,"time":1783352148312,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"RON"}}} -{"type":"assistant/chunk","seq":67,"time":1783352148312,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":68,"time":1783352148313,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":69,"time":1783352148313,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"SA"}}} -{"type":"assistant/chunk","seq":70,"time":1783352148313,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"FF"}}} -{"type":"assistant/chunk","seq":71,"time":1783352148344,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"RON"}}} -{"type":"assistant/chunk","seq":72,"time":1783352148345,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user is asking me to recall the project codeword that was mentioned earlier in the conversation. I was told to remember it: SAFFRON."}}}} -{"type":"assistant/chunk","seq":73,"time":1783352148345,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SAFFRON"}}}} -{"type":"assistant/chunk","seq":74,"time":1783352148345,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":95,"outputTokens":35,"cacheReadTokens":2816,"reasoningTokens":31}}}} -{"type":"assistant/chunk","seq":75,"time":1783352148345,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":76,"time":1783352148345,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"The user is asking me to recall the project codeword that was mentioned earlier in the conversation. I was told to remember it: SAFFRON."},{"type":"text","text":"SAFFRON"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":95,"outputTokens":35,"cacheReadTokens":2816,"reasoningTokens":31}},"sourceEventSeqs":[36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75],"surfaceOp":"append"} -{"type":"step/end","seq":77,"time":1783352148345,"data":{"turn":2,"step":1}} -{"type":"turn/end","seq":78,"time":1783352148345,"data":{"turn":2,"reason":{"kind":"completed"}}} +{"type":"assistant/chunk","seq":35,"time":1784903347067,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":36,"time":1783352147925,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":37,"time":1783352147925,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":38,"time":1783352148019,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} +{"type":"assistant/chunk","seq":39,"time":1783352148048,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" asking"}}} +{"type":"assistant/chunk","seq":40,"time":1783352148049,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":41,"time":1783352148049,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":42,"time":1783352148076,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" recall"}}} +{"type":"assistant/chunk","seq":43,"time":1783352148076,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":44,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" project"}}} +{"type":"assistant/chunk","seq":45,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" cod"}}} +{"type":"assistant/chunk","seq":46,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ew"}}} +{"type":"assistant/chunk","seq":47,"time":1783352148077,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ord"}}} +{"type":"assistant/chunk","seq":48,"time":1783352148106,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":49,"time":1783352148106,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" was"}}} +{"type":"assistant/chunk","seq":50,"time":1783352148106,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" mentioned"}}} +{"type":"assistant/chunk","seq":51,"time":1783352148106,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" earlier"}}} +{"type":"assistant/chunk","seq":52,"time":1783352148141,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} +{"type":"assistant/chunk","seq":53,"time":1783352148141,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":54,"time":1783352148141,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" conversation"}}} +{"type":"assistant/chunk","seq":55,"time":1783352148141,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":56,"time":1783352148141,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":57,"time":1783352148167,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" was"}}} +{"type":"assistant/chunk","seq":58,"time":1783352148196,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" told"}}} +{"type":"assistant/chunk","seq":59,"time":1783352148227,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":60,"time":1783352148227,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" remember"}}} +{"type":"assistant/chunk","seq":61,"time":1783352148257,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":62,"time":1783352148257,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} +{"type":"assistant/chunk","seq":63,"time":1783352148257,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" SA"}}} +{"type":"assistant/chunk","seq":64,"time":1783352148284,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"FF"}}} +{"type":"assistant/chunk","seq":65,"time":1783352148285,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"RON"}}} +{"type":"assistant/chunk","seq":66,"time":1783352148312,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":67,"time":1783352148312,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":68,"time":1783352148313,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"SA"}}} +{"type":"assistant/chunk","seq":69,"time":1783352148313,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"FF"}}} +{"type":"assistant/chunk","seq":70,"time":1783352148313,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":1,"text":"RON"}}} +{"type":"assistant/chunk","seq":71,"time":1783352148344,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user is asking me to recall the project codeword that was mentioned earlier in the conversation. I was told to remember it: SAFFRON."}}}} +{"type":"assistant/chunk","seq":72,"time":1783352148345,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SAFFRON"}}}} +{"type":"assistant/chunk","seq":73,"time":1783352148345,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":95,"outputTokens":35,"cacheReadTokens":2816,"reasoningTokens":31}}}} +{"type":"assistant/chunk","seq":74,"time":1783352148345,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":75,"time":1784903347068,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"The user is asking me to recall the project codeword that was mentioned earlier in the conversation. I was told to remember it: SAFFRON."},{"type":"text","text":"SAFFRON"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":95,"outputTokens":35,"cacheReadTokens":2816,"reasoningTokens":31}},"sourceEventSeqs":[35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74],"surfaceOp":"append"} +{"type":"step/end","seq":76,"time":1784903347068,"data":{"turn":2,"step":1}} +{"type":"turn/end","seq":77,"time":1784903347069,"data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl b/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl index 883e685a6d..7158b3a812 100644 --- a/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl @@ -2,24 +2,25 @@ {"type":"turn/start","seq":0,"time":1783778297065,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783778297066,"data":{"content":[{"type":"text","text":"Read nested/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"session/title","seq":2,"time":1783778297066,"data":{"title":"Read nested/task.txt with the read","messageSeqs":[1],"source":{"kind":"fallback"}}} -{"type":"step/start","seq":3,"time":1783778297069,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":4,"time":1783778297070,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":[{"role":"user","content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n"}]}]},"reason":"initial"}} -{"type":"assistant/chunk","seq":5,"time":1783778297070,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":6,"time":1783778297070,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_workspace_read","name":"read","argumentsDelta":"{\"file_path\":\"nested/task.txt\"}"}}} -{"type":"assistant/chunk","seq":7,"time":1783778297070,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}}}} -{"type":"assistant/chunk","seq":8,"time":1783778297070,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} -{"type":"assistant/chunk","seq":9,"time":1783778297070,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":10,"time":1783778297070,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"} -{"type":"tool/call","seq":11,"time":1783778297070,"data":{"turn":1,"step":1,"callId":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}} -{"type":"tool/result","seq":12,"time":1783778297072,"data":{"turn":1,"step":1,"callId":"call_workspace_read","content":[{"type":"text","text":"{{cwd}}/nested/task.txt\nfile\n\n1: snapshot task\n\n(End of file - total 1 lines)\n"}],"isError":false},"sourceEventSeqs":[11],"surfaceOp":"append"} -{"type":"user/message","seq":13,"time":1783778297072,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"plugin","plugin":"workspace-context"},"meta":{"kind":"workspace-instructions","version":1,"changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]}},"surfaceOp":"append"} -{"type":"step/end","seq":14,"time":1783778297072,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":15,"time":1783778297072,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":16,"time":1783778297073,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} -{"type":"assistant/chunk","seq":17,"time":1783778297073,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}} -{"type":"assistant/chunk","seq":18,"time":1783778297073,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}} -{"type":"assistant/chunk","seq":19,"time":1783778297073,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}} -{"type":"assistant/chunk","seq":20,"time":1783778297073,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":21,"time":1783778297073,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[16,17,18,19,20],"surfaceOp":"append"} -{"type":"step/end","seq":22,"time":1783778297073,"data":{"turn":1,"step":2}} -{"type":"turn/end","seq":23,"time":1783778297073,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"user/message","seq":3,"time":1784903339799,"data":{"content":[{"type":"text","text":"\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n"}],"source":{"kind":"plugin","plugin":"workspace-context"}},"surfaceOp":"append"} +{"type":"step/start","seq":4,"time":1784903339799,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":5,"time":1784903339800,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":6,"time":1783778297070,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":7,"time":1783778297070,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_workspace_read","name":"read","argumentsDelta":"{\"file_path\":\"nested/task.txt\"}"}}} +{"type":"assistant/chunk","seq":8,"time":1783778297070,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}}}} +{"type":"assistant/chunk","seq":9,"time":1783778297070,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} +{"type":"assistant/chunk","seq":10,"time":1784903339801,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":11,"time":1784903339801,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[6,7,8,9,10],"surfaceOp":"append"} +{"type":"tool/call","seq":12,"time":1784903339802,"data":{"turn":1,"step":1,"callId":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}} +{"type":"tool/result","seq":13,"time":1784903339813,"data":{"turn":1,"step":1,"callId":"call_workspace_read","content":[{"type":"text","text":"{{cwd}}/nested/task.txt\nfile\n\n1: snapshot task\n\n(End of file - total 1 lines)\n"}],"isError":false},"sourceEventSeqs":[12],"surfaceOp":"append"} +{"type":"user/message","seq":14,"time":1784903339813,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"workspace-instructions","changes":[{"action":"set","scope":"nested\u0000AGENTS.md","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]}},"surfaceOp":"append"} +{"type":"step/end","seq":15,"time":1784903339813,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":16,"time":1784903339820,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":17,"time":1783778297073,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":18,"time":1783778297073,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DONE"}}} +{"type":"assistant/chunk","seq":19,"time":1783778297073,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":20,"time":1783778297073,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}} +{"type":"assistant/chunk","seq":21,"time":1784903339821,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":22,"time":1784903339821,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[17,18,19,20,21],"surfaceOp":"append"} +{"type":"step/end","seq":23,"time":1784903339821,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":24,"time":1784903339822,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/cordis-agent/tests/cordis-tools.e2e.ts b/examples/cordis-agent/tests/cordis-tools.e2e.ts index b5f8e27564..9d6b409433 100644 --- a/examples/cordis-agent/tests/cordis-tools.e2e.ts +++ b/examples/cordis-agent/tests/cordis-tools.e2e.ts @@ -42,11 +42,11 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif const log = vi.spyOn(console, 'log').mockImplementation(() => {}) const agent = ctx.agentLoop.create(SessionId('cordis-e2e-listener'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) - agent.followup([{ + agent.followup({ content: [{ type: 'text', text: 'Use cordis_mount to mount a plugin that listens to the \'agent/status\' ' + 'cordis event and logs every change with console.log. Reply "mounted" once done.', - }]) + }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // The WORLD check: the turn's own running→idle transition must have driven @@ -58,7 +58,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif }) expect(resultText(mid)).toContain('dyn-') - agent.followup([{ type: 'text', text: 'Now unmount the plugin you just mounted.' }]) + agent.followup({ content: [{ type: 'text', text: 'Now unmount the plugin you just mounted.' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const after = await ctx.tools.execute({ @@ -72,14 +72,14 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif ctx = await cordisHarness() const agent = ctx.agentLoop.create(SessionId('cordis-e2e-selftool'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) - agent.followup([{ + agent.followup({ content: [{ type: 'text', text: 'Give yourself a new tool: use cordis_mount to mount a plugin with ' + 'inject ["tools"] that calls harness.registerTool(ctx, harness.defineTool({...})) ' + 'to register a tool named reverse_text with one required string parameter ' + '"text", returning the text reversed. Then CALL reverse_text with the ' + 'exact text "harness" and report its exact output.', - }]) + }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // World checks: the tool exists in the registry, was invoked as a real tool call, and its @@ -119,7 +119,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif ctx = await cordisHarness() const agent = ctx.agentLoop.create(SessionId('cordis-e2e-compose'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) - agent.followup([{ + agent.followup({ content: [{ type: 'text', text: 'Mount TWO separate plugins with cordis_mount. First a provider: apply calls ' + 'ctx.provide(\'shouter\', { shout: (s) => s.toUpperCase() }). Second a consumer with ' @@ -127,7 +127,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif + 'a tool named shout_text with one required string parameter "text" whose execute returns ' + 'ctx.shouter.shout(args.text) as a text content block. Then CALL shout_text with "quiet" ' + 'and report the exact output.', - }]) + }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // World checks: the service is really in the store, the tool really ran. @@ -144,7 +144,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('cordis tools: a real model modif .flatMap(event => event.data.content.filter(block => block.type === 'text').map(block => block.text)) expect(shoutResults.some(text => text.includes('QUIET'))).toBe(true) - agent.followup([{ type: 'text', text: 'Now unmount ONLY the provider plugin (the one that provided shouter).' }]) + agent.followup({ content: [{ type: 'text', text: 'Now unmount ONLY the provider plugin (the one that provided shouter).' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // The consumer must have been parked by cordis itself: service gone, diff --git a/examples/headless-agent/tests/code-mode.e2e.ts b/examples/headless-agent/tests/code-mode.e2e.ts index 845def7a46..6997b9e02e 100644 --- a/examples/headless-agent/tests/code-mode.e2e.ts +++ b/examples/headless-agent/tests/code-mode.e2e.ts @@ -311,12 +311,12 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('Code Mode: real model writes a p ctx = await codeModeHarness(workdir) const agent = ctx.agentLoop.create(SessionId('e2e-code-mode'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) - agent.followup([{ + agent.followup({ content: [{ type: 'text', text: 'Using one run_code program: run `echo alpha-7` with the bash tool, run `echo beta-9` with the bash tool, ' + 'then write both outputs joined by a plus sign into combined.txt (bash heredoc or redirect), ' + 'and return only the joined string.', - }]) + }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const events: SessionEvent[] = [...agent.session.events] @@ -363,10 +363,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('Code Mode: real model writes a p agentOptions: { provider: 'deepseek', model: 'deepseek-v4-flash' }, }) - handle.agent.followup([{ + handle.agent.followup({ content: [{ type: 'text', text: 'Use one run_code program to call tools.read on pkg/deep/task.txt. After it finishes, answer: Code Mode workspace handshake?', - }]) + }], source: { kind: 'user' } }) await waitForIdle(ctx, handle.agent) const events: SessionEvent[] = [...handle.agent.session.events] diff --git a/examples/headless-agent/tests/coding-task.e2e.ts b/examples/headless-agent/tests/coding-task.e2e.ts index 4a827858bb..3b7e200582 100644 --- a/examples/headless-agent/tests/coding-task.e2e.ts +++ b/examples/headless-agent/tests/coding-task.e2e.ts @@ -56,12 +56,12 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('coding task: fix a failing test ctx = await codingHarness(workdir, { persona: SYSTEM_PROMPT }) const agent = ctx.agentLoop.create(SessionId('e2e-task'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) - agent.followup([{ + agent.followup({ content: [{ type: 'text', text: 'In the current directory, `node add.test.js` fails because add.js has a bug. ' + 'Fix add.js so the test passes, run `node add.test.js` to verify, and report the result. ' + 'Do not modify add.test.js.', - }]) + }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // The agent claims success… diff --git a/examples/headless-agent/tests/compaction.e2e.ts b/examples/headless-agent/tests/compaction.e2e.ts index a96b7f3611..48b73c3e6e 100644 --- a/examples/headless-agent/tests/compaction.e2e.ts +++ b/examples/headless-agent/tests/compaction.e2e.ts @@ -46,12 +46,12 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('compaction: a long session compa }) const agent = ctx.agentLoop.create(SessionId('e2e-compaction'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) - agent.followup([{ + agent.followup({ content: [{ type: 'text', text: 'Read file1.txt, file2.txt, file3.txt, and file4.txt one at a ' + 'time using cat (a separate bash command for each). After reading all four, tell me how ' + 'many files you read and the number mentioned in file1.txt.', - }]) + }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const events = [...agent.session.events] diff --git a/examples/headless-agent/tests/full-loop.e2e.ts b/examples/headless-agent/tests/full-loop.e2e.ts index 4f61ec3fa3..9f50a77937 100644 --- a/examples/headless-agent/tests/full-loop.e2e.ts +++ b/examples/headless-agent/tests/full-loop.e2e.ts @@ -30,7 +30,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('full loop: real model + real bas ctx = await codingHarness(workdir, { persona: SYSTEM_PROMPT }) const agent = ctx.agentLoop.create(SessionId('e2e-loop'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) - agent.followup([{ type: 'text', text: 'Run `echo e2e-ok` with the bash tool and tell me its exact output.' }]) + agent.followup({ content: [{ type: 'text', text: 'Run `echo e2e-ok` with the bash tool and tell me its exact output.' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const events = [...agent.session.events] diff --git a/examples/headless-agent/tests/resume.e2e.ts b/examples/headless-agent/tests/resume.e2e.ts index a4ded767da..2c9a96e099 100644 --- a/examples/headless-agent/tests/resume.e2e.ts +++ b/examples/headless-agent/tests/resume.e2e.ts @@ -41,7 +41,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('resume: continue a persisted ses sessionId: SESSION_ID, agentOptions: { provider: 'deepseek', model: 'deepseek-v4-flash' }, })).agent - first.followup([{ type: 'text', text: `Remember this code for later: ${SECRET}. Just acknowledge it.` }]) + first.followup({ content: [{ type: 'text', text: `Remember this code for later: ${SECRET}. Just acknowledge it.` }], source: { kind: 'user' } }) await waitForIdle(ctx, first) await ctx.fiber.dispose() ctx = undefined @@ -58,7 +58,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('resume: continue a persisted ses // The prior user turn is in the rehydrated log before the model is asked. expect(JSON.stringify(resumed.session.deriveMessages())).toContain(SECRET) - resumed.followup([{ type: 'text', text: 'What was the code I asked you to remember? Reply with just the code.' }]) + resumed.followup({ content: [{ type: 'text', text: 'What was the code I asked you to remember? Reply with just the code.' }], source: { kind: 'user' } }) await waitForIdle(ctx, resumed) // The model recalls it — only possible from the resumed history. diff --git a/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl b/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl index d91782e1e2..99eaf6e4ee 100644 --- a/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl +++ b/examples/headless-agent/tests/snapshots/pty-tools/session.jsonl @@ -3,7 +3,7 @@ {"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Exercise the six PTY tools in order, including one missing-session signal error, then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"session/title","seq":2,"time":0,"data":{"title":"Exercise the six PTY tools","messageSeqs":[1],"source":{"kind":"fallback"}}} {"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model.\n\nVerify your work by running the code or tests. Keep answers brief and\nfactual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nUse a terminal session only when work needs persistent terminal state or interactive stdin; prefer bash/read/write/edit for bounded one-shot operations. Track every terminal session id and close sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.\n\nTrack every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."}},"required":["command","description"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Run as a background task and return its id; collect with task_output or stop with task_kill."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."},"run_in_background":{"type":"boolean","description":"Run as a background task and return its id; collect with task_output or stop with task_kill."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}},{"name":"terminal_close","description":"Close one persistent terminal and wait until its captured owned process tree is gone.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."}},"required":["sessionId"]}},{"name":"terminal_list","description":"List persistent terminal sessions owned by the current agent.","parameters":{"type":"object","properties":{}}},{"name":"terminal_open","description":"Create a persistent, owner-isolated terminal session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.","parameters":{"type":"object","properties":{"type":{"type":"string","description":"Registered terminal backend type, usually \"shell\"."},"name":{"type":"string","description":"Optional owner-local display name such as \"main\" or \"gdb\"."},"cwd":{"type":"string","description":"Initial working directory. Defaults to the deployment workspace root."}},"required":["type"]}},{"name":"terminal_read","description":"Read a bounded page of retained output from a persistent terminal without sending input.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."},"offset":{"type":"number","description":"Newest-relative line offset (default 0)."},"count":{"type":"number","description":"Requested line count (default 500; backend caps apply)."}},"required":["sessionId"]}},{"name":"terminal_send","description":"Send text to a persistent terminal. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a task id for task_output/task_kill.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id returned by terminal_open or terminal_list."},"text":{"type":"string","description":"UTF-8 text to write to the terminal."},"submit":{"type":"boolean","description":"Submit Enter after text (default true). Set false for control characters or incomplete REPL input."},"run_in_background":{"type":"boolean","description":"Return a task id immediately; collect with task_output or stop with task_kill."}},"required":["sessionId","text"]}},{"name":"terminal_signal","description":"Send an allowed signal to the current foreground process group of a persistent terminal.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."},"signal":{"type":"string","description":"Signal to deliver. Shell-targeted SIGKILL is rejected; use terminal_close.","enum":["SIGINT","SIGTERM","SIGKILL","SIGTSTP","SIGHUP"]}},"required":["sessionId","signal"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const — no oneOf/pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]})."}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} +{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model.\n\nVerify your work by running the code or tests. Keep answers brief and\nfactual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nUse a terminal session only when work needs persistent terminal state or interactive stdin; prefer bash/read/write/edit for bounded one-shot operations. Track every terminal session id and close sessions that no longer matter. An inferred_idle or timeout result does not prove the foreground command exited.\n\nTrack every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."}},"required":["command","description"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) and return its final result. Use this to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent runs to completion and you receive only its final answer, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Run as a background task and return its id; collect with task_output or stop with task_kill."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn), returning only its final result. Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive only its final answer, not its intermediate steps. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."},"run_in_background":{"type":"boolean","description":"Run as a background task and return its id; collect with task_output or stop with task_kill."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}},{"name":"terminal_close","description":"Close one persistent terminal and wait until its captured owned process tree is gone.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."}},"required":["sessionId"]}},{"name":"terminal_list","description":"List persistent terminal sessions owned by the current agent.","parameters":{"type":"object","properties":{}}},{"name":"terminal_open","description":"Create a persistent, owner-isolated terminal session from a registered backend type. Use this for shell or REPL state that must survive across tool calls.","parameters":{"type":"object","properties":{"type":{"type":"string","description":"Registered terminal backend type, usually \"shell\"."},"name":{"type":"string","description":"Optional owner-local display name such as \"main\" or \"gdb\"."},"cwd":{"type":"string","description":"Initial working directory. Defaults to the deployment workspace root."}},"required":["type"]}},{"name":"terminal_read","description":"Read a bounded page of retained output from a persistent terminal without sending input.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."},"offset":{"type":"number","description":"Newest-relative line offset (default 0)."},"count":{"type":"number","description":"Requested line count (default 500; backend caps apply)."}},"required":["sessionId"]}},{"name":"terminal_send","description":"Send text to a persistent terminal. By default Enter is submitted and the call waits for a prompt, stdin wait, output silence, timeout, or session exit. Background mode returns a task id for task_output/task_kill.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id returned by terminal_open or terminal_list."},"text":{"type":"string","description":"UTF-8 text to write to the terminal."},"submit":{"type":"boolean","description":"Submit Enter after text (default true). Set false for control characters or incomplete REPL input."},"run_in_background":{"type":"boolean","description":"Return a task id immediately; collect with task_output or stop with task_kill."}},"required":["sessionId","text"]}},{"name":"terminal_signal","description":"Send an allowed signal to the current foreground process group of a persistent terminal.","parameters":{"type":"object","properties":{"sessionId":{"type":"string","description":"Terminal session id."},"signal":{"type":"string","description":"Signal to deliver. Shell-targeted SIGKILL is rejected; use terminal_close.","enum":["SIGINT","SIGTERM","SIGKILL","SIGTSTP","SIGHUP"]}},"required":["sessionId","signal"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Keep AT MOST ONE todo `in_progress` at a time; while work remains, exactly one active task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":true,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} {"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"pty-spawn","name":"terminal_open","argumentsDelta":"{\"type\":\"shell\",\"name\":\"main\"}"}}} {"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"pty-spawn","name":"terminal_open","arguments":"{\"type\":\"shell\",\"name\":\"main\"}"}}}} diff --git a/examples/headless-agent/tests/todo-write.e2e.ts b/examples/headless-agent/tests/todo-write.e2e.ts index c3053572d9..9a9ccb72cc 100644 --- a/examples/headless-agent/tests/todo-write.e2e.ts +++ b/examples/headless-agent/tests/todo-write.e2e.ts @@ -28,10 +28,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('todo_write: real model records a ctx = await codingHarness(workdir, { persona: TODO_SYSTEM_PROMPT }) const agent = ctx.agentLoop.create(SessionId('e2e-todo'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) - agent.followup([{ type: 'text', text: + agent.followup({ content: [{ type: 'text', text: 'Use the todo_write tool to record a plan of exactly two steps: first ' + '"inspect the failing test" (in_progress), then "apply the fix" (pending). ' - + 'Send both in one todo_write call, then reply with the single word DONE.' }]) + + 'Send both in one todo_write call, then reply with the single word DONE.' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const events = [...agent.session.events] diff --git a/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.expected.txt b/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.expected.txt index 099bf63b81..026bed95b4 100644 --- a/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.expected.txt +++ b/examples/tui-agent/tests/snapshots/multi-turn-conversation/terminal.expected.txt @@ -11,18 +11,18 @@ buffer 2| " deepseek-v4-flash • main-session" style 1-34 dim 3| -4| " Entering plan mode (applies from the next step). Use /plan off to leave. " - style 1-72 fg=bright-black -5| -6| "▌ " +4| "▌ " style 0-0 fg=bright-blue -7| "▌ You " +5| "▌ You " style 0-0 fg=bright-blue style 2-4 fg=bright-blue bold -8| "▌ Reply with exactly the word: ONE. No tools. " +6| "▌ Reply with exactly the word: ONE. No tools. " style 0-0 fg=bright-blue -9| "▌ " +7| "▌ " style 0-0 fg=bright-blue +8| +9| " Entering plan mode (applies from the next step). Use /plan off to leave. " + style 1-72 fg=bright-black 10| 11| " Reasoning " style 1-9 fg=bright-black italic @@ -36,20 +36,20 @@ buffer 17| " Leaving plan mode (applies from the next step). " style 1-47 fg=bright-black 18| -19| " Context · plan-mode " - style 1-19 dim -20| " The user switched this session back to the default mode. " - style 1-56 fg=bright-black -21| -22| "▌ " +19| "▌ " style 0-0 fg=bright-blue -23| "▌ You " +20| "▌ You " style 0-0 fg=bright-blue style 2-4 fg=bright-blue bold -24| "▌ Reply with exactly the word: TWO. No tools. " +21| "▌ Reply with exactly the word: TWO. No tools. " style 0-0 fg=bright-blue -25| "▌ " +22| "▌ " style 0-0 fg=bright-blue +23| +24| " Context · plan-mode " + style 1-19 dim +25| " The user switched this session back to the default mode. " + style 1-56 fg=bright-black 26| 27| " Reasoning " style 1-9 fg=bright-black italic diff --git a/packages/bash/tool-bash/tests/integration.spec.ts b/packages/bash/tool-bash/tests/integration.spec.ts index f679cfaa51..f5fb4ef396 100644 --- a/packages/bash/tool-bash/tests/integration.spec.ts +++ b/packages/bash/tool-bash/tests/integration.spec.ts @@ -109,7 +109,7 @@ describe('bash tool through the agent loop', () => { const location = ctx.sessionPersistence.locate(agent.session.header) expect(location?.kind).toBe('jsonl') - agent.followup([{ type: 'text', text: 'inspect the current session' }]) + agent.followup({ content: [{ type: 'text', text: 'inspect the current session' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const result = findEvent(events(agent), 'tool/result') @@ -129,7 +129,7 @@ describe('bash tool through the agent loop', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('it-fg'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'run echo integration-ok' }]) + agent.followup({ content: [{ type: 'text', text: 'run echo integration-ok' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const log = events(agent) @@ -161,7 +161,7 @@ describe('bash tool through the agent loop', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('it-exit'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'run exit 9' }]) + agent.followup({ content: [{ type: 'text', text: 'run exit 9' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const toolResult = findEvent(events(agent), 'tool/result') @@ -181,7 +181,7 @@ describe('bash tool through the agent loop', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('it-bg'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'run echo bg-ok in the background' }]) + agent.followup({ content: [{ type: 'text', text: 'run echo bg-ok in the background' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const firstResult = findEvent(events(agent), 'tool/result') @@ -201,7 +201,7 @@ describe('bash tool through the agent loop', () => { expect(notice.data.source).toEqual({ kind: 'plugin', plugin: 'tool-tasks' }) // The next turn collects the output through the generic task tool. - agent.followup([{ type: 'text', text: 'collect it' }]) + agent.followup({ content: [{ type: 'text', text: 'collect it' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const readResult = findEvent(events(agent), 'tool/result', 'last') expect(readResult.data.isError).toBe(false) diff --git a/packages/compact/compact-basic/src/region.ts b/packages/compact/compact-basic/src/region.ts index 7ac97d1c0c..28c7766dcb 100644 --- a/packages/compact/compact-basic/src/region.ts +++ b/packages/compact/compact-basic/src/region.ts @@ -177,10 +177,10 @@ export async function compactSurfaceRegion( /** * Reconstruct the last routed request's cacheable prefix for the shadowed - * region: its system prompt and tool schemas, then the request-only message - * prefix followed by the region's own derived messages in surface order. The - * summarizer appends only the compaction instruction after this, so the call - * is a genuine prefix of the conversation and reuses the provider's KV cache. + * region: its system prompt and tool schemas, then the region's own derived + * messages in surface order. The summarizer appends only the compaction + * instruction after this, so the call is a genuine prefix of the conversation + * and reuses the provider's KV cache. * @param session - session supplying the request header and per-node projection. * @param shadowedSeqs - the surface-node seqs, in order, being compacted. * @returns the replayed conversation prefix to condense. diff --git a/packages/compact/compact-basic/src/summarizer.ts b/packages/compact/compact-basic/src/summarizer.ts index ce4f28f8b3..dce9f486df 100644 --- a/packages/compact/compact-basic/src/summarizer.ts +++ b/packages/compact/compact-basic/src/summarizer.ts @@ -78,7 +78,7 @@ export interface SummarizationInput { readonly system?: string /** The conversation's tool schemas, reused for prefix-cache alignment; absent when the request carried none. */ readonly tools?: readonly ToolSchema[] - /** The request prefix followed by the shadowed region, in surface order, that precedes the compaction instruction. */ + /** The shadowed region, in surface order, that precedes the compaction instruction. */ readonly messages: readonly Message[] } diff --git a/packages/compact/compact-basic/tests/compact-basic.spec.ts b/packages/compact/compact-basic/tests/compact-basic.spec.ts index 105485a7d5..bf5a8a0ac4 100644 --- a/packages/compact/compact-basic/tests/compact-basic.spec.ts +++ b/packages/compact/compact-basic/tests/compact-basic.spec.ts @@ -546,7 +546,7 @@ describe('pressure measurement and retention', () => { expect(session.surface.nodes.length).toBeLessThan(8) }) - it('counts the durable routed request envelope without putting its prefix on the surface', async () => { + it('counts the durable routed request envelope without putting it on the surface', async () => { const compact = service({ auto: false, thresholdRatio: 0.9, @@ -555,22 +555,15 @@ describe('pressure measurement and retention', () => { const session = conversation(2, 'x'.repeat(600)) expect(await compactIfNeeded(compact, session)).toBeNull() - const prefix = [{ role: 'user' as const, content: [{ type: 'text' as const, text: 'p'.repeat(600) }] }] session.append('request/header', { header: { config: { provider: MODEL, model: MODEL }, - system: 's'.repeat(600), - messagePrefix: prefix, + system: 's'.repeat(2_000), }, reason: 'resume', }) const result = await compactIfNeeded(compact, session) expect(result).not.toBeNull() - expect(prefix).toHaveLength(1) - // The routed request prefix must not reach the surface as its own message - // (the compaction summary itself is an expected plugin-sourced checkpoint). - expect(session.events.some(event => event.type === 'user/message' - && event.data.content.some(block => block.type === 'text' && block.text.includes('p'.repeat(600))))).toBe(false) }) it('uses the latest logged request envelope without an AgentOptions override', async () => { @@ -800,13 +793,12 @@ describe('compaction region transaction', () => { expect(replay.deriveMessages()).toEqual(session.deriveMessages()) }) - it('replays the latest routed header prefix so the summarizer reuses the cache', async () => { + it('replays the latest routed header so the summarizer reuses the cache', async () => { const compact = service() const session = conversation(3) const tools = [{ name: 'do_thing', description: 'd', parameters: { type: 'object' } }] - const messagePrefix: Message[] = [{ role: 'user', content: [{ type: 'text', text: 'SESSION PREFIX' }] }] session.append('request/header', { - header: { config: { provider: MODEL, model: MODEL }, system: 'CONVERSATION SYSTEM', tools, messagePrefix }, + header: { config: { provider: MODEL, model: MODEL }, system: 'CONVERSATION SYSTEM', tools }, reason: 'resume', }) const nodes = session.surface.nodes @@ -815,7 +807,6 @@ describe('compaction region transaction', () => { const { input } = compact.calls[0]! expect(input.system).toBe('CONVERSATION SYSTEM') expect(input.tools).toEqual(tools) - expect(input.messages[0]).toEqual(messagePrefix[0]) expect(summarizedText(input)).toContain('fixture user 1') }) diff --git a/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts b/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts index 2b8038bdab..9e956c3402 100644 --- a/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts +++ b/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts @@ -201,7 +201,7 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', () provider: 'unconfigured-agent-fallback', model: 'unconfigured-agent-fallback', }) - agent.followup([{ type: 'text', text: 'do a routed multi-step task' }]) + agent.followup({ content: [{ type: 'text', text: 'do a routed multi-step task' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(agent.session.requestHeader()?.config.model).toBe('mock') @@ -219,7 +219,7 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', () const { ctx } = await harness(8) try { const agent = ctx.agentLoop.create(SessionId('post-step-order'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'do tool work' }]) + agent.followup({ content: [{ type: 'text', text: 'do tool work' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const events = [...agent.session.events] @@ -251,7 +251,7 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', () const { ctx } = await harness(8) try { const agent = ctx.agentLoop.create(SessionId('repro'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'do a long multi-step task' }]) + agent.followup({ content: [{ type: 'text', text: 'do a long multi-step task' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const events = [...agent.session.events] @@ -312,7 +312,7 @@ describe('context-overflow recovery across the real loop and compact-basic', () }, }) - agent.followup([{ type: 'text', text: 'continue from history' }]) + agent.followup({ content: [{ type: 'text', text: 'continue from history' }], source: { kind: 'user' } }) await agent.whenIdle() expect(adapter.conversationRequests).toHaveLength(2) @@ -388,7 +388,7 @@ describe('context-overflow recovery across the real loop and compact-basic', () seed: overflowHistorySeed(), agentOptions: { provider: 'mock', model: 'mock' }, }) - agent.followup([{ type: 'text', text: 'continue from history' }]) + agent.followup({ content: [{ type: 'text', text: 'continue from history' }], source: { kind: 'user' } }) await agent.whenIdle() expect(adapter.conversationRequests).toHaveLength(3) diff --git a/packages/context/README.md b/packages/context/README.md index 4f06db67dd..bd23068a73 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -6,6 +6,6 @@ Product plugins that add model-visible request context without defining a tool. |---|---|---| | `session-reference/` | Bounded current-surface snapshots of other sessions | `ctx.sessionReferences` | | `time-context/` | Durable per-step current time and elapsed-time context | (none) | -| `workspace-context/` | `AGENTS.md`/`CLAUDE.md` workspace context loader | (listens on `agent/session-prefix` + `tools/post-execute`) | +| `workspace-context/` | `AGENTS.md`/`CLAUDE.md` workspace context loader | (listens on `agent/step` + `tools/post-execute`) | The [`workspace-context` decision record](../../.agents/notes/implemented/feature/2026-06-24-workspace-context.md) explains its per-agent/session isolation and lifecycle split. diff --git a/packages/context/session-reference/README.md b/packages/context/session-reference/README.md index 696cadf2f0..647bb20f9d 100644 --- a/packages/context/session-reference/README.md +++ b/packages/context/session-reference/README.md @@ -5,7 +5,7 @@ ## Public API - `listCandidates(agent, query?, limit?)` lists sessions other than `agent.id`, filters case-insensitively by id or cwd, and ranks same-cwd, cwd-less, then other-cwd records while preserving `listSessions()` creation order within each group. Each selected candidate uses its latest log-backed title as the mention label and falls back to the session id; titles and message bodies are not searched. -- `prepare(agent, content, references, signal?)` preserves first-mention order, deduplicates ids, rejects self-reference and more than the configured distinct-source limit, reads every source in parallel, and returns detached content plus zero or one aggregated `AdditionalContext`. Any invalid reference, failed read, cancellation, or budget failure rejects before the host injects context and sends or steers the direct message. +- `prepare(agent, content, references, signal?)` preserves first-mention order, deduplicates ids, rejects self-reference and more than the configured distinct-source limit, reads every source in parallel, and returns detached content plus zero or one aggregated `UserMessageData` context. Any invalid reference, failed read, cancellation, or budget failure rejects before the host injects context and sends or steers the direct message. - `encodeSessionReferenceUri()` and `decodeSessionReferenceUri()` implement `dsh-session:` so every JavaScript string id round-trips exactly. `formatSessionReferenceMention()` emits `@[label](uri)`, and `parseSessionReferenceText()` replaces Markdown mentions or bare canonical URIs with readable `@label` text while returning structured references. Explicit Markdown mentions reject every malformed URI; bare text is considered a reference only when a non-empty base64url-shaped payload follows the scheme, and a matching noncanonical candidate still fails. Empty or punctuation-only scheme mentions remain ordinary discussion text. ## Snapshot semantics diff --git a/packages/context/session-reference/src/index.ts b/packages/context/session-reference/src/index.ts index 19f2dad852..3341990806 100644 --- a/packages/context/session-reference/src/index.ts +++ b/packages/context/session-reference/src/index.ts @@ -7,9 +7,9 @@ import { Context, Service } from 'cordis' import z from 'schemastery' -import type { AdditionalContext, Agent } from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' import type { ContentBlock } from '@deepseek-ai/dsh-llm' -import type { SessionId } from '@deepseek-ai/dsh-session' +import type { SessionId, UserMessageData } from '@deepseek-ai/dsh-session' import type { SessionSurfaceSnapshot } from '@deepseek-ai/dsh-session-query' import { DEFAULT_CANDIDATE_LIMIT, @@ -192,7 +192,7 @@ export class SessionReferenceService extends Service { inputIndex: index, })), } - const additionalContext: AdditionalContext = { + const additionalContext: UserMessageData = { source, content: [{ type: 'text', text: prompt }], } diff --git a/packages/context/session-reference/src/types.ts b/packages/context/session-reference/src/types.ts index a19f6f729e..3804ae3677 100644 --- a/packages/context/session-reference/src/types.ts +++ b/packages/context/session-reference/src/types.ts @@ -1,8 +1,7 @@ /** Public session-reference request, candidate, and preparation records. */ -import type { AdditionalContext } from '@deepseek-ai/dsh-agent' import type { ContentBlock } from '@deepseek-ai/dsh-llm' -import type { SessionId } from '@deepseek-ai/dsh-session' +import type { SessionId, UserMessageData } from '@deepseek-ai/dsh-session' /** Durable provenance for one prepared cross-session context. */ export interface SessionReferenceSource { @@ -53,7 +52,7 @@ export interface PreparedReferencedMessage { /** Readable message content after host mention tokens are removed. */ content: ContentBlock[] /** Aggregated untrusted snapshot, absent when the message has no references. */ - additionalContext?: AdditionalContext + additionalContext?: UserMessageData } /** Text-only projected conversation item. */ diff --git a/packages/context/time-context/README.md b/packages/context/time-context/README.md index f4938982a1..de7c9577d6 100644 --- a/packages/context/time-context/README.md +++ b/packages/context/time-context/README.md @@ -18,7 +18,7 @@ When `timeZone` is omitted, the plugin resolves the Node process's system zone o ## Timing semantics -The plugin prepends an `agent/pre-step` listener. When an injection is due, it appends one injected `user/message` through `agent.inject()` before `step/start` and ordinary automatic compaction, with source `{ kind: 'plugin', plugin: 'time-context' }`. A suppressed attempt appends nothing. +The plugin prepends an `agent/step` listener. When an injection is due, it appends one injected `user/message` through `agent.inject()` before `step/start` and ordinary automatic compaction, with source `{ kind: 'plugin', plugin: 'time-context' }`. A suppressed attempt appends nothing. Positive-interval scheduling scans the raw durable session events for the latest `user/message` with that source, including a reading shadowed by compaction. The schedule therefore applies across turns and resumed processes without process-local cache state. It reduces append frequency and history growth but never removes an existing reading, and sessions schedule independently. diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts index 739f56cda0..8b080206e1 100644 --- a/packages/context/time-context/src/index.ts +++ b/packages/context/time-context/src/index.ts @@ -173,9 +173,6 @@ export function apply(ctx: Context, config: Config): void { const previous = step === 1 ? precedingMessageTime(agent) : precedingStepContextTime(agent, turn) - agent.inject( - [{ type: 'text', text: renderText(now, turn, step, previous, formatter, resolvedTimeZone) }], - { source: { kind: 'plugin', plugin: name } }, - ) + agent.inject({ content: [{ type: 'text', text: renderText(now, turn, step, previous, formatter, resolvedTimeZone) }], source: { kind: 'plugin', plugin: name } }) }, { prepend: true }) } diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts index d9c95f0bc4..96e6410e1d 100644 --- a/packages/context/time-context/tests/time-context.spec.ts +++ b/packages/context/time-context/tests/time-context.spec.ts @@ -44,11 +44,8 @@ function sessionAgent(session: Session, id = 'agent'): Agent { ctx: new Context(), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), - inject(content, options) { - session.append('user/message', { - content, - source: options?.source ?? { kind: 'user' }, - }, { surfaceOp: 'append' }) + inject(input) { + session.append('user/message', input, { surfaceOp: 'append' }) return AgentMessageId('stub') }, send: () => AgentMessageId('stub'), @@ -374,7 +371,7 @@ describe('real agent-loop request history', () => { }) const agent = ctx.agentLoop.create(SessionId(`late-${mode}`), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'start' }]) + agent.followup({ content: [{ type: 'text', text: 'start' }], source: { kind: 'user' } }) await agent.whenIdle() expect(laterSawReading).toBe(false) @@ -400,7 +397,7 @@ describe('real agent-loop request history', () => { })) const agent = ctx.agentLoop.create(SessionId('loop'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'start' }]) + agent.followup({ content: [{ type: 'text', text: 'start' }], source: { kind: 'user' } }) await agent.whenIdle() expect(adapter.requests).toHaveLength(2) diff --git a/packages/context/workspace-context/README.md b/packages/context/workspace-context/README.md index df35697159..abe4e0a34c 100644 --- a/packages/context/workspace-context/README.md +++ b/packages/context/workspace-context/README.md @@ -1,10 +1,10 @@ # @deepseek-ai/dsh-workspace-context -Per-session workspace instruction loading for `AGENTS.md`-compatible files. The plugin freezes the initial user-global and project instruction chain into the request prefix, then discovers nested files and reports later changes or removals through durable context messages after successful filesystem tool calls. +Per-session workspace instruction loading for `AGENTS.md`-compatible files. The plugin injects the initial user-global and project instruction chain into durable history, then discovers nested files and reports later changes or removals after successful filesystem tool calls. ## Lifecycle -The baseline is composed once per agent-loop instance on `agent/session-prefix`. It reads `$DSH_HOME/AGENTS.md` followed by, in each directory from the project root to `agent.session.header.cwd`, every existing base candidate and then every existing local-overlay candidate. Within one directory, candidates whose content is byte-identical after trimming leading and trailing whitespace collapse to the earliest candidate in configured order, so a `CLAUDE.md` that merely duplicates its sibling `AGENTS.md` is rendered once. The prefix is placed before all derived history, recorded in `EpochHeader.messagePrefix`, and reused verbatim for that loop instance. Because the plugin prepends its contribution before delegating, a later-registered skills catalog appears after workspace instructions. +The baseline is injected at the first `agent/step` of each live session. It reads `$DSH_HOME/AGENTS.md` followed by, in each directory from the project root to `agent.session.header.cwd`, every existing base candidate and then every existing local-overlay candidate. Within one directory, candidates whose content is byte-identical after trimming leading and trailing whitespace collapse to the earliest candidate in configured order, so a `CLAUDE.md` that merely duplicates its sibling `AGENTS.md` is rendered once. The durable sourced `user/message` enters the same request as the claimed prompt. The plugin also listens on `tools/post-execute` for successful first-party `read`, `write`, and `edit` calls. Each touch checks newly reached descendant scopes and every previously loaded scope. Each configured candidate name is an independent scope in its directory: a newly present file is attached through the result's `additionalContexts`; a changed file appends a replacement; a file that disappears or becomes a per-directory duplicate of an earlier candidate appends a removal notice. Native calls and Code Mode sub-dispatches share this path: `run_code` defers each nested context until its outer result, so the loop still appends updates after tool-call/result adjacency is complete. This follows structured filesystem activity rather than shell `cd`, because each local bash call starts a fresh shell and parsing arbitrary shell syntax would be unreliable. @@ -12,7 +12,7 @@ Instruction reads use the optional `ctx.fs` provider. The plugin does not static ## Prompt Shape -Baseline instructions are request-only user-role prefix messages framed with the familiar system-reminder pattern: +Baseline instructions are durable user-role messages framed with the familiar system-reminder pattern: ```md @@ -28,7 +28,7 @@ Instructions from: AGENTS.md ``` -Newly reached scopes use a durable raw `context/message`: +Newly reached scopes use a durable sourced `user/message`: ```md @@ -42,7 +42,7 @@ These instructions apply to work under `packages/app`. Use them as guidance when A same-file edit starts with `Updated instructions from: ` and says to use the new content instead of the previously loaded content. When a candidate disappears or becomes a per-directory duplicate of an earlier candidate, the message is `Instructions removed: ` followed by `The previously loaded instructions from this file no longer apply.` Literal `` text inside an instruction file is escaped so file content cannot close the plugin-owned frame. -The plugin owns the complete `` framing, and every `context/message` (from this plugin or any other) reaches the model verbatim as a user-role message with no wrapping. +The plugin owns the complete `` framing, and every injected `user/message` reaches the model verbatim with no core wrapper. ## State And Refresh @@ -50,7 +50,7 @@ Model-visible text contains no hidden state markers. Each dynamic context event An unchanged path and SHA-1 content digest is not injected again. A per-session, per-scope provider cache stores only `{ path, version, digest, trimmedDigest }`: when the provider's opaque `FsVersion` and the effective visible state both match, reconciliation skips the content read; a changed version triggers a bounded read and SHA-1 confirmation before any model-visible update. The `trimmedDigest` — SHA-1 over the whitespace-trimmed content — is the per-directory duplicate key, so an unchanged file can still be removed when an earlier candidate converges on its content. Resume works because SHA-1 state is persisted in the typed source, while an empty in-memory version cache merely causes one confirming read. Compaction re-arms a scope after its context event leaves the visible surface even when the cached version is unchanged. A removal is a tombstone, so a later candidate reappearance is loaded again. Only model-visible changes actually rendered within the byte budget enter the source, pending state, and version cache; an omitted change remains eligible for a later touch, while a same-digest version refresh updates only the provider cache. -The frozen baseline itself is not rewritten mid-instance. Its initial path/digest map is retained as comparison state; the next successful filesystem touch appends any baseline replacement or removal. A resumed loop recomposes the current baseline and also reconciles still-visible dynamic scopes during prefix composition. There is no file watcher, so an on-disk change becomes visible at the next successful `read`, `write`, or `edit` touch, or when a resumed loop composes its prefix. +The initial baseline event itself is not rewritten. Its path/digest map is retained as comparison state; the next successful filesystem touch appends any baseline replacement or removal. A resumed loop recomposes the current baseline and also reconciles still-visible dynamic scopes before its first request. There is no file watcher, so an on-disk change becomes visible at the next successful `read`, `write`, or `edit` touch, or when a resumed loop prepares its baseline. ## Configuration @@ -77,11 +77,11 @@ Instruction content is read through `streamText()` under `maxSourceBytes`, even ## Model Experience -### Baseline session prefix +### Baseline context #### What the model sees -At the first request of each loop instance, the model receives one user-role prefix message containing the bounded user-global and project instruction chain in broad-to-specific order. +At the first request of each loop instance, the model receives one durable user-role message containing the bounded user-global and project instruction chain in broad-to-specific order. ##### Baseline instruction template @@ -101,17 +101,17 @@ Instructions from: AGENTS.md #### Token effect -The rendered baseline is frozen and resent on every request in that loop instance. `maxBytes` bounds the complete message, broader files are omitted before the most-specific file is truncated, and an empty chain contributes zero tokens. +The rendered baseline is appended once and remains in derived history until compaction. `maxBytes` bounds the complete message, broader files are omitted before the most-specific file is truncated, and an empty chain contributes zero tokens. #### KV Cache effect -Prefix-stable within one loop instance because the baseline is frozen. A new or resumed instance recomposes it, so instruction, precedence, cwd, candidate, or byte-budget changes may invalidate reuse from the first changed baseline token. +Append-only after the existing reusable prefix. A new or resumed instance may append a recomposed baseline, so instruction, precedence, cwd, candidate, or byte-budget changes affect cache reuse from that history position. ### Newly discovered scope context #### What the model sees -After a successful first-party filesystem call reaches a deeper directory, the next request includes one retained raw `context/message` with the newly applicable instruction file. +After a successful first-party filesystem call reaches a deeper directory, the next request includes one retained sourced `user/message` with the newly applicable instruction file. ##### Additional instruction template @@ -160,7 +160,7 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work - **Discovery follows structured fs tools, not shell navigation** — a `bash` command that changes directories does not trigger nested instruction discovery because shell syntax and per-call shell state are not a reliable filesystem seam. -- **Refresh is touch-driven** — there is no watcher; external edits become visible on the next successful first-party `read`, `write`, or `edit`, or when a resumed loop recomposes its prefix. +- **Refresh is touch-driven** — there is no watcher; external edits become visible on the next successful first-party `read`, `write`, or `edit`, or when a resumed loop prepares its baseline. - **Candidate semantics stay intentionally small** — lowercase names, `.claude/rules/`, and `@path` imports are not interpreted; project scopes load `AGENTS.local.md`/`CLAUDE.local.md` overlays by default, but the user-global `$DSH_HOME` scope has no local overlay and other custom names require explicit candidate configuration. - **Per-directory dedup is content-based** — sibling candidates collapse only when byte-identical after trimming leading and trailing whitespace; a `CLAUDE.md` that symlinks its sibling `AGENTS.md` resolves to the same content and collapses like any duplicate, while a distinct real copy that has drifted from `AGENTS.md` loads in full alongside it. - **Symlinked instruction files are followed across the trust boundary** — a candidate whose final component is a symlink is resolved and its target loaded, so a cloned repository can surface off-tree file content as lower-authority workspace guidance (it never overrides system, developer, or direct user instructions). Confine `ctx.fs` with the filesystem policy gate or an OS sandbox when loading untrusted repositories. diff --git a/packages/context/workspace-context/src/index.ts b/packages/context/workspace-context/src/index.ts index 95430e6c27..6c2c5b95c7 100644 --- a/packages/context/workspace-context/src/index.ts +++ b/packages/context/workspace-context/src/index.ts @@ -97,14 +97,12 @@ export function apply(ctx: Context, config: Config): void { { includeBaselineScopes: false, signal }, ) if (update !== undefined) { - agent.inject(update.context.content, { - source: update.context.source, - }) + agent.inject({ content: update.context.content, source: update.context.source }) applyInstructionVersionUpdates(agent.session, update.versionUpdates, instructionVersions) } if (instructions !== undefined && instructions.rendered.text.length > 0) { const baselineMessage = workspaceContextMessage(instructions.rendered.text) - agent.inject(baselineMessage.content, { source: { kind: 'plugin', plugin: 'workspace-context' } }) + agent.inject({ content: baselineMessage.content, source: { kind: 'plugin', plugin: 'workspace-context' } }) } baselineLoaded.add(agent.session) }) diff --git a/packages/context/workspace-context/src/state.ts b/packages/context/workspace-context/src/state.ts index d397218e95..99a5254ed7 100644 --- a/packages/context/workspace-context/src/state.ts +++ b/packages/context/workspace-context/src/state.ts @@ -4,9 +4,9 @@ * @module @deepseek-ai/dsh-workspace-context/state */ -import type { AdditionalContext, Agent } from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' import type { Message } from '@deepseek-ai/dsh-llm' -import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { Session, SessionEvent, UserMessageData } from '@deepseek-ai/dsh-session' import type { FileSystem, FsVersion } from '@deepseek-ai/dsh-fs' import type { ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools' import type { ResolvedConfig } from './config.ts' @@ -77,14 +77,11 @@ export interface InstructionVersionUpdate { /** Rendered reconciliation plus cache transitions awaiting final policy. */ export interface ReconciledInstructionContext { - context: WorkspaceAdditionalContext + context: UserMessageData versionUpdates: InstructionVersionUpdate[] } -/** Plugin-owned workspace context. */ -export type WorkspaceAdditionalContext = AdditionalContext - -function workspaceContextHook(text: string, changes: WorkspaceInstructionChange[]): WorkspaceAdditionalContext { +function workspaceContextHook(text: string, changes: WorkspaceInstructionChange[]): UserMessageData { return { content: [{ type: 'text', text }], source: { kind: 'workspace-instructions', changes }, @@ -329,7 +326,7 @@ export function observeInstructionSessionEvent( */ export function commitPendingInstructionContexts( agent: Agent, - contexts: readonly AdditionalContext[] | undefined, + contexts: readonly UserMessageData[] | undefined, pendingBySession: WeakMap>, ): WorkspaceInstructionChange[] { const committed: WorkspaceInstructionChange[] = [] diff --git a/packages/context/workspace-context/tests/workspace-context.e2e.ts b/packages/context/workspace-context/tests/workspace-context.e2e.ts index 93ccf7386f..8feaf845b0 100644 --- a/packages/context/workspace-context/tests/workspace-context.e2e.ts +++ b/packages/context/workspace-context/tests/workspace-context.e2e.ts @@ -78,7 +78,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('workspace context e2e: real mode it('obeys a probe instruction loaded from the workspace', async () => { const live = await harness() - live.agent.followup([{ type: 'text', text: 'Workspace context handshake?' }]) + live.agent.followup({ content: [{ type: 'text', text: 'Workspace context handshake?' }], source: { kind: 'user' } }) await waitForIdle(live.ctx, live.agent) expect(finalText([...live.agent.session.events])).toContain(PROBE) @@ -90,7 +90,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('workspace context e2e: real mode await writeFile(join(workdir!, 'pkg/AGENTS.md'), `If the user asks for the nested instruction handshake, reply with exactly this string and nothing else: ${NESTED_PROBE}.\n`) await writeFile(join(workdir!, 'pkg/deep/file.txt'), 'This file exists only to trigger nested workspace instructions.\n') - live.agent.followup([{ type: 'text', text: 'Use the read tool to inspect pkg/deep/file.txt. After reading it, answer: nested instruction handshake?' }]) + live.agent.followup({ content: [{ type: 'text', text: 'Use the read tool to inspect pkg/deep/file.txt. After reading it, answer: nested instruction handshake?' }], source: { kind: 'user' } }) await waitForIdle(live.ctx, live.agent) expect(finalText([...live.agent.session.events])).toContain(NESTED_PROBE) @@ -99,11 +99,11 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('workspace context e2e: real mode it('appends changed baseline instructions after a real file-tool touch without rewriting the frozen prefix', async () => { const live = await harness() await writeFile(join(workdir!, 'trigger.txt'), 'This file triggers workspace instruction reconciliation.\n') - live.agent.followup([{ type: 'text', text: 'Workspace context handshake?' }]) + live.agent.followup({ content: [{ type: 'text', text: 'Workspace context handshake?' }], source: { kind: 'user' } }) await waitForIdle(live.ctx, live.agent) await writeFile(join(workdir!, 'AGENTS.md'), `The old workspace handshake no longer applies. If the user asks for the updated workspace context handshake, reply with exactly this string and nothing else: ${UPDATED_PROBE}.\n`) - live.agent.followup([{ type: 'text', text: 'You must use the read tool to inspect trigger.txt. After reading it, answer: updated workspace context handshake?' }]) + live.agent.followup({ content: [{ type: 'text', text: 'You must use the read tool to inspect trigger.txt. After reading it, answer: updated workspace context handshake?' }], source: { kind: 'user' } }) await waitForIdle(live.ctx, live.agent) const events = [...live.agent.session.events] diff --git a/packages/context/workspace-context/tests/workspace-context.spec.ts b/packages/context/workspace-context/tests/workspace-context.spec.ts index f2ea056cdf..1e994500f6 100644 --- a/packages/context/workspace-context/tests/workspace-context.spec.ts +++ b/packages/context/workspace-context/tests/workspace-context.spec.ts @@ -6,8 +6,8 @@ import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import * as workspaceContext from '@deepseek-ai/dsh-workspace-context' import LlmService, { CallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm' -import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent } from '@deepseek-ai/dsh-session' -import AgentRegistry, { agentEvents, AgentMessageId, type AdditionalContext, type Agent } from '@deepseek-ai/dsh-agent' +import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent, type UserMessageData } from '@deepseek-ai/dsh-session' +import AgentRegistry, { agentEvents, AgentMessageId, type Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' import type { @@ -179,11 +179,8 @@ function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent { status: 'idle', followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), - inject(content, options) { - session.append('user/message', { - content, - source: options?.source ?? { kind: 'user' }, - }, { surfaceOp: 'append' }) + inject(input) { + session.append('user/message', input, { surfaceOp: 'append' }) return AgentMessageId('stub') }, send: () => AgentMessageId('stub'), @@ -204,12 +201,12 @@ function blocksText(blocks: { type: string; text?: string }[] | undefined): stri return blocks?.map(block => block.type === 'text' ? block.text ?? '' : '').join('\n') ?? '' } -function workspaceContextOf(result: { additionalContexts?: AdditionalContext[] }): AdditionalContext | undefined { +function workspaceContextOf(result: { additionalContexts?: UserMessageData[] }): UserMessageData | undefined { return result.additionalContexts?.find(context => context.source.kind === 'workspace-instructions') } -function workspaceChangeContext(scope: string, digest: string): AdditionalContext { +function workspaceChangeContext(scope: string, digest: string): UserMessageData { return { content: [{ type: 'text', text: `instructions for ${scope}` }], source: { @@ -219,7 +216,7 @@ function workspaceChangeContext(scope: string, digest: string): AdditionalContex } } -function appendAdditionalContexts(agent: Agent, result: { additionalContexts?: AdditionalContext[] }): number | undefined { +function appendAdditionalContexts(agent: Agent, result: { additionalContexts?: UserMessageData[] }): number | undefined { let lastSeq: number | undefined for (const context of result.additionalContexts ?? []) { lastSeq = agent.session.append('user/message', { @@ -1013,9 +1010,7 @@ describe('workspace context request injection', () => { const ctx = new Context() await mountWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 }) ctx.on('agent/step', (agent) => { - agent.inject([{ type: 'text', text: 'Available skills' }], { - source: { kind: 'plugin', plugin: 'test-skills' }, - }) + agent.inject({ content: [{ type: 'text', text: 'Available skills' }], source: { kind: 'plugin', plugin: 'test-skills' } }) }) const prefix = await composeBaselinePrefix(ctx, stubAgent(root)) @@ -1514,7 +1509,7 @@ describe('workspace context request injection', () => { } }) - it('cleans up its agent/session-prefix listener when the plugin fiber is disposed', async () => { + it('cleans up its agent/step listener when the plugin fiber is disposed', async () => { const root = await tempRepo() const home = await tempRepo() try { @@ -1711,13 +1706,13 @@ describe('dynamic nested workspace context injection', () => { }, })) - agent.followup([{ type: 'text', text: 'read and abort' }]) + agent.followup({ content: [{ type: 'text', text: 'read and abort' }], source: { kind: 'user' } }) await agent.whenIdle() expect(agent.session.events.filter(event => event.type === 'user/message' && event.data.source.kind !== 'user', )).toHaveLength(0) - agent.followup([{ type: 'text', text: 'retry the read' }]) + agent.followup({ content: [{ type: 'text', text: 'retry the read' }], source: { kind: 'user' } }) await agent.whenIdle() const contexts = agent.session.events.filter(event => event.type === 'user/message' && event.data.source.kind !== 'user') diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 2f33e60e1e..997709aaf0 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -917,8 +917,8 @@ export const EVENT_API: readonly EventApiEntry[] = [ name: 'agent/prompt-submit', mode: 'waterfall', signature: '\'agent/prompt-submit\'(this: Scoped, agent: Agent, content: ContentBlock[], source: MessageSource, signal: AbortSignal, next: () => Promise): Promise', - jsDoc: '/**\n * Allow, rewrite, or block one claimed prompt before it becomes a user\n * message. Call `next()` for the unchanged default. The signal controls only this turn;\n * listeners may cooperate with it but must not retain it for another turn.\n * @param agent - the agent whose turn claimed the message.\n * @param content - the claimed message\'s blocks, as queued.\n * @param source - the message\'s resolved source.\n * @param signal - the current turn\'s explicit abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */', - summary: 'Allow, rewrite, or block one claimed prompt before it becomes a user message.', + jsDoc: '/**\n * Allow, rewrite, or block one claimed prompt before it becomes a user\n * message or opens a turn. Call `next()` for the unchanged default. The\n * signal controls only this admission attempt; listeners may cooperate with\n * it but must not retain it for a later attempt or turn.\n * @param agent - the agent whose turn claimed the message.\n * @param content - the claimed message\'s blocks, as queued.\n * @param source - the message\'s resolved source.\n * @param signal - the current turn\'s explicit abort signal.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */', + summary: 'Allow, rewrite, or block one claimed prompt before it becomes a user message or opens a turn.', }, { name: 'agent/request', @@ -1162,13 +1162,9 @@ export const EVENT_API: readonly EventApiEntry[] = [ /** Shapes of every exported type the SERVICE_API signatures reference (transitively), sorted by name. */ export const TYPE_API: readonly TypeApiEntry[] = [ - { - name: 'AdditionalContext', - declaration: 'export type AdditionalContext = UserMessageData;', - }, { name: 'Agent', - declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly status: AgentStatus;\n readonly ctx: Context;\n send(content: ContentBlock[], options: SendOptions): AgentMessageId;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise;\n followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId;\n retry(): void;\n}', + declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly status: AgentStatus;\n readonly ctx: Context;\n send(input: UserMessageData, options: SendOptions): AgentMessageId;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise;\n followup(input: UserMessageData): AgentMessageId;\n steer(input: UserMessageData): AgentMessageId;\n inject(input: UserMessageData): AgentMessageId;\n retry(): void;\n}', }, { name: 'AgentCancelCause', @@ -1194,10 +1190,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'AgentStatus', declaration: 'export type AgentStatus = \'idle\' | \'running\';', }, - { - name: 'AliasSendOptions', - declaration: 'export interface AliasSendOptions {\n source?: MessageSource;\n}', - }, { name: 'ApprovalOutcome', declaration: 'export type ApprovalOutcome = \'allowed-once\' | \'rejected\' | \'cancelled\' | \'unavailable\';', @@ -1408,7 +1400,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'EpochHeader', - declaration: 'export interface EpochHeader {\n config: LlmCallConfig;\n system?: string;\n tools?: ToolSchema[];\n messagePrefix?: Message[];\n}', + declaration: 'export interface EpochHeader {\n config: LlmCallConfig;\n system?: string;\n tools?: ToolSchema[];\n}', }, { name: 'FileDiff', @@ -1580,7 +1572,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'PreparedReferencedMessage', - declaration: 'export interface PreparedReferencedMessage {\n content: ContentBlock[];\n additionalContext?: AdditionalContext;\n}', + declaration: 'export interface PreparedReferencedMessage {\n content: ContentBlock[];\n additionalContext?: UserMessageData;\n}', }, { name: 'PresetOption', @@ -1724,7 +1716,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SendOptions', - declaration: 'export interface SendOptions {\n target: SendTarget;\n wakeup: boolean;\n source: MessageSource;\n}', + declaration: 'export interface SendOptions {\n target: SendTarget;\n wakeup: boolean;\n}', }, { name: 'SendTarget', @@ -1744,7 +1736,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionEventMap', - declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n trigger: TurnTrigger;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': UserMessageData;\n \'prompt/blocked\': {\n content: ContentBlock[];\n source: MessageSource;\n reason: string;\n };\n \'assistant/chunk\': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n content: ContentBlock[];\n provenance: AssistantProvenance;\n usage?: TokenUsage;\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n \'steering/message\': UserMessageData & {\n turn: number;\n };\n \'todo/write\': {\n todos: TodoItem[];\n };\n \'request/header\': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n}', + declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n trigger: TurnTrigger;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': UserMessageData;\n \'assistant/chunk\': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n content: ContentBlock[];\n provenance: AssistantProvenance;\n usage?: TokenUsage;\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n \'steering/message\': UserMessageData & {\n turn: number;\n };\n \'todo/write\': {\n todos: TodoItem[];\n };\n \'request/header\': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n}', }, { name: 'SessionEventMetadataFilter', @@ -2108,7 +2100,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolExecutionFailure', - declaration: 'export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: AdditionalContext[];\n readonly concludesTurn?: never;\n}', + declaration: 'export interface ToolExecutionFailure {\n readonly isError: true;\n readonly error: ToolFailure;\n readonly value?: never;\n readonly content: ContentBlock[];\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessageData[];\n readonly concludesTurn?: never;\n}', }, { name: 'ToolExecutionInput', @@ -2124,7 +2116,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolExecutionSuccess', - declaration: 'export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: AdditionalContext[];\n readonly concludesTurn?: true;\n}', + declaration: 'export interface ToolExecutionSuccess {\n readonly isError: false;\n readonly value: JsonValue;\n readonly content: ContentBlock[];\n readonly error?: never;\n readonly meta?: JsonValue;\n readonly additionalContexts?: UserMessageData[];\n readonly concludesTurn?: true;\n}', }, { name: 'ToolExecutionToken', @@ -2164,7 +2156,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolRunContext', - declaration: 'export interface ToolRunContext extends ToolExecution {\n deferContext(context: AdditionalContext): void;\n concludeTurn(): void;\n}', + declaration: 'export interface ToolRunContext extends ToolExecution {\n deferContext(context: UserMessageData): void;\n concludeTurn(): void;\n}', }, { name: 'ToolSchema', @@ -2228,7 +2220,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'TurnEndReasonMap', - declaration: 'export interface TurnEndReasonMap {\n completed: {\n kind: \'completed\';\n };\n aborted: {\n kind: \'aborted\';\n };\n error: {\n kind: \'error\';\n step: number;\n } & ({\n failure: LlmFailure;\n message?: never;\n code?: never;\n } | {\n message: string;\n code?: string;\n failure?: never;\n });\n disposed: {\n kind: \'disposed\';\n };\n \'max-tokens\': {\n kind: \'max-tokens\';\n };\n rejected: {\n kind: \'rejected\';\n reason: string;\n };\n interrupted: {\n kind: \'interrupted\';\n };\n}', + declaration: 'export interface TurnEndReasonMap {\n completed: {\n kind: \'completed\';\n };\n aborted: {\n kind: \'aborted\';\n };\n error: {\n kind: \'error\';\n step: number;\n } & ({\n failure: LlmFailure;\n message?: never;\n code?: never;\n } | {\n message: string;\n code?: string;\n failure?: never;\n });\n disposed: {\n kind: \'disposed\';\n };\n \'max-tokens\': {\n kind: \'max-tokens\';\n };\n interrupted: {\n kind: \'interrupted\';\n };\n}', }, { name: 'TurnTrigger', diff --git a/packages/cordis/tool-cordis/tests/integration.spec.ts b/packages/cordis/tool-cordis/tests/integration.spec.ts index 7f917e5b3c..9cedbd2a33 100644 --- a/packages/cordis/tool-cordis/tests/integration.spec.ts +++ b/packages/cordis/tool-cordis/tests/integration.spec.ts @@ -47,7 +47,7 @@ describe('cordis tools through the agent loop', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('it-cordis'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'give yourself reverse_text, use it, clean up' }]) + agent.followup({ content: [{ type: 'text', text: 'give yourself reverse_text, use it, clean up' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const log = agent.session.events diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 42847eaac7..6285d38874 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -14,7 +14,6 @@ import type { Scope } from '@deepseek-ai/dsh-scope' import type { AgentMessage, Agent, - AliasSendOptions, CancelOptions, AgentInterruptReason, AgentOptions, @@ -27,9 +26,7 @@ import type { import { BlockAssembler, LlmError, assertNever, deepFreeze, errorChain, isHarnessError, llmFailureOf, markAgentLoopRequest, } from '@deepseek-ai/dsh-llm' -import type { - ContentBlock, GenerateOptions, LlmCallConfig, LlmFailure, Message, -} from '@deepseek-ai/dsh-llm' +import type { GenerateOptions, LlmCallConfig, LlmFailure, Message } from '@deepseek-ai/dsh-llm' import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session' import type { Session, SessionId, TurnEndReason, TurnTrigger, UserMessageData } from '@deepseek-ai/dsh-session' import { renderPrompt } from '@deepseek-ai/dsh-system-prompt' @@ -89,10 +86,11 @@ export class ReactLoopAgent implements Agent { /** Accept and route one unified send item. */ send( - content: ContentBlock[], + input: UserMessageData, options: SendOptions, ): AgentMessageId { - const { target, wakeup, source } = options + const { content, source } = input + const { target, wakeup } = options const id = AgentMessageId(randomUUID()) if (target === 'next-step' && !wakeup) { if (this.turnOpen) { @@ -120,29 +118,26 @@ export class ReactLoopAgent implements Agent { } /** Queue one ordinary prompt turn and wake the driver. */ - followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId { - return this.send(content, { + followup(input: UserMessageData): AgentMessageId { + return this.send(input, { target: 'next-turn', wakeup: true, - source: options?.source ?? { kind: 'user' }, }) } /** Steer the open turn, falling back to a waking prompt while idle. */ - steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId { - return this.send(content, { + steer(input: UserMessageData): AgentMessageId { + return this.send(input, { target: 'next-step', wakeup: true, - source: options?.source ?? { kind: 'user' }, }) } /** Append model-facing context without waking the driver. */ - inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId { - return this.send(content, { + inject(input: UserMessageData): AgentMessageId { + return this.send(input, { target: 'next-step', wakeup: false, - source: options?.source ?? { kind: 'plugin', plugin: '' }, }) } diff --git a/packages/core/agent-loop/src/tool-calls.ts b/packages/core/agent-loop/src/tool-calls.ts index 71164cef05..0ae7b5c3a1 100644 --- a/packages/core/agent-loop/src/tool-calls.ts +++ b/packages/core/agent-loop/src/tool-calls.ts @@ -11,8 +11,7 @@ import type { Context } from 'cordis' import { assertNever, type ToolCallBlock } from '@deepseek-ai/dsh-llm' -import type { AdditionalContext } from '@deepseek-ai/dsh-agent' -import type { Session } from '@deepseek-ai/dsh-session' +import type { Session, UserMessageData } from '@deepseek-ai/dsh-session' import { TOOL_ABORTED_BEFORE_DISPATCH, TOOL_REGISTRY_SCHEDULER, type ToolExecutionInput, type ToolExecutionMode, type ToolExecutionResult, type ToolRunContext } from '@deepseek-ai/dsh-tools' /** One tool call after argument parsing, ready to schedule. */ @@ -58,7 +57,7 @@ export async function executeToolCalls( step: number, toolCalls: ToolCallBlock[], signal: AbortSignal, - acceptContext: (context: AdditionalContext) => void, + acceptContext: (context: UserMessageData) => void, ): Promise<{ concluded: boolean }> { const agent = ctx.agents.requireInitiator() const { session } = agent @@ -120,7 +119,7 @@ async function runGroup( group: PlannedCall[], mode: ToolExecutionMode['kind'], signal: AbortSignal, - acceptContext: (context: AdditionalContext) => void, + acceptContext: (context: UserMessageData) => void, ): Promise { const { session } = ctx.agents.requireInitiator() const { maxParallelToolCalls } = ctx.agentLoop.config diff --git a/packages/core/agent-loop/tests/agent-initiator.spec.ts b/packages/core/agent-loop/tests/agent-initiator.spec.ts index 9991bdf8f2..4d45203b3f 100644 --- a/packages/core/agent-loop/tests/agent-initiator.spec.ts +++ b/packages/core/agent-loop/tests/agent-initiator.spec.ts @@ -41,7 +41,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise { } function send(agent: Agent, text: string): void { - agent.followup([{ type: 'text', text }]) + agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) } /** Adapter that holds both drivers at the same awaited continuation. */ diff --git a/packages/core/agent-loop/tests/agent.spec.ts b/packages/core/agent-loop/tests/agent.spec.ts index b2cc302ee6..e0aa4467dc 100644 --- a/packages/core/agent-loop/tests/agent.spec.ts +++ b/packages/core/agent-loop/tests/agent.spec.ts @@ -21,7 +21,7 @@ async function harness(adapter: MockAdapter): Promise { } function send(agent: Agent, text: string): void { - agent.followup([{ type: 'text', text }]) + agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) } describe('Agent', () => { @@ -32,7 +32,7 @@ describe('Agent', () => { let flushes = 0 ctx.on('session/flush', () => { flushes += 1 }) - agent.inject([{ type: 'text', text: 'context' }], { source: { kind: 'plugin', plugin: 'p' } }) + agent.inject({ content: [{ type: 'text', text: 'context' }], source: { kind: 'plugin', plugin: 'p' } }) expect(agent.session.events.map(event => event.type)).toEqual(['user/message']) expect(agent.status).toBe('idle') @@ -41,11 +41,11 @@ describe('Agent', () => { expect(flushes).toBe(0) }) - it('inject() defaults its source to an empty plugin, never user', async () => { + it('inject() preserves an explicitly empty plugin source', async () => { const ctx = await harness(new MockAdapter([textResponse('ok')])) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.inject([{ type: 'text', text: 'no explicit source' }]) + agent.inject({ content: [{ type: 'text', text: 'empty plugin source' }], source: { kind: 'plugin', plugin: '' } }) const injected = agent.session.events.at(-1) expect(injected?.type === 'user/message' && injected.data.source) @@ -57,10 +57,7 @@ describe('Agent', () => { const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) expect(() => { - agent.inject( - [{ type: 'text', text: 'x', bad: 1n } as never], - { source: { kind: 'plugin', plugin: 'p' } }, - ) + agent.inject({ content: [{ type: 'text', text: 'x', bad: 1n } as never], source: { kind: 'plugin', plugin: 'p' } }) }).toThrow(/non-JSON-serializable/) expect(agent.session.events).toHaveLength(0) }) @@ -70,10 +67,7 @@ describe('Agent', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.steer( - [{ type: 'text', text: 'steer idle' }], - { source: { kind: 'plugin', plugin: 'test' } }, - ) + agent.steer({ content: [{ type: 'text', text: 'steer idle' }], source: { kind: 'plugin', plugin: 'test' } }) await agent.whenIdle() expect(agent.session.events.some(event => event.type === 'user/message')).toBe(true) diff --git a/packages/core/agent-loop/tests/cancel.spec.ts b/packages/core/agent-loop/tests/cancel.spec.ts index 882bc0957f..a9fe5d835b 100644 --- a/packages/core/agent-loop/tests/cancel.spec.ts +++ b/packages/core/agent-loop/tests/cancel.spec.ts @@ -33,7 +33,7 @@ async function harness(adapter: MockAdapter) { } function send(agent: Agent, text: string) { - agent.followup([{ type: 'text', text }]) + agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) } /** Resolve on the agent's next idle transition (event-based, not status poll). */ @@ -63,7 +63,7 @@ describe('Agent.cancel()', () => { ctx.on('agent/cancel-requested', (subject, cause) => { if (subject !== agent) return seen.push(`first:${cause.kind}`) - subject.followup([{ type: 'text', text: 'queued by cancel observer' }]) + subject.followup({ content: [{ type: 'text', text: 'queued by cancel observer' }], source: { kind: 'user' } }) throw new Error('observer failed') }) ctx.on('agent/cancel-requested', (subject, cause) => { @@ -106,11 +106,7 @@ describe('Agent.cancel()', () => { ctx.on('agent/inbox/discard', (subject, items) => { if (subject === agent) discards.push(items) }) // Queue a turn WITHOUT waking the driver, so it sits in the inbox. - agent.send([{ type: 'text', text: 'preserved' }], { - target: 'next-turn', - wakeup: false, - source: { kind: 'user' }, - }) + agent.send({ content: [{ type: 'text', text: 'preserved' }], source: { kind: 'user' } }, { target: 'next-turn', wakeup: false }) // keepInbox cancel: no active turn, work preserved, no discard event. agent.cancel({ kind: 'user' }, { keepInbox: true }) expect(discards).toEqual([]) @@ -128,11 +124,7 @@ describe('Agent.cancel()', () => { // A quiet item alone must NOT wake the driver: no turn runs and whenIdle // resolves (the agent is quiescent), leaving the item queued. - agent.send([{ type: 'text', text: 'quiet' }], { - target: 'next-turn', - wakeup: false, - source: { kind: 'user' }, - }) + agent.send({ content: [{ type: 'text', text: 'quiet' }], source: { kind: 'user' } }, { target: 'next-turn', wakeup: false }) await agent.whenIdle() expect(agent.status).toBe('idle') expect(agent.session.events.some(e => e.type === 'turn/start')).toBe(false) @@ -148,11 +140,7 @@ describe('Agent.cancel()', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.send([{ type: 'text', text: 'quiet' }], { - target: 'next-turn', - wakeup: false, - source: { kind: 'user' }, - }) + agent.send({ content: [{ type: 'text', text: 'quiet' }], source: { kind: 'user' } }, { target: 'next-turn', wakeup: false }) const idle = agent.whenIdle() // Cancel reaches quiescence with no status transition and no waking send; // whenIdle must still resolve (previously it hung until the next send). @@ -587,7 +575,7 @@ describe('Agent.cancel()', () => { expect(agent.status).toBe('running') // Steer (joins the running turn's steering FIFO), then cancel: the steering // must be dropped, NOT re-enqueued as a new queued turn. - agent.steer([{ type: 'text', text: 'steer text' }]) + agent.steer({ content: [{ type: 'text', text: 'steer text' }], source: { kind: 'user' } }) agent.cancel({ kind: 'user' }) await waitForIdle(ctx, agent) diff --git a/packages/core/agent-loop/tests/config-session-id.spec.ts b/packages/core/agent-loop/tests/config-session-id.spec.ts index 15df16d2cf..00e9ded5cc 100644 --- a/packages/core/agent-loop/tests/config-session-id.spec.ts +++ b/packages/core/agent-loop/tests/config-session-id.spec.ts @@ -98,7 +98,7 @@ describe('config-driven session id', () => { first = ctx.agents.get(SessionId('config-exact-reload')) } expect(first).toBeDefined() - first!.followup([{ type: 'text', text: 'remember me' }], { source: { kind: 'user' } }) + first!.followup({ content: [{ type: 'text', text: 'remember me' }], source: { kind: 'user' } }) await waitForIdle(ctx, first!) await firstLoop.dispose() @@ -110,7 +110,7 @@ describe('config-driven session id', () => { } expect(second).toBeDefined() expect(JSON.stringify(second!.session.deriveMessages())).toContain('remember me') - second!.followup([{ type: 'text', text: 'continue' }], { source: { kind: 'user' } }) + second!.followup({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'user' } }) await waitForIdle(ctx, second!) await ctx.sessions.flush(second!.session) const loaded = await ctx.sessionPersistence.load(SessionId('config-exact-reload')) @@ -137,9 +137,7 @@ describe('config-driven session id', () => { cleanupStarted.resolve(undefined) await cleanupGate.promise }) - first.inject([{ type: 'text', text: 'persist before replacement' }], { - source: { kind: 'plugin', plugin: 'test' }, - }) + first.inject({ content: [{ type: 'text', text: 'persist before replacement' }], source: { kind: 'plugin', plugin: 'test' } }) await ctx.sessions.flush(first.session) expect(JSON.stringify((await ctx.sessionPersistence.inspect(sessionId)).events)) .toContain('persist before replacement') @@ -183,9 +181,7 @@ describe('config-driven session id', () => { cleanupStarted.resolve(undefined) await cleanupGate.promise }) - first.inject([{ type: 'text', text: 'persist before cancellation' }], { - source: { kind: 'plugin', plugin: 'test' }, - }) + first.inject({ content: [{ type: 'text', text: 'persist before cancellation' }], source: { kind: 'plugin', plugin: 'test' } }) await ctx.sessions.flush(first.session) expect(JSON.stringify((await ctx.sessionPersistence.inspect(sessionId)).events)) .toContain('persist before cancellation') @@ -349,7 +345,7 @@ describe('config-driven session id', () => { expect(a1.id).toBe(a1.session.id) expect(a1.session.id).toMatch(idPattern) expect(ctx1.agents.get(SessionId('cfg'))).toBeUndefined() - a1.followup([{ type: 'text', text: 'q' }], { source: { kind: 'user' } }) + a1.followup({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }) await waitForIdle(ctx1, a1) await ctx1.fiber.dispose() @@ -368,7 +364,7 @@ describe('config-driven session id', () => { expect(a2.id).toBe(a2.session.id) expect(a2.session.id).toMatch(idPattern) expect(a2.session.id).not.toBe(a1.session.id) - a2.followup([{ type: 'text', text: 'q2' }], { source: { kind: 'user' } }) + a2.followup({ content: [{ type: 'text', text: 'q2' }], source: { kind: 'user' } }) await waitForIdle(ctx2, a2) await ctx2.fiber.dispose() }) @@ -389,7 +385,7 @@ describe('config-driven session id', () => { await ctx1.plugin(SessionPersistenceJsonl, { root }) ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('first')])) const a1 = (await ctx1.agents.create({ sessionId: SessionId('sticky-1') })).agent - a1.followup([{ type: 'text', text: 'remember me' }], { source: { kind: 'user' } }) + a1.followup({ content: [{ type: 'text', text: 'remember me' }], source: { kind: 'user' } }) await waitForIdle(ctx1, a1) await ctx1.fiber.dispose() diff --git a/packages/core/agent-loop/tests/contract-regressions.spec.ts b/packages/core/agent-loop/tests/contract-regressions.spec.ts index 78574c693d..f6629571d7 100644 --- a/packages/core/agent-loop/tests/contract-regressions.spec.ts +++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts @@ -50,7 +50,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise { } function send(agent: Agent, text: string) { - agent.followup([{ type: 'text', text }]) + agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) } describe('assistant replay provenance', () => { @@ -85,7 +85,7 @@ describe('abort during tool execution ends the turn', () => { description: '', parameters: {}, async execute() { - agent.inject([{ type: 'text', text: 'accepted before abort' }], { source: { kind: 'plugin', plugin: 'test' } }) + agent.inject({ content: [{ type: 'text', text: 'accepted before abort' }], source: { kind: 'plugin', plugin: 'test' } }) agent.cancel({ kind: 'user' }) return [{ type: 'text', text: 'done' }] }, @@ -185,7 +185,7 @@ describe('abort during tool execution ends the turn', () => { description: '', parameters: {}, async execute(_args, exec) { - agent.inject([{ type: 'text', text: 'accepted before disposal' }], { source: { kind: 'plugin', plugin: 'test' } }) + agent.inject({ content: [{ type: 'text', text: 'accepted before disposal' }], source: { kind: 'plugin', plugin: 'test' } }) started.resolve(undefined) const signal = exec.signal if (!signal) throw new Error('tool execution signal is missing') @@ -254,7 +254,7 @@ describe('abort during tool execution ends the turn', () => { await waitForIdle(ctx, agent) ctx.on('agent/step', (subject, turn) => { if (subject === agent && turn === 2) { - agent.inject([{ type: 'text', text: 'new turn context' }], { source: { kind: 'plugin', plugin: 'test' } }) + agent.inject({ content: [{ type: 'text', text: 'new turn context' }], source: { kind: 'plugin', plugin: 'test' } }) } }) send(agent, 'start a text-only turn') @@ -282,7 +282,7 @@ describe('steering from late extension points is never stranded', () => { ctx.on('agent/stopping', () => { if (!steeredOnce) { steeredOnce = true - agent.steer([{ type: 'text', text: 'one more thing' }]) + agent.steer({ content: [{ type: 'text', text: 'one more thing' }], source: { kind: 'user' } }) } }) @@ -307,7 +307,7 @@ describe('steering from late extension points is never stranded', () => { ctx.on('session/event', (subject, event) => { if (subject !== agent.session || event.type !== 'step/end' || steeredOnce) return steeredOnce = true - agent.steer([{ type: 'text', text: 'goal reminder from step/end' }]) + agent.steer({ content: [{ type: 'text', text: 'goal reminder from step/end' }], source: { kind: 'user' } }) }) send(agent, 'go') @@ -338,7 +338,7 @@ describe('steering from late extension points is never stranded', () => { if (event.type === 'turn/start') turns.push(event.data.turn) if (event.type === 'turn/end' && !steeredOnce) { steeredOnce = true - agent.steer([{ type: 'text', text: 'too late for this turn' }]) + agent.steer({ content: [{ type: 'text', text: 'too late for this turn' }], source: { kind: 'user' } }) } }) @@ -493,7 +493,7 @@ describe('adapter registration, routing, and accepted-input ownership', () => { description: '', parameters: {}, async execute() { - agent.steer([{ type: 'text', text: 's' }], { source: { kind: 'plugin', plugin: 'goal' } }) + agent.steer({ content: [{ type: 'text', text: 's' }], source: { kind: 'plugin', plugin: 'goal' } }) return [] }, })) @@ -550,7 +550,7 @@ describe('turn numbering continues across seeded sessions', () => { const turns: number[] = [] ctx2.on('session/event', (_s, event) => { if (event.type === 'turn/start') turns.push(event.data.turn) }) - forked.followup([{ type: 'text', text: 'continue' }]) + forked.followup({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'user' } }) await new Promise((resolve) => { ctx2.on('agent/status', (subject, status) => { if (subject === forked && status === 'idle') resolve() diff --git a/packages/core/agent-loop/tests/coverage-edges.spec.ts b/packages/core/agent-loop/tests/coverage-edges.spec.ts index df26054294..4a0cc18d01 100644 --- a/packages/core/agent-loop/tests/coverage-edges.spec.ts +++ b/packages/core/agent-loop/tests/coverage-edges.spec.ts @@ -38,7 +38,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise { } function send(agent: Agent, text: string) { - agent.followup([{ type: 'text', text }]) + agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) } describe('tool JSON parse', () => { diff --git a/packages/core/agent-loop/tests/interception.spec.ts b/packages/core/agent-loop/tests/interception.spec.ts index 7a08a169ba..dd2eb53b05 100644 --- a/packages/core/agent-loop/tests/interception.spec.ts +++ b/packages/core/agent-loop/tests/interception.spec.ts @@ -42,7 +42,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise { } function send(agent: Agent, text: string) { - agent.followup([{ type: 'text', text }]) + agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) } function events(agent: Agent): SessionEvent[] { @@ -151,7 +151,7 @@ describe('agent/prompt-submit', () => { const reasons: TurnEndReason[] = [] ctx.on('session/event', (_s, event: SessionEvent) => { if (event.type === 'turn/end') reasons.push(event.data.reason) }) - agent.followup([{ type: 'text', text: 'do something' }]) + agent.followup({ content: [{ type: 'text', text: 'do something' }], source: { kind: 'user' } }) await agent.whenIdle() // the model was never called @@ -252,7 +252,7 @@ describe('agent/session-start', () => { const ctx = await harness(adapter) ctx.on('agent/session-start', (agent) => { - agent.inject([{ type: 'text', text: 'session preamble' }], { source: { kind: 'plugin', plugin: 'test' } }) + agent.inject({ content: [{ type: 'text', text: 'session preamble' }], source: { kind: 'plugin', plugin: 'test' } }) }) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) @@ -396,10 +396,7 @@ describe('worked example: a native hook plugin is just a cordis plugin on the se apply(ctx: Context) { // 1. SessionStart: seed a standing instruction. ctx.on('agent/session-start', (agent, source) => { - agent.inject( - [{ type: 'text', text: `policy active (started: ${source})` }], - { source: { kind: 'plugin', plugin: 'native-guard' } }, - ) + agent.inject({ content: [{ type: 'text', text: `policy active (started: ${source})` }], source: { kind: 'plugin', plugin: 'native-guard' } }) }) // 2. PromptSubmit: block a forbidden prompt, annotate the rest. ctx.on('agent/prompt-submit', async (_agent, content, _source, _signal, next): Promise => { diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts index 78025c4159..b647ad99dd 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -42,7 +42,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise { } function send(agent: Agent, text: string) { - agent.followup([{ type: 'text', text }]) + agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) } describe('agent loop', () => { @@ -271,7 +271,7 @@ describe('agent loop', () => { parameters: {}, async execute() { // steer while the turn is running (during tool execution) - agent.steer([{ type: 'text', text: 'change of plans' }]) + agent.steer({ content: [{ type: 'text', text: 'change of plans' }], source: { kind: 'user' } }) return [{ type: 'text', text: 'tool done' }] }, })) @@ -299,8 +299,8 @@ describe('agent loop', () => { const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) const idle = waitForIdle(ctx, agent) - agent.steer([{ type: 'text', text: 'first idle steer' }]) - agent.steer([{ type: 'text', text: 'second idle steer' }]) + agent.steer({ content: [{ type: 'text', text: 'first idle steer' }], source: { kind: 'user' } }) + agent.steer({ content: [{ type: 'text', text: 'second idle steer' }], source: { kind: 'user' } }) await idle expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(2) @@ -321,7 +321,7 @@ describe('agent loop', () => { ctx.on('agent/step', (subject) => { if (subject !== agent || !fail) return fail = false - subject.steer([{ type: 'text', text: 'pending steering' }]) + subject.steer({ content: [{ type: 'text', text: 'pending steering' }], source: { kind: 'user' } }) throw new Error('step failed') }) @@ -347,7 +347,7 @@ describe('agent loop', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.inject([{ type: 'text', text: 'file changed: a.ts' }], { source: { kind: 'plugin', plugin: 'watcher' } }) + agent.inject({ content: [{ type: 'text', text: 'file changed: a.ts' }], source: { kind: 'plugin', plugin: 'watcher' } }) expect(agent.status).toBe('idle') expect(adapter.requests).toHaveLength(0) expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(0) @@ -369,9 +369,7 @@ describe('agent loop', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('raw-context'), { provider: 'mock', model: 'mock' }) const text = 'Additional instructions from: pkg/AGENTS.md' - agent.inject([{ type: 'text', text }], { - source: { kind: 'plugin', plugin: 'workspace-context' }, - }) + agent.inject({ content: [{ type: 'text', text }], source: { kind: 'plugin', plugin: 'workspace-context' } }) send(agent, 'go') await waitForIdle(ctx, agent) @@ -398,11 +396,9 @@ describe('agent loop', () => { async execute() { await Promise.resolve() const first = { type: 'text' as const, text: 'mid-turn notice' } - agent.inject([first], { - source: { kind: 'plugin', plugin: 'x' }, - }) + agent.inject({ content: [first], source: { kind: 'plugin', plugin: 'x' } }) first.text = 'mutated after inject' - agent.inject([{ type: 'text', text: 'second notice' }], { source: { kind: 'plugin', plugin: 'x' } }) + agent.inject({ content: [{ type: 'text', text: 'second notice' }], source: { kind: 'plugin', plugin: 'x' } }) visibleDuringTool = agent.session.events.some(e => e.type === 'user/message' && e.data.source.kind === 'plugin') return [{ type: 'text', text: 'ok' }] }, @@ -455,9 +451,7 @@ describe('agent loop', () => { parameters: {}, async execute() { expect(() => { - agent.inject([{ type: 'text', text: 'invalid' }], { - source: { kind: 'plugin', plugin: 'test', bigint: 1n } as never, - }) + agent.inject({ content: [{ type: 'text', text: 'invalid' }], source: { kind: 'plugin', plugin: 'test', bigint: 1n } as never }) }).toThrow('agent context must be losslessly JSON-serializable') return [{ type: 'text', text: 'rejected invalid context' }] }, @@ -482,9 +476,7 @@ describe('agent loop', () => { ctx.on('session/event', (_session, event) => { if (event.type === 'step/end') steps++ }) ctx.on('agent/stopping', (subject) => { if (steps < 3) { - subject.steer([{ type: 'text', text: 'continue' }], { - source: { kind: 'plugin', plugin: 'loop-test' }, - }) + subject.steer({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'plugin', plugin: 'loop-test' } }) } }) @@ -692,9 +684,7 @@ describe('agent loop', () => { // (step 2 is a plain stop with no tool calls → stops). ctx.on('agent/stopping', (subject) => { if (steps < 2) { - subject.steer([{ type: 'text', text: 'continue after truncation' }], { - source: { kind: 'plugin', plugin: 'max-tokens-test' }, - }) + subject.steer({ content: [{ type: 'text', text: 'continue after truncation' }], source: { kind: 'plugin', plugin: 'max-tokens-test' } }) } }) @@ -920,12 +910,9 @@ describe('agent loop', () => { const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) const idle = waitForIdle(ctx, agent) - agent.followup([{ type: 'text', text: 'user message' }]) + agent.followup({ content: [{ type: 'text', text: 'user message' }], source: { kind: 'user' } }) await Promise.resolve() - agent.followup( - [{ type: 'text', text: 'plugin message' }], - { source: { kind: 'plugin', plugin: 'test' } }, - ) + agent.followup({ content: [{ type: 'text', text: 'plugin message' }], source: { kind: 'plugin', plugin: 'test' } }) await idle const triggers = agent.session.events diff --git a/packages/core/agent-loop/tests/properties.spec.ts b/packages/core/agent-loop/tests/properties.spec.ts index 05674e4b7d..101c07b656 100644 --- a/packages/core/agent-loop/tests/properties.spec.ts +++ b/packages/core/agent-loop/tests/properties.spec.ts @@ -115,7 +115,7 @@ describe('agent loop scheduling properties', () => { const { seen: trace } = recordStatus(ctx, agent) const idle = nextIdle(ctx, agent) // Send all in one synchronous tick: they queue before the loop wakes. - for (const text of texts) agent.followup([{ type: 'text', text }]) + for (const text of texts) agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) await idle // No message lost: every send appears as a user/message, in order. @@ -142,7 +142,7 @@ describe('agent loop scheduling properties', () => { const agent = ctx.agentLoop.create(SessionId('a'), { provider: 'mock', model: 'mock' }) for (const text of texts) { const idle = nextIdle(ctx, agent) - agent.followup([{ type: 'text', text }]) + agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) await idle } // Each send was drained at a separate turn start: N turns, 1..N. @@ -171,7 +171,7 @@ describe('agent loop scheduling properties', () => { for (const step of steps) { const idle = nextIdle(ctx, agent) lastIdle = idle - agent.followup([{ type: 'text', text: step.text }]) + agent.followup({ content: [{ type: 'text', text: step.text }], source: { kind: 'user' } }) if (step.settle) await idle } await lastIdle diff --git a/packages/core/agent-loop/tests/request-cache.e2e.ts b/packages/core/agent-loop/tests/request-cache.e2e.ts index 37079b8565..cddcb0a8f9 100644 --- a/packages/core/agent-loop/tests/request-cache.e2e.ts +++ b/packages/core/agent-loop/tests/request-cache.e2e.ts @@ -73,10 +73,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('log-derived request cache hits ( const agent = ctx.agentLoop.create(SessionId('cache-e2e'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) // Turn 1: forces a tool call → at least two steps (two model requests). - agent.followup([{ type: 'text', text: 'Look up the key "deploy-color" with the lookup tool and tell me the value.' }]) + agent.followup({ content: [{ type: 'text', text: 'Look up the key "deploy-color" with the lookup tool and tell me the value.' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // Turn 2: a follow-up over the same (longer) prefix. - agent.followup([{ type: 'text', text: 'Thanks. Repeat that value one more time.' }]) + agent.followup({ content: [{ type: 'text', text: 'Thanks. Repeat that value one more time.' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const usages = [...agent.session.events] diff --git a/packages/core/agent-loop/tests/request-error.spec.ts b/packages/core/agent-loop/tests/request-error.spec.ts index cd123cd311..ac95a261e9 100644 --- a/packages/core/agent-loop/tests/request-error.spec.ts +++ b/packages/core/agent-loop/tests/request-error.spec.ts @@ -40,7 +40,7 @@ describe('agent/request-error', () => { recoveries += 1 }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await agent.whenIdle() expect(recoveries).toBe(0) @@ -75,7 +75,7 @@ describe('agent/request-error', () => { subject.retry() }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await agent.whenIdle() expect(seen.map(item => ({ @@ -113,7 +113,7 @@ describe('agent/request-error', () => { subject.cancel({ kind: 'user' }) }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await agent.whenIdle() expect(adapter.requests).toHaveLength(1) @@ -136,7 +136,7 @@ describe('agent/request-error', () => { throw new Error('recovery failed') }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await agent.whenIdle() expect(adapter.requests).toHaveLength(1) diff --git a/packages/core/agent-loop/tests/request-reconstruction.spec.ts b/packages/core/agent-loop/tests/request-reconstruction.spec.ts index c40443a872..4f7f86092e 100644 --- a/packages/core/agent-loop/tests/request-reconstruction.spec.ts +++ b/packages/core/agent-loop/tests/request-reconstruction.spec.ts @@ -41,7 +41,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise { } function send(agent: Agent, text: string) { - agent.followup([{ type: 'text', text }]) + agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) } /** Assert `previous` is a strict value-prefix of `current`. */ @@ -170,7 +170,7 @@ describe('request stability across the loop', () => { ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => { if (!injected) { injected = true - agent.inject([{ type: 'text', text: '[late context]' }], { source: { kind: 'plugin', plugin: 'test' } }) + agent.inject({ content: [{ type: 'text', text: '[late context]' }], source: { kind: 'plugin', plugin: 'test' } }) } return next() }) diff --git a/packages/core/agent-loop/tests/resume.spec.ts b/packages/core/agent-loop/tests/resume.spec.ts index c9a438e2ab..8a8d604773 100644 --- a/packages/core/agent-loop/tests/resume.spec.ts +++ b/packages/core/agent-loop/tests/resume.spec.ts @@ -146,7 +146,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume', const adapter1 = new MockAdapter([textResponse('a')]) const { ctx: ctx1, root } = await persistentHarness(adapter1) const a1 = (await ctx1.agents.create({ sessionId: SessionId('nocwd-sess') })).agent - a1.followup([{ type: 'text', text: 'q' }], { source: { kind: 'user' } }) + a1.followup({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }) await waitForIdle(ctx1, a1) await ctx1.fiber.dispose() @@ -174,7 +174,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume', ctx1.on('agent/session-start', (_agent, source) => void sources1.push(source)) const a1 = (await ctx1.agents.create({ sessionId: SessionId('start-sess') })).agent expect(sources1).toEqual(['startup']) - a1.followup([{ type: 'text', text: 'q' }], { source: { kind: 'user' } }) + a1.followup({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }) await waitForIdle(ctx1, a1) await ctx1.fiber.dispose() @@ -475,9 +475,9 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume', const adapter1 = new MockAdapter([textResponse('answer')]) const { ctx: ctx1, root } = await persistentHarness(adapter1) const a1 = (await ctx1.agents.create({ sessionId: SessionId('inject-sess'), meta: { cwd: '/w' } })).agent - a1.followup([{ type: 'text', text: 'q' }], { source: { kind: 'user' } }) + a1.followup({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }) await waitForIdle(ctx1, a1) - a1.inject([{ type: 'text', text: 'background task 42 finished' }], { source: { kind: 'plugin', plugin: 'tool-bash' } }) + a1.inject({ content: [{ type: 'text', text: 'background task 42 finished' }], source: { kind: 'plugin', plugin: 'tool-bash' } }) await a1.whenIdle() await ctx1.fiber.dispose() @@ -503,7 +503,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume', const adapter1 = new MockAdapter([textResponse('first answer')]) const { ctx: ctx1, root } = await persistentHarness(adapter1) const a1 = (await ctx1.agents.create({ sessionId: SessionId('sess-resume'), meta: { cwd: '/w' } })).agent - a1.followup([{ type: 'text', text: 'first question' }], { source: { kind: 'user' } }) + a1.followup({ content: [{ type: 'text', text: 'first question' }], source: { kind: 'user' } }) await waitForIdle(ctx1, a1) const events1 = [...a1.session.events] const seqs1 = events1.map(e => e.seq) @@ -530,7 +530,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume', expect(a2.session.deriveMessages()).toEqual(replay.deriveMessages()) // …and a new turn continues numbering (turn 2) with contiguous seqs. - a2.followup([{ type: 'text', text: 'second question' }], { source: { kind: 'user' } }) + a2.followup({ content: [{ type: 'text', text: 'second question' }], source: { kind: 'user' } }) await waitForIdle(ctx2, a2) const allSeqs = a2.session.events.map(e => e.seq) expect(allSeqs).toEqual(allSeqs.map((_, i) => i)) // 0..N contiguous, no duplicates diff --git a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts index cccd72123a..9f69f5ff6d 100644 --- a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts +++ b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts @@ -203,11 +203,11 @@ describe('agent scope lifecycle', () => { if (event.type === 'user/message') heard.push('a-sees:user-message') }) - b.followup(text('for b')) + b.followup({ content: text('for b'), source: { kind: 'user' } }) await waitForIdle(ctx, b) expect(heard).toEqual([]) // nothing of b's leaked into a's scope - a.followup(text('for a')) + a.followup({ content: text('for a'), source: { kind: 'user' } }) await waitForIdle(ctx, a) expect(heard).toContain('a-sees:a:running') expect(heard).toContain('a-sees:user-message') @@ -934,7 +934,7 @@ describe('agent scope lifecycle', () => { if (event.type === 'turn/start') { off(); resolve() } }) }) - agent.followup(text('work')) + agent.followup({ content: text('work'), source: { kind: 'user' } }) await turnOpen await owner.dispose() expect(order).toEqual([ diff --git a/packages/core/agent-loop/tests/tool-calls.spec.ts b/packages/core/agent-loop/tests/tool-calls.spec.ts index 16abfa89e8..66af3e10be 100644 --- a/packages/core/agent-loop/tests/tool-calls.spec.ts +++ b/packages/core/agent-loop/tests/tool-calls.spec.ts @@ -105,7 +105,7 @@ describe('tool-call scheduler: grouping and barriers', () => { ctx.tools.register(gated.tool) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 3) expect(gated.started).toEqual(['1', '2', '3']) gated.release('1'); gated.release('2'); gated.release('3') @@ -133,7 +133,7 @@ describe('tool-call scheduler: grouping and barriers', () => { async execute(args) { order.push(`w-${args.id}`); return [{ type: 'text', text: 'w' }] }, })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(order).toEqual(['r-start-A1', 'r-end-A1', 'w-A2', 'r-start-A3', 'r-end-A3']) @@ -169,7 +169,7 @@ describe('tool-call scheduler: grouping and barriers', () => { })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => replacement.started.length === 1) await new Promise(r => setTimeout(r, 5)) expect(replacement.started).toEqual(['1']) @@ -200,7 +200,7 @@ describe('tool-call scheduler: grouping and barriers', () => { }) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => initial.started.length === 2) initial.release('1') await until(() => events(agent).some(event => @@ -226,7 +226,7 @@ describe('tool-call scheduler: model-order results despite out-of-order settleme ctx.tools.register(gated.tool) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 2) gated.release('2') await new Promise(r => setTimeout(r, 5)) @@ -248,7 +248,7 @@ describe('tool-call scheduler: model-order results despite out-of-order settleme const gated = gatedParallelTool('p') ctx.tools.register(gated.tool) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 2) gated.release('2'); gated.release('1') await waitForIdle(ctx, agent) @@ -295,7 +295,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () => ctx.tools.register(gated.tool) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 2) await new Promise(r => setTimeout(r, 5)) expect(gated.started).toEqual(['1', '2']) @@ -324,7 +324,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () => const gated = gatedParallelTool('p') ctx.tools.register(gated.tool) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 1) await new Promise(r => setTimeout(r, 5)) expect(gated.started).toEqual(['1']) @@ -350,7 +350,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () => const gated = gatedParallelTool('p') ctx.tools.register(gated.tool) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 1) await new Promise(r => setTimeout(r, 5)) expect(gated.started).toEqual(['1']) @@ -377,7 +377,7 @@ describe('tool-call scheduler: ordered middleware and additional contexts', () = ctx.on('tools/post-execute', async (exec, _result, next): Promise => { post.push(String(exec.callId)); return next() }) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 3) gated.release('3'); gated.release('2'); gated.release('1') await waitForIdle(ctx, agent) @@ -398,7 +398,7 @@ describe('tool-call scheduler: ordered middleware and additional contexts', () = ({ kind: 'accept', additionalContexts: [{ content: [{ type: 'text', text: `ctx-${exec.callId}` }], source: { kind: 'plugin', plugin: 'p' } }] })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 2) gated.release('2'); gated.release('1') await waitForIdle(ctx, agent) @@ -436,7 +436,7 @@ describe('tool-call scheduler: ordered middleware and additional contexts', () = }) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 1) gated.release('1') await waitForIdle(ctx, agent) @@ -466,7 +466,7 @@ describe('tool-call scheduler: abort handling', () => { } }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(gated.started).toEqual([]) @@ -498,7 +498,7 @@ describe('tool-call scheduler: abort handling', () => { return next() }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(gated.started).toEqual([]) @@ -528,7 +528,7 @@ describe('tool-call scheduler: abort handling', () => { })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 2) agent.cancel({ kind: 'user' }) gated.release('1') @@ -575,7 +575,7 @@ describe('tool-call scheduler: abort handling', () => { })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await until(() => gated.started.length === 2) agent.cancel({ kind: 'user' }) gated.release('1') diff --git a/packages/core/agent-loop/tests/tool-order.spec.ts b/packages/core/agent-loop/tests/tool-order.spec.ts index 30384e7b0d..4ceb57a036 100644 --- a/packages/core/agent-loop/tests/tool-order.spec.ts +++ b/packages/core/agent-loop/tests/tool-order.spec.ts @@ -58,7 +58,7 @@ async function runTurn(registrationOrder: string[], toolOrder?: SystemPromptConf const ctx = await harness(adapter, toolOrder) for (const name of registrationOrder) registerNamed(ctx, name) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) return { ctx, agent, adapter } } @@ -102,7 +102,7 @@ describe('loop-level canonical tool order', () => { if (error instanceof Error) errors.push(error) }) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(0) expect(errors.map(e => e.message)).toEqual(['toolOrder lists unregistered tool "ghost"; known tools: alpha']) diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 86796fa9f0..136853442b 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -56,10 +56,10 @@ Turn and step boundaries and the model token stream are durable `session/event` The handle every plugin programs against: -- `agent.send(content, options)` — the one delivery primitive over the (`target` × `wakeup`) matrix; `Agent` is an abstract class whose `followup`/`steer`/`inject` aliases are fixed-preset delegates to it. It returns the accepted message's opaque `AgentMessageId`, which the message's `agent/inbox/enqueue`/`dequeue`/`discard` events carry so a caller can correlate a queued item with its lifecycle. `SendOptions` requires `target`, `wakeup`, and `source`; callers wanting the ordinary user-message preset use `followup(content)`. `target: 'next-turn'` queues one independent FIFO item that, if admitted, becomes the sole ordinary prompt in its turn. `target: 'next-step'` with `wakeup: true` submits steering, while `target: 'next-step'` with `wakeup: false` injects durable context without running the model. The [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md) owns the turn rationale. -- `agent.followup(content, options?)` — the `next-turn`/wakeup preset of `send()`: queue an ordinary follow-up turn and wake the driver. -- `agent.steer(content, options?)` — the `next-step`/wakeup preset: while a turn is open, stage steering for its next safe boundary without dispatching `agent/prompt-submit`; when idle, delegate to a woken follow-up. Cancellation or disposal may discard pending steering. -- `agent.inject(content, options?)` — the `next-step`/no-wakeup preset: append model-facing context without running the model; the next request sees a verbatim user-role message whose provenance is carried by `source`. While a turn is open, injection waits in the outbox for the next safe boundary. While idle, it appends immediately without opening a turn; persistence reacts to `session/event` independently. Injection emits no `agent/inbox/*` event. +- `agent.send(input, options)` — the one delivery primitive over the (`target` × `wakeup`) matrix. `input` is the existing `UserMessageData { content, source }`, while `SendOptions` requires only the routing policy `target` and `wakeup`. It returns the accepted message's opaque `AgentMessageId`, which the message's `agent/inbox/enqueue`/`dequeue`/`discard` events carry so a caller can correlate a queued item with its lifecycle. `target: 'next-turn'` queues one independent FIFO item that, if admitted, becomes the sole ordinary prompt in its turn. `target: 'next-step'` with `wakeup: true` submits steering, while `target: 'next-step'` with `wakeup: false` injects durable context without running the model. The [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md) owns the turn rationale. +- `agent.followup(input)` — the `next-turn`/wakeup preset of `send()`: queue an ordinary follow-up turn and wake the driver. +- `agent.steer(input)` — the `next-step`/wakeup preset: while a turn is open, stage steering for its next safe boundary without dispatching `agent/prompt-submit`; when idle, delegate to a woken follow-up. Cancellation or disposal may discard pending steering. +- `agent.inject(input)` — the `next-step`/no-wakeup preset: append model-facing context without running the model; the next request sees a verbatim user-role message whose provenance is carried by the required `input.source`. While a turn is open, injection waits in the outbox for the next safe boundary. While idle, it appends immediately without opening a turn; persistence reacts to `session/event` independently. Injection emits no `agent/inbox/*` event. - `agent.cancel(cause, options?)` — cancel the active turn and, unless `options.keepInbox`, ALL pending work. Callers must choose the `user | parent` cause explicitly; an active holder copies its discriminant into a detached frozen signal reason before aborting. An effective call emits `agent/cancel-requested` with the cause before clearing queued and steering work; dropped items are reported on `agent/inbox/discard`, and observers may synchronize state but cannot veto cancellation. `keepInbox: true` aborts the turn but preserves queued and steering items (no discard, and un-started work is not dropped). The same-process typed seam adds no runtime validation or compatibility fallback for untyped callers. Repeated active-turn cancellation is first-wins for the signal, and idle cancellation is a safe no-op with no notification. ACP maps to `user`, while in-process parent propagation maps to `parent`. The cause is runtime-only; durable `turn/end` stays coarse `aborted`. - `agent.whenIdle()` — resolve once the agent reaches quiescence after settling out of `running` (idle → immediately; disposed → awaits the loop exit). A non-owner's quiescence-observation hook: it observes the work settling WITHOUT tearing the agent down. Teardown is separate — a lifecycle owner stops and unregisters via `AgentHandle.dispose()`, which awaits the loop exit directly. - `agent.session`, `agent.status`, `agent.options`, `agent.id` @@ -78,7 +78,7 @@ The handle every plugin programs against: #### What the model sees -`send`, `steer`, and `inject` feed the owning session. `agent/prompt-submit`, `agent/session-prefix`, and other declared events let plugins block a prompt or add request material; this interface contributes no fixed prose itself. +`send`, `steer`, and `inject` feed the owning session. `agent/prompt-submit`, `agent/step`, and other declared events let plugins block a prompt or add durable request material; this interface contributes no fixed prose itself. #### Token effect @@ -109,5 +109,5 @@ Prefix-stable while an agent's scoped registrations are unchanged. Setup or relo - **Inter-agent channels beyond delegation** — shared state, streaming child output, and background/poll semantics remain outside the current synchronous `ctx.subagents` seam. - **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx)` transaction instead. - **`cancel()` clears the inbox by default** — it aborts the in-flight turn plus queued and steering work; `cancel(cause, { keepInbox: true })` aborts only the turn and preserves pending items. There is still no step-only abort that keeps the in-flight turn running ([stop-surface Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md)). -- **`AdditionalContext` carries exactly one `MessageSource`** — contributions from several plugins merged onto one tool call collapse under one source; mixed provenance is unrepresentable. +- **Each additional `UserMessageData` carries exactly one `MessageSource`** — contributions from several plugins merged onto one tool call collapse under one source; mixed provenance is unrepresentable. - **`SessionStartSource` reserves `'clear'`/`'compact'` with no emitter yet** — only `'startup'`/`'resume'` occur until the driving subsystems land (`TODO(compaction)`). diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts index 1c71d9bb60..511e6e3510 100644 --- a/packages/core/agent/src/types.ts +++ b/packages/core/agent/src/types.ts @@ -40,8 +40,7 @@ export type SendTarget = 'next-turn' | 'next-step' * (`next-turn`/wakeup), {@link Agent.steer} (`next-step`/wakeup), and * {@link Agent.inject} (`next-step`/no-wakeup). * - * The object is complete so routing and provenance are explicit; callers that - * want the ordinary user-message preset use {@link Agent.followup}. + * The object is complete so routing policy is explicit. */ export interface SendOptions { /** Queue the item joins. */ @@ -54,14 +53,6 @@ export interface SendOptions { * (the injection preset). */ wakeup: boolean - /** Producer provenance; direct human input uses `{ kind: 'user' }`. */ - source: MessageSource -} - -/** Options accepted by the fixed-preset aliases, which own `target` and `wakeup`. */ -export interface AliasSendOptions { - /** Producer provenance; each alias supplies its documented default when omitted. */ - source?: MessageSource } /** @@ -82,8 +73,8 @@ export function AgentMessageId(id: string): AgentMessageId { /** * One accepted {@link Agent.send} message, carried by the `agent/inbox/*` live * events. `id` is the value `send` returned to the caller, stable across this - * message's enqueue, dequeue, and discard events. Source defaults are already - * applied, so these are the exact values the item was accepted with. + * message's enqueue, dequeue, and discard events. Its content and source are + * the exact input values accepted by the agent. */ export interface AgentMessage extends UserMessageData { /** The id `send` returned for this message. */ @@ -108,9 +99,6 @@ export interface CancelOptions { */ export type AgentStatus = 'idle' | 'running' -/** Additional model-facing context produced beside a prompt or tool result. */ -export type AdditionalContext = UserMessageData - /** * Prompt interception result. `allow.content` replaces the prompt, while * `additionalContexts` appends model-facing context before the turn starts. @@ -118,7 +106,7 @@ export type AdditionalContext = UserMessageData * `next()` preserves both fields unless it intentionally replaces them. */ export type PromptDecision = - | { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: AdditionalContext[] } + | { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: UserMessageData[] } | { kind: 'block'; reason: string } /** Model-request failure with an optional machine-routable provider code. */ @@ -171,11 +159,11 @@ export interface Agent { * without running the model: an open turn stages it for the next safe log * position, while an idle injection appends it immediately without opening * a turn. - * @param content - the model-facing content blocks to deliver. - * @param options - target queue, wakeup decision, and source. + * @param input - model-facing content and its producer provenance. + * @param options - target queue and wakeup decision. * @returns the accepted message's {@link AgentMessageId}, stable across its `agent/inbox/*` events. */ - send(content: ContentBlock[], options: SendOptions): AgentMessageId + send(input: UserMessageData, options: SendOptions): AgentMessageId /** * Clear queued and steering work — unless `keepInbox` — and abort the active @@ -195,11 +183,10 @@ export interface Agent { * Queue an ordinary follow-up turn and wake the driver — the * `next-turn`/wakeup preset of {@link send}. The item becomes the sole * ordinary message of its own turn. - * @param content - the prompt content blocks. - * @param options - message source. + * @param input - prompt content and its producer provenance. * @returns the accepted message's {@link AgentMessageId}. */ - followup(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId + followup(input: UserMessageData): AgentMessageId /** * Submit steering into the running turn — the `next-step`/wakeup preset of @@ -208,23 +195,20 @@ export interface Agent { * remainder stays staged without waking the agent; retry or a later prompt * takes it. Idle steering falls back to a woken follow-up turn, while * cancellation or disposal may discard pending steering. - * @param content - the steering content blocks. - * @param options - message source. + * @param input - steering content and its producer provenance. * @returns the accepted message's {@link AgentMessageId}. */ - steer(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId + steer(input: UserMessageData): AgentMessageId /** * Append model-facing context without running the model — the * `next-step`/no-wakeup preset of {@link send}. An open-turn injection stages * at the next safe log position; an idle injection appends immediately - * without opening a turn. An omitted source defaults to - * `{ kind: 'plugin', plugin: '' }`. - * @param content - the injected context content blocks. - * @param options - context source. + * without opening a turn. + * @param input - injected context and its producer provenance. * @returns the accepted message's {@link AgentMessageId}. */ - inject(content: ContentBlock[], options?: AliasSendOptions): AgentMessageId + inject(input: UserMessageData): AgentMessageId /** * Re-open a turn on the current session log without a new prompt — the @@ -325,8 +309,9 @@ declare module 'cordis' { // ---- the machine's extension seams ---- /** * Allow, rewrite, or block one claimed prompt before it becomes a user - * message. Call `next()` for the unchanged default. The signal controls only this turn; - * listeners may cooperate with it but must not retain it for another turn. + * message or opens a turn. Call `next()` for the unchanged default. The + * signal controls only this admission attempt; listeners may cooperate with + * it but must not retain it for a later attempt or turn. * @param agent - the agent whose turn claimed the message. * @param content - the claimed message's blocks, as queued. * @param source - the message's resolved source. diff --git a/packages/core/session/README.md b/packages/core/session/README.md index f0adf0b9b9..5621e3ec9d 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -62,7 +62,7 @@ Providers stream token-sized deltas, so a raw log stores hundreds of `assistant/ ### Request-header reconstruction (`request-header.ts`) -`request/header` records a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, or `change`. `foldRequestHeader()` selects the latest snapshot; legacy delta events and the removed `fallback` reason are rejected. `messagePrefix` remains separate from derived history. See the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md). +`request/header` records a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, or `change`. `foldRequestHeader()` selects the latest snapshot; legacy delta events and the removed `fallback` reason are rejected. See the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md). A `user/message` renders its `content` verbatim as a user-role message whether it is a direct human prompt, a synthetic injection, or an admitted goal round; its typed `source` is the only channel that tells them apart and carries any domain-specific durable facts. Turn execution remains enclosed by `turn/start` and `turn/end`, while an idle injection may append and flush a `user/message` between turns without running the model. diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 0db67dca35..873ce424b4 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -42,7 +42,7 @@ The live registry pipeline has three transformable waterfalls, then the definiti - `ToolExecutionToken` — a fresh branded `Symbol` assigned by the registry. It supports equality correlation only and never crosses a model, log, or worker boundary. - `ToolExecution` — the readonly pipeline view: immutable `{ token, callId, name, arguments, signal, agent?, parent? }`; the registry separately retains and re-fuses the original caller signal. `ToolDispatchExecution` is the `tools/execute`-only view whose required signal is mutable, so a wrapper may replace and restore it but cannot delete it. A nested call's `parent` is a `ToolExecutionToken`, not an execution object. - `ToolRunContext` — the execution passed to a tool body, extending `ToolExecution` with `deferContext(context)`. Composite tools use it to ferry context produced by nested dispatches to the outer result even when the tool later throws or cancellation wins; it never injects immediately. -- `ToolExecutionResult` — discriminated execution-local outcome. Success is `{ isError:false, value:JsonValue, content, meta?, additionalContexts? }`; failure is `{ isError:true, error:{ message, info? }, content, meta?, additionalContexts? }` and has no value. Call identity stays on the immutable `ToolExecution`. The registry snapshots, validates, and freezes the canonical value before rendering, then materializes the durable presentation fields before final observation. `ToolFailure.info` carries an internal `{ name, code }` for a `HarnessError`; `additionalContexts` preserves every deferred or post-execute `AdditionalContext` for the loop's post-result FIFO. +- `ToolExecutionResult` — discriminated execution-local outcome. Success is `{ isError:false, value:JsonValue, content, meta?, additionalContexts? }`; failure is `{ isError:true, error:{ message, info? }, content, meta?, additionalContexts? }` and has no value. Call identity stays on the immutable `ToolExecution`. The registry snapshots, validates, and freezes the canonical value before rendering, then materializes the durable presentation fields before final observation. `ToolFailure.info` carries an internal `{ name, code }` for a `HarnessError`; `additionalContexts` preserves every deferred or post-execute `UserMessageData` for the loop's post-result FIFO. - `PreToolDecision` — `{kind:'allow'}` | `{kind:'deny', reason}` | `{kind:'ask', reason?}`. Input rewrite is deliberately not offered; `ask` is serviced by [`ctx.approval`](../../ui/user-approval/README.md) when mounted and otherwise degrades to deny. - `PostToolDecision` — accept may replace `content` or `value`, never both, and may attach `additionalContexts`; block turns feedback into a valueless failure. Content replacement preserves the canonical value and metadata. Value replacement is revalidated and rerenders content/metadata. Accept preserves tool-deferred contexts before decision contexts; block discards tool-deferred contexts and exposes only contexts explicitly supplied by the blocking decision. - `ToolGuard` — `(execution) => string | undefined`; the returned string is a final monotonic denial reason evaluated after the reorderable pre-execute waterfall and before dispatch. diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 3f0a78857d..799b5dc12f 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -10,9 +10,9 @@ import { AnonymousEntries, NamedEntries, ScopedLayers, scopeOf, scopeTarget } fr import type { ScopeKey, ScopeLayer, Scoped } from '@deepseek-ai/dsh-scope' import type { CallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm' import { assertNever, deepFreeze, HarnessError } from '@deepseek-ai/dsh-llm' -import type { AdditionalContext, Agent } from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' import { snapshotJsonValue } from '@deepseek-ai/dsh-session' -import type { JsonValue } from '@deepseek-ai/dsh-session' +import type { JsonValue, UserMessageData } from '@deepseek-ai/dsh-session' import type { ToolProviderResult } from '@deepseek-ai/dsh-system-prompt' import type { CodeRuntime } from '@deepseek-ai/dsh-code-runtime' // Type-only: makes `ctx.get('approval')` resolve to the ApprovalService @@ -306,7 +306,7 @@ export interface ToolRunContext extends ToolExecution { * the agent loop. Contexts retain their individual source and metadata and * are emitted in call order. */ - deferContext(context: AdditionalContext): void + deferContext(context: UserMessageData): void /** Mark a successful final result as terminal for the current agent turn. */ concludeTurn(): void } @@ -440,7 +440,7 @@ export interface ToolExecutionSuccess { readonly content: ContentBlock[] readonly error?: never readonly meta?: JsonValue - readonly additionalContexts?: AdditionalContext[] + readonly additionalContexts?: UserMessageData[] /** The agent loop stops after committing this successful result batch. */ readonly concludesTurn?: true } @@ -452,7 +452,7 @@ export interface ToolExecutionFailure { readonly value?: never readonly content: ContentBlock[] readonly meta?: JsonValue - readonly additionalContexts?: AdditionalContext[] + readonly additionalContexts?: UserMessageData[] readonly concludesTurn?: never } @@ -475,9 +475,9 @@ export type PreToolDecision = * next request, or block by turning corrective feedback into an error result. */ export type PostToolDecision = - | { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: AdditionalContext[] } - | { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: AdditionalContext[] } - | { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: AdditionalContext[] } + | { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: UserMessageData[] } + | { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: UserMessageData[] } + | { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: UserMessageData[] } /** * Best-effort human-readable message from an arbitrary thrown value: Error @@ -652,7 +652,7 @@ export class ToolRegistry extends Service { } /** Context deferred by a running tool body, keyed by its scheduler-owned execution. */ - private deferredContexts = new WeakMap() + private deferredContexts = new WeakMap() /** Successful executions whose tool body declared the current turn complete. */ private concludingExecutions = new WeakSet() /** Enclosing transport tokens marked terminal by a successful nested call. */ @@ -969,7 +969,7 @@ export class ToolRegistry extends Service { } private createExecution(exec: ToolExecutionInput): ScheduledToolPreparation | { kind: 'ready'; exec: MutableToolRunContext } { - const deferredContexts: AdditionalContext[] = [] + const deferredContexts: UserMessageData[] = [] const token = createExecutionToken() const callId = exec.callId const name = exec.name @@ -987,7 +987,7 @@ export class ToolRegistry extends Service { signal, ...agent !== undefined ? { agent } : {}, ...parent !== undefined ? { parent } : {}, - deferContext(context: AdditionalContext): void { + deferContext(context: UserMessageData): void { deferredContexts.push(context) }, concludeTurn(): void { diff --git a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts index a8e5049442..301f381a34 100644 --- a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts +++ b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts @@ -225,7 +225,7 @@ describe('dsh-agent-spine-demo bundle', () => { agentOptions: { provider: 'mock', model: 'mock' }, }) - handle.agent.followup([{ type: 'text', text: 'recover' }]) + handle.agent.followup({ content: [{ type: 'text', text: 'recover' }], source: { kind: 'user' } }) await waitForIdle(ctx, handle.agent) expect(adapter.requests).toBe(2) @@ -325,7 +325,7 @@ describe('dsh-agent-spine-demo bundle', () => { }) const agent = handle.agent - agent.followup([{ type: 'text', text: 'hi' }]) + agent.followup({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const sentText = adapter.requests[0]?.messages.map(messageText).join('\n') @@ -354,7 +354,7 @@ describe('dsh-agent-spine-demo bundle', () => { agentOptions: { provider: 'mock', model: 'mock' }, }) - handle.agent.followup([{ type: 'text', text: 'hi' }]) + handle.agent.followup({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }) await waitForIdle(ctx, handle.agent) expect(adapter.requests[0]?.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'hi' }] }]) @@ -444,7 +444,7 @@ describe('dsh-agent-spine-demo bundle', () => { agentOptions: { provider: 'mock', model: 'mock' }, }) - handle.agent.followup([{ type: 'text', text: 'hi' }]) + handle.agent.followup({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }) await waitForIdle(ctx, handle.agent) expect(messageText(adapter.requests[0]?.messages[1])).toContain('workspace rule before skills') diff --git a/packages/examples/cli-demo/src/cli.ts b/packages/examples/cli-demo/src/cli.ts index cbac81b164..34d578b259 100644 --- a/packages/examples/cli-demo/src/cli.ts +++ b/packages/examples/cli-demo/src/cli.ts @@ -298,7 +298,7 @@ export async function runOneShot(ctx: Context, options: OneShotOptions): Promise try { /* v8 ignore next -- skips send only when cancellation wins the listener-registration race above */ if (!firstTurnEnded) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition - agent.followup([{ type: 'text', text: options.task }]) + agent.followup({ content: [{ type: 'text', text: options.task }], source: { kind: 'user' } }) } await turnEnded } finally { diff --git a/packages/examples/cli-demo/tests/cli.spec.ts b/packages/examples/cli-demo/tests/cli.spec.ts index 08583cb02d..c497eb59dc 100644 --- a/packages/examples/cli-demo/tests/cli.spec.ts +++ b/packages/examples/cli-demo/tests/cli.spec.ts @@ -372,7 +372,7 @@ describe('runOneShot and executeCli', () => { ctx.on('agent/inbox/enqueue', (subject) => { if (subject !== agent || injected) return injected = true - agent.inject([{ type: 'text', text: 'startup injection' }], { source: { kind: 'plugin', plugin: 'test' } }) + agent.inject({ content: [{ type: 'text', text: 'startup injection' }], source: { kind: 'plugin', plugin: 'test' } }) other.append('turn/start', { turn: 1, trigger: { kind: 'injection', source: { kind: 'plugin', plugin: 'test' } } }) other.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) }) @@ -473,7 +473,7 @@ describe('runOneShot and executeCli', () => { startup.ctx.on('session/event', (session, event) => { if (session === startup.agent.session && event.type === 'assistant/chunk') started() }) - startup.agent.followup([{ type: 'text', text: 'first' }]) + startup.agent.followup({ content: [{ type: 'text', text: 'first' }], source: { kind: 'user' } }) await running const startupAbort = new AbortController() const waiting = runOneShot(startup.ctx, { task: 'second', signal: startupAbort.signal }) diff --git a/packages/fs/tool-fs/tests/fs-tools.e2e.ts b/packages/fs/tool-fs/tests/fs-tools.e2e.ts index b768813b22..5c19c906a0 100644 --- a/packages/fs/tool-fs/tests/fs-tools.e2e.ts +++ b/packages/fs/tool-fs/tests/fs-tools.e2e.ts @@ -36,10 +36,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('fs tools with-key smoke', () => // (config.cwd = workdir) is the workspace. const agent = ctx.agentLoop.create(SessionId('fs-e2e'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) - agent.followup([{ type: 'text', text: + agent.followup({ content: [{ type: 'text', text: 'Create a file named note.txt containing exactly the line: status: draft. ' + 'Then read it back, then edit it to replace the literal word draft with final. ' - + 'Tell me when done.' }]) + + 'Tell me when done.' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // Verify the WORLD: the edit landed on disk. @@ -68,8 +68,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('fs tools with-key smoke', () => meta: { cwd: sessionDir }, agentOptions: { provider: 'deepseek', model: 'deepseek-v4-flash' }, }) - handle.agent.followup([{ type: 'text', text: - 'Use the write tool to create a file named where.txt containing exactly the line: here. Tell me when done.' }]) + handle.agent.followup({ content: [{ type: 'text', text: + 'Use the write tool to create a file named where.txt containing exactly the line: here. Tell me when done.' }], source: { kind: 'user' } }) await waitForIdle(ctx, handle.agent) // The file is in the SESSION dir, not the config dir. diff --git a/packages/goal/command-goal/tests/command-goal.spec.ts b/packages/goal/command-goal/tests/command-goal.spec.ts index 799cf00104..670aa5585a 100644 --- a/packages/goal/command-goal/tests/command-goal.spec.ts +++ b/packages/goal/command-goal/tests/command-goal.spec.ts @@ -2,12 +2,11 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import AgentRegistry, { AgentMessageId } from '@deepseek-ai/dsh-agent' -import type { Agent, AgentStatus, AliasSendOptions } from '@deepseek-ai/dsh-agent' +import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' import CommandService from '@deepseek-ai/dsh-commands' import GoalService from '@deepseek-ai/dsh-goal' import type { GoalRef } from '@deepseek-ai/dsh-goal' -import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' -import { Session, SessionId } from '@deepseek-ai/dsh-session' +import { Session, SessionId, type UserMessageData } from '@deepseek-ai/dsh-session' import * as commandGoal from '@deepseek-ai/dsh-command-goal' interface Harness { @@ -17,24 +16,9 @@ interface Harness { readonly plugin: Awaited> } -/** Number the next balanced injection or message turn. */ -function nextTurn(session: Session): number { - return session.events.reduce( - (maximum, event) => event.type === 'turn/start' ? Math.max(maximum, event.data.turn) : maximum, - 0, - ) + 1 -} - -/** Append one idle injection using the public Agent contract's balanced shape. */ -function appendInjection(session: Session, content: ContentBlock[], options?: AliasSendOptions): void { - const source: MessageSource = options?.source ?? { kind: 'plugin', plugin: '' } - const turn = nextTurn(session) - session.append('turn/start', { turn, trigger: { kind: 'injection', source } }) - session.append('user/message', { - content, - source, - }, { surfaceOp: 'append' }) - session.append('turn/end', { turn, reason: { kind: 'completed' } }) +/** Append one idle injection using the public Agent contract. */ +function appendInjection(session: Session, input: UserMessageData): void { + session.append('user/message', input, { surfaceOp: 'append' }) } /** Build a live idle agent accepted by the exact-identity goal service. */ @@ -50,7 +34,7 @@ function stubAgent(id: string): { agent: Agent; session: Session } { send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), - inject(content, options) { appendInjection(session, content, options); return AgentMessageId('stub') }, + inject(input) { appendInjection(session, input); return AgentMessageId('stub') }, cancel() { status = 'idle' }, retry() {}, whenIdle() { return Promise.resolve() }, @@ -126,7 +110,7 @@ describe('/goal human command', () => { expect(created.text).toContain('Rounds: 0/256') expect(created.text).toContain('Activation: armed') expect(test.ctx.goals.get(test.agent)?.objective).toBe('finish the release') - expect(test.session.events.map(event => event.type)).toEqual(['turn/start', 'user/message', 'turn/end']) + expect(test.session.events.map(event => event.type)).toEqual(['user/message']) const count = test.session.events.length await expect(run(test, ' replacement')).resolves.toEqual({ diff --git a/packages/goal/goal-session/src/index.ts b/packages/goal/goal-session/src/index.ts index 4caccccbef..f2c1eab6be 100644 --- a/packages/goal/goal-session/src/index.ts +++ b/packages/goal/goal-session/src/index.ts @@ -215,9 +215,7 @@ export function apply(ctx: Context): void { } state.attempt = reservation try { - agent.followup(content, { - source: { kind: 'goal', goalId: goal.id, revision: goal.revision, round }, - }) + agent.followup({ content: content, source: { kind: 'goal', goalId: goal.id, revision: goal.revision, round } }) } catch (error: unknown) { state.attempt = undefined ctx.logger.warn(`goal-session: could not queue round ${round} for agent "${agent.id}": ${renderThrown(error)}`) @@ -308,7 +306,6 @@ export function apply(ctx: Context): void { ctx.on('agent/cancel-requested', (agent, cause) => { const state = stateFor(agent) const attempt = state.attempt - state.attempt = undefined state.competingQueued = false const goal = currentGoal(state) if (goal?.phase === 'active' && goal.activation === 'armed') { @@ -316,6 +313,12 @@ export function apply(ctx: Context): void { disarm(state) return } + // An admitted round closes durably as aborted; retain it so the normal + // turn outcome path appends pause after cancellation reaches idle. + // Pausing here would stage context into the active outbox only for this + // same cancel() call to discard it. + if (attempt.turn !== undefined || attempt.phase === 'admitted') return + state.attempt = undefined try { applyOutcome(state, goal, { kind: 'pause', reason: cause.kind }) } catch (error: unknown) { diff --git a/packages/goal/goal-session/tests/goal-session.spec.ts b/packages/goal/goal-session/tests/goal-session.spec.ts index 4008d23882..9e1e82fc4a 100644 --- a/packages/goal/goal-session/tests/goal-session.spec.ts +++ b/packages/goal/goal-session/tests/goal-session.spec.ts @@ -4,7 +4,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import { agentEvents } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' -import GoalService, { GoalId } from '@deepseek-ai/dsh-goal' +import GoalService, { foldGoal, GoalId } from '@deepseek-ai/dsh-goal' import type { GoalView } from '@deepseek-ai/dsh-goal' import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' @@ -273,7 +273,7 @@ describe('same-session goal driving', () => { ? Promise.resolve({ kind: 'block', reason: 'stop this round' }) : next()) test.ctx.on('goal/changed', (agent, change) => { - if (change.operation === 'block') agent.followup([{ type: 'text', text: 'inspect the blocker' }]) + if (change.operation === 'block') agent.followup({ content: [{ type: 'text', text: 'inspect the blocker' }], source: { kind: 'user' } }) }) test.ctx.goals.create(test.agent, { objective: 'stop and inspect' }) @@ -314,13 +314,17 @@ describe('same-session goal driving', () => { const goal = await waitForGoal(test.ctx, test.agent, current => current?.phase === 'paused') expect(goal).toMatchObject({ roundsStarted: 1, activation: 'disarmed' }) + expect(foldGoal(test.agent.session.events)).toMatchObject({ + goal: { phase: 'paused', revision: 2 }, + roundsStarted: 1, + }) expect(test.adapter.requests).toHaveLength(1) }) it('lets already-queued human work finish before reserving the next round', async () => { const test = await harness([textResponse('human answer'), textResponse('goal answer')]) test.ctx.goals.create(test.agent, { objective: 'continue after the human', maxGoalRounds: 1 }) - test.agent.followup([{ type: 'text', text: 'human goes first' }]) + test.agent.followup({ content: [{ type: 'text', text: 'human goes first' }], source: { kind: 'user' } }) await waitForGoal(test.ctx, test.agent, goal => goal?.phase === 'blocked') @@ -361,7 +365,7 @@ describe('same-session goal driving', () => { test.ctx.on('agent/inbox/enqueue', (agent, info) => { if (agent !== test.agent || info.source.kind !== 'goal' || inserted) return inserted = true - agent.followup([{ type: 'text', text: 'human joined the pending batch' }]) + agent.followup({ content: [{ type: 'text', text: 'human joined the pending batch' }], source: { kind: 'user' } }) }) test.ctx.goals.create(test.agent, { objective: 'yield to nested human input', maxGoalRounds: 1 }) @@ -444,11 +448,11 @@ describe('same-session goal driving', () => { // Reject only the goal-sourced round follow-up, not the state-change injection // that precedes it. const realFollowup = test.agent.followup.bind(test.agent) - vi.spyOn(test.agent, 'followup').mockImplementation((content, options) => { - if (options?.source?.kind === 'goal') { + vi.spyOn(test.agent, 'followup').mockImplementation((input) => { + if (input.source.kind === 'goal') { throw new Error('queue rejected') } - return realFollowup(content, options) + return realFollowup(input) }) test.ctx.goals.create(test.agent, { objective: 'handle queue failure' }) @@ -465,12 +469,12 @@ describe('same-session goal driving', () => { it('preserves a custom agent side effect when followup disarms before throwing', async () => { const test = await harness([]) const realFollowup = test.agent.followup.bind(test.agent) - vi.spyOn(test.agent, 'followup').mockImplementation((content, options) => { - if (options?.source?.kind === 'goal') { + vi.spyOn(test.agent, 'followup').mockImplementation((input) => { + if (input.source.kind === 'goal') { test.ctx.goals.disarm(test.agent) throw new Error('queue rejected after disarm') } - return realFollowup(content, options) + return realFollowup(input) }) test.ctx.goals.create(test.agent, { objective: 'preserve the newer activation state' }) @@ -548,9 +552,7 @@ describe('same-session goal driving', () => { it('blocks forged goal attribution without touching an absent reservation', async () => { const test = await harness([]) - test.agent.followup([{ type: 'text', text: 'forged automatic work' }], { - source: { kind: 'goal', goalId: GoalId('forged-goal'), revision: 1, round: 1 }, - }) + test.agent.followup({ content: [{ type: 'text', text: 'forged automatic work' }], source: { kind: 'goal', goalId: GoalId('forged-goal'), revision: 1, round: 1 } }) await test.agent.whenIdle() expect(test.adapter.requests).toHaveLength(0) @@ -559,7 +561,7 @@ describe('same-session goal driving', () => { it('does not invent goal state when ordinary queued work is cancelled', async () => { const test = await harness([]) - test.agent.followup([{ type: 'text', text: 'cancel ordinary work' }]) + test.agent.followup({ content: [{ type: 'text', text: 'cancel ordinary work' }], source: { kind: 'user' } }) test.agent.cancel({ kind: 'user' }) await test.agent.whenIdle() @@ -569,7 +571,7 @@ describe('same-session goal driving', () => { it('disarms without durably pausing when cancellation belongs to unrelated human work', async () => { const test = await harness(['hang']) - test.agent.followup([{ type: 'text', text: 'inspect something first' }]) + test.agent.followup({ content: [{ type: 'text', text: 'inspect something first' }], source: { kind: 'user' } }) await waitForRequests(test.adapter, 1) const created = test.ctx.goals.create(test.agent, { objective: 'continue after inspection' }) diff --git a/packages/goal/goal/src/index.ts b/packages/goal/goal/src/index.ts index f8b324c031..b5b803ba76 100644 --- a/packages/goal/goal/src/index.ts +++ b/packages/goal/goal/src/index.ts @@ -490,7 +490,8 @@ export class GoalService extends Service { const pending: PendingGoalChange = { change, activation, applied: false } cache.pending.push(pending) try { - agent.inject(renderGoalChange(change), { + agent.inject({ + content: renderGoalChange(change), source: { kind: 'goal', goalId: ref.id, revision: ref.revision, round: 0, change }, }) } catch (error: unknown) { diff --git a/packages/goal/goal/tests/goal.spec.ts b/packages/goal/goal/tests/goal.spec.ts index 38419df8d3..d0f0264068 100644 --- a/packages/goal/goal/tests/goal.spec.ts +++ b/packages/goal/goal/tests/goal.spec.ts @@ -1,9 +1,9 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import AgentRegistry, { agentEvents, AgentMessageId } from '@deepseek-ai/dsh-agent' -import type { Agent, AgentStatus, AliasSendOptions } from '@deepseek-ai/dsh-agent' +import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' import { HarnessError, type ContentBlock, type MessageSource } from '@deepseek-ai/dsh-llm' -import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' +import SessionStore, { Session, SessionId, type UserMessageData } from '@deepseek-ai/dsh-session' import GoalService, { GoalError, GoalId, @@ -13,10 +13,7 @@ import GoalService, { } from '@deepseek-ai/dsh-goal' import type { GoalChangeMeta, GoalRef, GoalSnapshotChangeMeta } from '@deepseek-ai/dsh-goal' -interface DeferredInjection { - content: ContentBlock[] - options: AliasSendOptions | undefined -} +type DeferredInjection = UserMessageData interface StubAgent { agent: Agent @@ -32,23 +29,9 @@ function nextTurn(session: Session): number { return session.events.reduce((max, event) => event.type === 'turn/start' ? Math.max(max, event.data.turn) : max, 0) + 1 } -/** Mirror the public Agent.inject idle/open-turn contract for domain tests. */ -function appendInjection(session: Session, content: ContentBlock[], options?: AliasSendOptions): void { - const source: MessageSource = options?.source ?? { kind: 'plugin', plugin: '' } - const context = { - content, - source, - } - const last = session.events.at(-1) - const open = last !== undefined && last.type !== 'turn/end' - if (open) { - session.append('user/message', context, { surfaceOp: 'append' }) - return - } - const turn = nextTurn(session) - session.append('turn/start', { turn, trigger: { kind: 'injection', source } }) - session.append('user/message', context, { surfaceOp: 'append' }) - session.append('turn/end', { turn, reason: { kind: 'completed' } }) +/** Mirror the public Agent.inject contract for domain tests. */ +function appendInjection(session: Session, input: UserMessageData): void { + session.append('user/message', input, { surfaceOp: 'append' }) } /** Build a registry-compatible agent around one concrete session. */ @@ -66,9 +49,9 @@ function stubAgentForSession(session: Session): StubAgent { send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), - inject(content, options) { - if (shouldDefer) deferred.push({ content, options }) - else appendInjection(session, content, options) + inject(input) { + if (shouldDefer) deferred.push(input) + else appendInjection(session, input) return AgentMessageId('stub') }, cancel() {}, @@ -83,7 +66,7 @@ function stubAgentForSession(session: Session): StubAgent { setStatus(value) { status = value }, drain() { shouldDefer = false - for (const injection of deferred.splice(0)) appendInjection(session, injection.content, injection.options) + for (const injection of deferred.splice(0)) appendInjection(session, injection) }, } } @@ -112,7 +95,7 @@ function appendRound(session: Session, ref: GoalRef, round: number): void { } describe('GoalService creation and replay', () => { - it('applies the configured default and writes one balanced verbatim context snapshot', async () => { + it('applies the configured default and writes one verbatim context snapshot', async () => { vi.useFakeTimers() vi.setSystemTime(1_700_000_000_000) const { ctx, agent, session } = await harness({ defaultMaxGoalRounds: 17 }) @@ -133,8 +116,8 @@ describe('GoalService creation and replay', () => { }) expect(goal.id).toMatch(/^goal-/) expect(seen).toEqual(['create']) - expect(session.events.map(event => event.type)).toEqual(['turn/start', 'user/message', 'turn/end']) - const context = session.events[1] + expect(session.events.map(event => event.type)).toEqual(['user/message']) + const context = session.events[0] expect(context?.type).toBe('user/message') if (context?.type !== 'user/message') throw new Error('expected goal context') expect(context.data.source).toMatchObject({ kind: 'goal', goalId: goal.id, revision: 1, round: 0 }) @@ -438,7 +421,7 @@ describe('GoalService mutations', () => { expect(deferred).toHaveLength(3) expect(session.events).toHaveLength(0) - appendInjection(session, [{ type: 'text', text: 'unrelated' }], { source: { kind: 'plugin', plugin: 'test' } }) + appendInjection(session, { content: [{ type: 'text', text: 'unrelated' }], source: { kind: 'plugin', plugin: 'test' } }) expect(ctx.goals.get(agent)).toMatchObject({ revision: 3, phase: 'paused' }) test.drain() expect(deferred).toHaveLength(0) @@ -472,9 +455,9 @@ describe('GoalService mutations', () => { const stub = stubAgent('goal-rejected-injection') const append = stub.agent.inject.bind(stub.agent) let reject = true - stub.agent.inject = (content, options) => { + stub.agent.inject = (input) => { if (reject) throw new Error('injection rejected') - return append(content, options) + return append(input) } ctx.agents.register(stub.agent) @@ -493,7 +476,7 @@ describe('GoalService mutations', () => { test.ctx.goals.edit(test.agent, created, { objective: 'ordered edit' }) const second = test.deferred[1] if (second === undefined) throw new Error('expected a second deferred goal mutation') - appendInjection(test.session, second.content, second.options) + appendInjection(test.session, second) expect(() => test.ctx.goals.get(test.agent)).toThrow('advance the current goal') }) @@ -548,10 +531,10 @@ describe('GoalService mutations', () => { createdAt: 12, updatedAt: 12, } - appendInjection(session, renderGoalChange(change), { + appendInjection(session, { content: renderGoalChange(change), source: { kind: 'goal', goalId: change.goal.id, revision: 1, round: 0, change }, }) - appendInjection(session, [{ type: 'text', text: 'corrupt' }], { + appendInjection(session, { content: [{ type: 'text', text: 'corrupt' }], source: { kind: 'goal', goalId: change.goal.id, revision: 2, round: 0, change: { ...change, operation: 'edit', extra: true } as never, @@ -645,7 +628,7 @@ describe('goal replay validation', () => { expect(decodeGoalChange(undefined)).toBeUndefined() expect(decodeGoalChange({ kind: 'other' })).toBeUndefined() const session = new Session(SessionId('unrelated')) - appendInjection(session, [{ type: 'text', text: 'other' }], { + appendInjection(session, { content: [{ type: 'text', text: 'other' }], source: { kind: 'plugin', plugin: 'test' }, }) expect(foldGoal(session.events)).toEqual({ roundsStarted: 0 }) diff --git a/packages/goal/tool-goal/README.md b/packages/goal/tool-goal/README.md index 346ac07dcc..b5d81e9b2c 100644 --- a/packages/goal/tool-goal/README.md +++ b/packages/goal/tool-goal/README.md @@ -12,7 +12,7 @@ All calls are exclusive, so a model-ordered batch observes earlier mutations and All three canonical values match the compact JSON already rendered to Native callers: `{ goal: null }` or `{ goal: { id, revision, objective, phase, roundsStarted, maxGoalRounds, blockedReason? }, activation }`. Programmatic consumers therefore receive the same domain structure without parsing the rendered JSON. -An autonomous goal round that successfully reports `complete` or `blocked` contributes the existing terminal `agent/turn-stop` decision for that physical turn. Direct-human mutations never contribute this stop: the assistant may acknowledge the change and concurrent human steering remains available to the loop. +An autonomous goal round that successfully reports `complete` or `blocked` marks that tool execution with `concludeTurn()` so the physical turn stops after the step. Direct-human mutations never contribute this stop: the assistant may acknowledge the change and concurrent human steering remains available to the loop. ## Authority diff --git a/packages/goal/tool-goal/tests/tool-goal.spec.ts b/packages/goal/tool-goal/tests/tool-goal.spec.ts index 6354e53ac6..90339fafe6 100644 --- a/packages/goal/tool-goal/tests/tool-goal.spec.ts +++ b/packages/goal/tool-goal/tests/tool-goal.spec.ts @@ -2,11 +2,11 @@ import { describe, expect, it } from 'vitest' import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import AgentRegistry, { agentEvents, AgentMessageId } from '@deepseek-ai/dsh-agent' -import type { Agent, AgentStatus, AliasSendOptions } from '@deepseek-ai/dsh-agent' +import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' import GoalService, { GoalId } from '@deepseek-ai/dsh-goal' import type { GoalRef } from '@deepseek-ai/dsh-goal' import { CallId } from '@deepseek-ai/dsh-llm' -import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' +import type { MessageSource } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' @@ -34,12 +34,8 @@ function stubAgent(rawId: string, supplied?: Session): StubAgent { send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), - inject(content: ContentBlock[], options?: AliasSendOptions) { - const source = options?.source ?? { kind: 'plugin', plugin: '' } - session.append('user/message', { - content, - source, - }, { surfaceOp: 'append' }) + inject(input) { + session.append('user/message', input, { surfaceOp: 'append' }) return AgentMessageId('stub') }, cancel() {}, diff --git a/packages/guard/repeat-tool-guard/src/index.ts b/packages/guard/repeat-tool-guard/src/index.ts index 1aa35c8353..7385ced5aa 100644 --- a/packages/guard/repeat-tool-guard/src/index.ts +++ b/packages/guard/repeat-tool-guard/src/index.ts @@ -8,8 +8,9 @@ import type { Context } from 'cordis' import z from 'schemastery' -import type { AdditionalContext, Agent, PromptDecision } from '@deepseek-ai/dsh-agent' +import type { Agent, PromptDecision } from '@deepseek-ai/dsh-agent' import type { MessageSource } from '@deepseek-ai/dsh-llm' +import type { UserMessageData } from '@deepseek-ai/dsh-session' import type { PostToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools' export const name = 'repeat-tool-guard' @@ -142,7 +143,7 @@ function validateThresholds(values: number[]): number[] { * Prepend the guard's reminder while preserving every downstream context's * source and metadata. */ -function prependContext(ours: AdditionalContext, theirs: AdditionalContext[] | undefined): AdditionalContext[] { +function prependContext(ours: UserMessageData, theirs: UserMessageData[] | undefined): UserMessageData[] { return [ours, ...theirs ?? []] } @@ -184,7 +185,7 @@ export function apply(ctx: Context, config: Config): void { * same pipeline), and a model hammering a denied call is exactly the loop * worth breaking. */ - function observe(exec: ToolExecution): AdditionalContext | undefined { + function observe(exec: ToolExecution): UserMessageData | undefined { // A direct `ctx.tools.execute()` caller has no model to remind and no id // to key on; only agent-loop calls participate. if (!exec.agent) return undefined diff --git a/packages/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts b/packages/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts index d0a3214518..23defab161 100644 --- a/packages/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts +++ b/packages/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts @@ -56,7 +56,7 @@ describe('threshold escalation', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const found = reminders(agent) @@ -77,7 +77,7 @@ describe('threshold escalation', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const found = reminders(agent) @@ -99,7 +99,7 @@ describe('chain semantics', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const found = reminders(agent) @@ -123,7 +123,7 @@ describe('chain semantics', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(reminders(agent)).toHaveLength(1) @@ -141,7 +141,7 @@ describe('chain semantics', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const found = reminders(agent) @@ -162,7 +162,7 @@ describe('chain semantics', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const found = reminders(agent) @@ -178,7 +178,7 @@ describe('chain semantics', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(reminders(agent)).toHaveLength(1) // probe was NOT excluded @@ -194,7 +194,7 @@ describe('chain semantics', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(reminders(agent)).toHaveLength(1) // all three canonicalize identically @@ -215,8 +215,8 @@ describe('chain semantics', () => { ])) const agentA = ctx.agentLoop.create(SessionId('a'), { provider: 'mock-a', model: 'model-a' }) const agentB = ctx.agentLoop.create(SessionId('b'), { provider: 'mock-b', model: 'model-b' }) - agentA.followup([{ type: 'text', text: 'go' }]) - agentB.followup([{ type: 'text', text: 'go' }]) + agentA.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) + agentB.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await Promise.all([waitForIdle(ctx, agentA), waitForIdle(ctx, agentB)]) expect(reminders(agentA)).toHaveLength(0) // 2 repeats < 3, despite B's 3 in the same registry @@ -234,9 +234,9 @@ describe('chain semantics', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) - agent.followup([{ type: 'text', text: 'again' }]) + agent.followup({ content: [{ type: 'text', text: 'again' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(reminders(agent)).toHaveLength(0) @@ -256,13 +256,13 @@ describe('chain semantics', () => { const fiber = await ctx.plugin(Object.assign((inner: Context) => { first = inner.agentLoop.create(SessionId('reused'), { provider: 'mock', model: 'mock' }) }, { inject: ['agentLoop'] })) - first.followup([{ type: 'text', text: 'go' }]) + first.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, first) await fiber.dispose() await first.whenIdle() const second = ctx.agentLoop.create(SessionId('reused'), { provider: 'mock', model: 'mock' }) - second.followup([{ type: 'text', text: 'go' }]) + second.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, second) expect(reminders(second)).toHaveLength(0) @@ -278,7 +278,7 @@ describe('chain semantics', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(reminders(agent)).toHaveLength(1) @@ -294,7 +294,7 @@ describe('chain semantics', () => { textResponse('done'), ])) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(reminders(agent)).toHaveLength(0) @@ -316,7 +316,7 @@ describe('fold onto the downstream decision', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const found = reminders(agent) @@ -347,7 +347,7 @@ describe('fold onto the downstream decision', () => { ]) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const found = reminders(agent) diff --git a/packages/hooks/hook-protocol/README.md b/packages/hooks/hook-protocol/README.md index 3a84807285..3ab7f79505 100644 --- a/packages/hooks/hook-protocol/README.md +++ b/packages/hooks/hook-protocol/README.md @@ -27,7 +27,7 @@ Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claud Declaration-merged into `SessionEventMap` (log-only, like `compact/*` — NOT a `SurfaceEventType`, no `surfaceOp`): `hook/invoked` (a hook command ran) and `hook/result` (its outcome, paired by `handlerId`, with `appendHookResult` owning the decision rule). Payloads and per-event JSDoc are in the generated [persistence log event catalog](../../../docs/persistence-catalog.md); `stderrSummary` is truncated to the record's `stderrSummaryMaxChars` (the bridge's config, reference default `DEFAULT_STDERR_SUMMARY_MAX_CHARS` = 500; omitted when empty). -Like every event they must sit inside an open turn. The mid-turn points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn by construction; `SessionStart` gets no `hook/*` record (its injected `user/message` is the durable evidence) — see the hooks Agent Note. +Like every event they must sit inside an open turn. The mid-turn points (`PreToolUse`/`PostToolUse`/`Stop`) fire inside the loop's open turn by construction. `SessionStart` and the pre-turn `UserPromptSubmit` admission seam get no `hook/*` record; allowed context is instead evidenced by its sourced `user/message` — see the hooks Agent Note. ## Model Experience diff --git a/packages/hooks/hooks-claude/README.md b/packages/hooks/hooks-claude/README.md index b492b2939d..9400a7886b 100644 --- a/packages/hooks/hooks-claude/README.md +++ b/packages/hooks/hooks-claude/README.md @@ -38,7 +38,7 @@ The hooks **themselves** run in the agent's session workspace: for the agent-sco | `UserPromptSubmit` | `agent/prompt-submit` (waterfall) | `deny` → `PromptDecision.block`; additionalContext-only → delegate via `next()` then prepend a separately sourced context to downstream `additionalContexts` (a later listener can still block/rewrite) | | `PreToolUse` | `tools/pre-execute` (waterfall) | `deny` → `PreToolDecision.deny`; `ask` → `PreToolDecision.ask` | | `PostToolUse` | `tools/post-execute` (waterfall) | `deny` → `block` with feedback; additionalContext-only → delegate via `next()` then prepend a separately sourced context to the downstream decision; Code Mode defers sub-call contexts until the outer `run_code` result | -| `Stop` | `agent/turn-continuation` (waterfall) | a blocking Stop hook forces `continue`, feeding its reason as next-step steering | +| `Stop` | `agent/stopping` (serial) | a blocking Stop hook feeds its reason through `steer()`, forcing another step | | `SubagentStart` | `subagent/start` (emit) | additionalContext → `agent.inject()` into a live in-process child; a remote child has no local injection target | | `SubagentStop` | `subagent/end` (emit) | observe-only | diff --git a/packages/hooks/hooks-claude/src/index.ts b/packages/hooks/hooks-claude/src/index.ts index 05204ab161..8d13aef8d2 100644 --- a/packages/hooks/hooks-claude/src/index.ts +++ b/packages/hooks/hooks-claude/src/index.ts @@ -12,8 +12,9 @@ import { readFileSync } from 'node:fs' import type { Context } from 'cordis' import z from 'schemastery' -import type { AdditionalContext, Agent, PromptDecision } from '@deepseek-ai/dsh-agent' +import type { Agent, PromptDecision } from '@deepseek-ai/dsh-agent' import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' +import type { UserMessageData } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-session-persistence' import type { PostToolDecision, PreToolDecision, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools' import { @@ -122,11 +123,12 @@ export function apply(ctx: Context, config: Config): void { /** * Run every command hook configured for `point` whose matcher selects * `matchQuery`, with the per-event `payload` on stdin, and fold the results. - * Writes a `hook/invoked`/`hook/result` pair per hook into the session when one - * is available (the mid-turn points always have an open turn). Returns the - * merged outcome (a neutral, already-most-restrictive view) for the caller to - * map onto its seam decision. `matchQuery` is the event's matcher subject - * (tool name, session source, …); `''` for events that ignore matchers. + * Writes a `hook/invoked`/`hook/result` pair per hook when `opts.turn` names + * an open turn. Pre-turn `UserPromptSubmit` and detached lifecycle points + * omit the pair. Returns the merged outcome (a neutral, + * already-most-restrictive view) for the caller to map onto its seam + * decision. `matchQuery` is the event's matcher subject (tool name, session + * source, …); `''` for events that ignore matchers. */ async function runPoint( point: string, @@ -183,14 +185,14 @@ export function apply(ctx: Context, config: Config): void { // TODO(hook-continue-false): `merged.stop` is logged but needs a run-level halt seam. /** Build additional model context from hook output, or return undefined when empty. */ - function contextFrom(merged: MergedHookOutcome): AdditionalContext | undefined { + function contextFrom(merged: MergedHookOutcome): UserMessageData | undefined { if (merged.additionalContext.length === 0) return undefined const content: ContentBlock[] = merged.additionalContext.map(text => ({ type: 'text', text })) return { content, source: PLUGIN_SOURCE } } /** Prepend one context without flattening downstream provenance or metadata. */ - function prependContext(ours: AdditionalContext, theirs: AdditionalContext[] | undefined): AdditionalContext[] { + function prependContext(ours: UserMessageData, theirs: UserMessageData[] | undefined): UserMessageData[] { return [ours, ...theirs ?? []] } @@ -201,7 +203,7 @@ export function apply(ctx: Context, config: Config): void { detached.track(runPoint('SessionStart', source, sessionStartPayload(ctx, agent, source), { agent, signal: detached.signal }) .then((merged) => { const context = contextFrom(merged) - if (context) agent.inject(context.content, { source: context.source }) + if (context) agent.inject({ content: context.content, source: context.source }) }) .catch((error: unknown) => { ctx.logger.warn(`hooks-claude: SessionStart hook failed: ${String(error)}`) @@ -211,8 +213,7 @@ export function apply(ctx: Context, config: Config): void { // --- UserPromptSubmit → PromptDecision. The prompt text is the payload; no // matcher subject (CC ignores matchers for this event). --- ctx.on('agent/prompt-submit', async (agent, content, _source, signal, next): Promise => { - const turn = lastTurn(agent) - const merged = await runPoint('UserPromptSubmit', '', promptPayload(ctx, agent, content), { agent, turn, signal }) + const merged = await runPoint('UserPromptSubmit', '', promptPayload(ctx, agent, content), { agent, signal }) if (merged.decision === 'deny') { return { kind: 'block', reason: merged.reason ?? 'blocked by UserPromptSubmit hook' } } @@ -266,7 +267,7 @@ export function apply(ctx: Context, config: Config): void { if (merged.decision === 'deny') { // A blocking Stop hook forces continuation. const text = merged.reason ?? 'continue: blocked by Stop hook' - agent.steer([{ type: 'text', text }], { source: PLUGIN_SOURCE }) + agent.steer({ content: [{ type: 'text', text }], source: PLUGIN_SOURCE }) } }) @@ -277,7 +278,7 @@ export function apply(ctx: Context, config: Config): void { detached.track(runPoint('SubagentStart', SUBAGENT_TYPE, subagentPayload(ctx, 'SubagentStart', info, child), { ...child ? { agent: child } : {}, signal: detached.signal }) .then((merged) => { const context = contextFrom(merged) - if (context && child) child.inject(context.content, { source: context.source }) + if (context && child) child.inject({ content: context.content, source: context.source }) }) .catch((error: unknown) => { ctx.logger.warn(`hooks-claude: SubagentStart hook failed: ${String(error)}`) })) }) @@ -301,13 +302,11 @@ const SUBAGENT_TYPE = 'general-purpose' // --- Per-event stdin payloads (the CC DIALECT shape). Field names match CC's // hook input schema; this is the part a bridge owns. --- -/** The last (open or just-closed) turn number in the agent's log, or 0. */ +/** The last open turn number in the agent's log, or 0 without an agent. */ function lastTurn(agent: Agent | undefined): number { if (!agent) return 0 const last = [...agent.session.events].findLast(e => e.type === 'turn/start') - /* v8 ignore next -- the `: 0` arm is a defensive fallback: lastTurn is only - called from the mid-turn seams (prompt-submit/pre-/post-execute/continuation), - which always run inside an open turn, so `last` is always a turn/start here. */ + /* v8 ignore next -- agent-present callers are tool/stop seams inside an open turn. */ return last?.type === 'turn/start' ? last.data.turn : 0 } diff --git a/packages/hooks/hooks-claude/tests/bridge.spec.ts b/packages/hooks/hooks-claude/tests/bridge.spec.ts index 5c0b784cc7..e458f08630 100644 --- a/packages/hooks/hooks-claude/tests/bridge.spec.ts +++ b/packages/hooks/hooks-claude/tests/bridge.spec.ts @@ -93,15 +93,14 @@ describe('hooks-claude bridge — UserPromptSubmit', () => { const adapter = new MockAdapter([textResponse('should not run')]) const ctx = await harness(dir, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'do something' }]) + agent.followup({ content: [{ type: 'text', text: 'do something' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // The prompt was blocked before the model and before a turn opened. expect(adapter.requests).toHaveLength(0) expect(events(agent).some(e => e.type === 'turn/start')).toBe(false) - // The hook ran and was recorded. - expect(events(agent).some(e => e.type === 'hook/invoked' && e.data.point === 'UserPromptSubmit')).toBe(true) - expect(events(agent).some(e => e.type === 'hook/result' && e.data.decision === 'block')).toBe(true) + // Admission has no open turn in which turn-scoped hook provenance could live. + expect(events(agent).some(e => e.type === 'hook/invoked' || e.type === 'hook/result')).toBe(false) }) it('a UserPromptSubmit hook printing additionalContext injects it for the model', async () => { @@ -115,7 +114,7 @@ describe('hooks-claude bridge — UserPromptSubmit', () => { const adapter = new MockAdapter([textResponse('ok')]) const ctx = await harness(dir, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // The injected context reached the model and is recorded with the plugin source. @@ -140,7 +139,7 @@ describe('hooks-claude bridge — PreToolUse', () => { let ran = false ctx.tools.register(defineContentToolFixture({ name: 'danger', description: 'd', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'should not run' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'use danger' }]) + agent.followup({ content: [{ type: 'text', text: 'use danger' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(ran).toBe(false) @@ -163,7 +162,7 @@ describe('hooks-claude bridge — PreToolUse', () => { let ran = false ctx.tools.register(defineContentToolFixture({ name: 'safe', description: 's', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'ran ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'use safe' }]) + agent.followup({ content: [{ type: 'text', text: 'use safe' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(ran).toBe(true) @@ -185,7 +184,7 @@ describe('hooks-claude bridge — PostToolUse', () => { const ctx = await harness(dir, adapter) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'raw output' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const result = events(agent).find(e => e.type === 'tool/result') @@ -206,7 +205,7 @@ describe('hooks-claude bridge — PostToolUse', () => { const ctx = await harness(dir, adapter) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const log = events(agent) @@ -230,7 +229,7 @@ describe('hooks-claude bridge — PostToolUse', () => { let ran = false ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'x' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // `ask` degrades to deny today (FIXME permissions): the tool does not run and the result is isError. @@ -255,11 +254,11 @@ describe('hooks-claude bridge — SessionStart', () => { const ctx = await harness(dir, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) // session-start fires async (detached .then → agent.inject); wait for the - // injected context/message to actually land before sending, rather than a + // injected user/message to actually land before sending, rather than a // fixed sleep that flakes under load. await waitFor(() => events(agent).some(e => e.type === 'user/message' && e.data.content.some(b => b.type === 'text' && b.text.includes('project uses tabs')))) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(JSON.stringify(adapter.requests[0]!.messages)).toContain('project uses tabs') @@ -352,7 +351,7 @@ describe('hooks-claude bridge — load resilience', () => { await ctx.plugin(HooksClaude, { configPath: '/nonexistent/hooks.json' }) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // The turn ran normally — no hooks, no crash. expect(adapter.requests).toHaveLength(1) @@ -374,7 +373,7 @@ describe('hooks-claude bridge — load resilience', () => { await fiber.dispose() ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(1) // not blocked → the listener is gone expect(events(agent).some(e => e.type === 'hook/invoked')).toBe(false) // no hook ran diff --git a/packages/hooks/hooks-claude/tests/coverage-cases.ts b/packages/hooks/hooks-claude/tests/coverage-cases.ts index d2da3f6d52..912974850f 100644 --- a/packages/hooks/hooks-claude/tests/coverage-cases.ts +++ b/packages/hooks/hooks-claude/tests/coverage-cases.ts @@ -74,7 +74,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, adapter, { ...sessionRoot !== undefined ? { sessionRoot } : {} }) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('transcript'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) return { payload: JSON.parse(readFileSync(cap, 'utf8')) as { transcript_path: string }, @@ -104,7 +104,7 @@ export function defineCoverageCases(group: CoverageGroup): void { ctx.logger.warn = warn as never ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(existsSync(marker)).toBe(true) // substituted command ran }) @@ -120,7 +120,7 @@ export function defineCoverageCases(group: CoverageGroup): void { let sawArgs: unknown ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: { command: { type: 'string' } }, async execute(args) { sawArgs = args; return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // updatedInput is NOT honored — the tool ran with the ORIGINAL args. expect((sawArgs as { command?: string }).command).toBe('original') @@ -136,7 +136,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const adapter = new MockAdapter([textResponse('ran')]) const ctx = await harness(path, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // The prompt proceeded unchanged; no injected context. expect(adapter.requests).toHaveLength(1) @@ -166,7 +166,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, adapter) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const res = events(agent).find(e => e.type === 'hook/result') expect(res?.type === 'hook/result' && res.data.stderrSummary?.endsWith('…')).toBe(true) @@ -191,7 +191,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, adapter, { stderrSummaryMaxChars: 40 }) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const res = events(agent).find(e => e.type === 'hook/result') expect(res?.type === 'hook/result' && res.data.stderrSummary).toBe('x'.repeat(40) + '…') @@ -207,7 +207,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const adapter = new MockAdapter([textResponse('one'), textResponse('two')]) const ctx = await harness(path, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(2) expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('continue please') @@ -223,7 +223,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const adapter = new MockAdapter([textResponse('one'), textResponse('two')]) const ctx = await harness(path, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // A second model request ran → the empty-reason block forced continuation. expect(adapter.requests).toHaveLength(2) @@ -238,7 +238,13 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, new MockAdapter([])) // Register a fake child agent under the id the event carries. const injected: string[] = [] - const child = { id: SessionId('child-x'), inject: (content: { type: string; text?: string }[]) => { injected.push(content.map(b => b.text ?? '').join('')) }, session: { id: SessionId('child-x'), header: { id: 'child-x' } } } as unknown as Parameters[0] + const child = { + id: SessionId('child-x'), + inject: (input: { content: Array<{ type: string; text?: string }> }) => { + injected.push(input.content.map(block => block.text ?? '').join('')) + }, + session: { id: SessionId('child-x'), header: { id: 'child-x' } }, + } as unknown as Parameters[0] ctx.agents.register(child) ctx.emit(subagentCarrier(ctx), 'subagent/start', { runId: SubagentRunId('run-x'), provider: 'p', id: SessionId('child-x'), local: true }) await waitFor(() => injected.includes('child guidance')) @@ -271,7 +277,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, adapter) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'x' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const result = events(agent).find(e => e.type === 'tool/result') expect(result?.type === 'tool/result' && result.data.content.some(b => b.type === 'text' && b.text.includes('blocked by PreToolUse hook'))).toBe(true) @@ -285,7 +291,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, adapter) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const result = events(agent).find(e => e.type === 'tool/result') expect(result?.type === 'tool/result' && result.data.content.some(b => b.type === 'text' && b.text.includes('blocked by PostToolUse hook'))).toBe(true) @@ -314,7 +320,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const adapter = new MockAdapter([textResponse('no')]) const ctx = await harness(path, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(events(agent).some(e => e.type === 'turn/start')).toBe(false) }) @@ -328,7 +334,7 @@ export function defineCoverageCases(group: CoverageGroup): void { let ran = false ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'x' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // ask (no reason) → degrades to deny with the registry's generic message. expect(ran).toBe(false) @@ -343,7 +349,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, adapter) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const res = events(agent).find(e => e.type === 'hook/result') expect(res?.type === 'hook/result' && res.data.exitCode).toBe(0) @@ -368,7 +374,7 @@ export function defineCoverageCases(group: CoverageGroup): void { HooksClaude.apply(ctx, { configPath: join(d, 'hooks.json') }) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(existsSync(marker)).toBe(true) }) @@ -383,7 +389,7 @@ export function defineCoverageCases(group: CoverageGroup): void { let ran = false ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(ran).toBe(true) const res = events(agent).find(e => e.type === 'hook/result') @@ -398,7 +404,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, adapter) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const result = events(agent).find(e => e.type === 'tool/result') expect(result?.type === 'tool/result' && result.data.isError).toBe(true) @@ -417,7 +423,7 @@ export function defineCoverageCases(group: CoverageGroup): void { let ran = false ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const res = events(agent).find(e => e.type === 'hook/result') expect(res?.type === 'hook/result' && res.data.decision).toBe('stop') // recorded @@ -434,7 +440,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, adapter) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const result = events(agent).find(e => e.type === 'tool/result') expect(result?.type === 'tool/result' && result.data.isError).toBe(true) @@ -454,7 +460,7 @@ export function defineCoverageCases(group: CoverageGroup): void { let ran = false ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(ran).toBe(true) // the mismatched deny was discarded → the tool ran }) @@ -472,7 +478,7 @@ export function defineCoverageCases(group: CoverageGroup): void { // The factory create() path honors meta.cwd (the plain agentLoop.create() does not). const { SessionId } = await import('@deepseek-ai/dsh-session') const handle = await ctx.agents.create({ sessionId: SessionId('s1'), meta: { cwd: workspace }, agentOptions: { provider: 'mock', model: 'mock' } }) - handle.agent.followup([{ type: 'text', text: 'go' }]) + handle.agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, handle.agent) expect(events(handle.agent).some(e => e.type === 'user/message' && e.data.content.some(b => b.type === 'text' && b.text.includes(`dir=${workspace}`)))).toBe(true) @@ -490,7 +496,7 @@ export function defineCoverageCases(group: CoverageGroup): void { // A later listener that blocks every prompt (registered AFTER the bridge). ctx.on('agent/prompt-submit', async () => ({ kind: 'block' as const, reason: 'policy veto' })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) // the downstream block won: the model was never called, no user/message was // recorded, and the (sole, fully-blocked) prompt closed the turn `rejected` @@ -516,7 +522,7 @@ export function defineCoverageCases(group: CoverageGroup): void { }], })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const req = JSON.stringify(adapter.requests[0]!.messages) expect(req).toContain('from-bridge') @@ -543,7 +549,7 @@ export function defineCoverageCases(group: CoverageGroup): void { ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) ctx.on('tools/post-execute', async () => ({ kind: 'accept' as const, value: [{ type: 'text' as const, text: 'rewritten-result' }] })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const result = events(agent).find(e => e.type === 'tool/result') expect(result?.type === 'tool/result' && result.data.content.some(b => b.type === 'text' && b.text === 'rewritten-result')).toBe(true) @@ -565,7 +571,7 @@ export function defineCoverageCases(group: CoverageGroup): void { }], })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const contexts = events(agent).filter(event => event.type === 'user/message' && event.data.source.kind !== 'user') @@ -587,7 +593,7 @@ export function defineCoverageCases(group: CoverageGroup): void { ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) ctx.on('tools/post-execute', async () => ({ kind: 'block' as const, feedback: [{ type: 'text' as const, text: 'downstream-block' }] })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const result = events(agent).find(e => e.type === 'tool/result') expect(result?.type === 'tool/result' && result.data.isError).toBe(true) @@ -611,7 +617,7 @@ export function defineCoverageCases(group: CoverageGroup): void { bash.run = (() => Promise.reject(new Error('executor down'))) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const res = events(agent).find(e => e.type === 'hook/result') expect(res?.type === 'hook/result' && 'exitCode' in res.data).toBe(false) @@ -634,7 +640,7 @@ export function defineCoverageCases(group: CoverageGroup): void { await waitFor(() => threw) expect(threw).toBe(true) agent.inject = original - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(1) // loop survived the thrown inject }) @@ -661,7 +667,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const { SessionId } = await import('@deepseek-ai/dsh-session') const handle = await ctx.agents.create({ sessionId: SessionId('s1'), meta: { cwd: sessionDir }, agentOptions: { provider: 'mock', model: 'mock' } }) - handle.agent.followup([{ type: 'text', text: 'go' }]) + handle.agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, handle.agent) expect(existsSync(marker)).toBe(true) // the marker landed in the SESSION dir @@ -711,7 +717,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, adapter) const warn = vi.fn(); ctx.logger.warn = warn as never const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(warn).toHaveBeenCalledWith(expect.stringContaining('systemMessage')) // Not surfaced: the systemMessage text never reaches the model request. @@ -730,7 +736,7 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) // Send immediately — do NOT wait for the session-start inject. - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(1) // the turn ran regardless of hook timing }) diff --git a/packages/hooks/hooks-codex/README.md b/packages/hooks/hooks-codex/README.md index 0c8c4c16ba..5bd470a5c7 100644 --- a/packages/hooks/hooks-codex/README.md +++ b/packages/hooks/hooks-codex/README.md @@ -44,7 +44,7 @@ The hooks themselves run in the agent's session workspace: for the agent-scoped | `UserPromptSubmit` | `agent/prompt-submit` (waterfall) | `block` (exit 2) → `PromptDecision.block`; additionalContext-only → delegate via `next()` then prepend a separately sourced context to downstream `additionalContexts` | | `PreToolUse` | `tools/pre-execute` (waterfall) | `block` → `PreToolDecision.deny` (no `allow`/`ask`) | | `PostToolUse` | `tools/post-execute` (waterfall) | `block` → `block` with feedback; additionalContext-only → delegate via `next()` then prepend a separately sourced context to the downstream decision; Code Mode defers sub-call contexts until the outer `run_code` result | -| `Stop` | `agent/turn-continuation` (waterfall) | a blocking Stop hook forces `continue` with the reason as next-step steering | +| `Stop` | `agent/stopping` (serial) | a blocking Stop hook feeds its reason through `steer()`, forcing another step | A tool call's payload carries the real `tool_name` (the same value the matcher tests) and Codex's `tool_input: { command }` shape (the `command` arg when present, else `''`). The matcher subject is the tool name (`PreToolUse`/`PostToolUse`) or the session source (`SessionStart`); `UserPromptSubmit`/`Stop` ignore matchers. diff --git a/packages/hooks/hooks-codex/src/index.ts b/packages/hooks/hooks-codex/src/index.ts index 4f0768806c..b5b5f9d503 100644 --- a/packages/hooks/hooks-codex/src/index.ts +++ b/packages/hooks/hooks-codex/src/index.ts @@ -15,8 +15,9 @@ import { readFileSync } from 'node:fs' import type { Context } from 'cordis' import z from 'schemastery' -import type { AdditionalContext, Agent, PromptDecision } from '@deepseek-ai/dsh-agent' +import type { Agent, PromptDecision } from '@deepseek-ai/dsh-agent' import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' +import type { UserMessageData } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-session-persistence' import type { PostToolDecision, PreToolDecision, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools' import { @@ -102,6 +103,12 @@ export function apply(ctx: Context, config: Config): void { const detached = createDetachedRuns() ctx.effect(() => () => detached.drain(), 'hooks-codex: drain detached hook runs') + /** + * Run and fold one configured Codex hook point. + * + * A supplied turn records the hook provenance pair inside that open turn. + * Pre-turn `UserPromptSubmit` and detached lifecycle points omit it. + */ async function runPoint( point: string, matchQuery: string, @@ -163,14 +170,14 @@ export function apply(ctx: Context, config: Config): void { // TODO(hook-continue-false): `merged.stop` is logged but needs a run-level halt seam. - function contextFrom(merged: MergedHookOutcome): AdditionalContext | undefined { + function contextFrom(merged: MergedHookOutcome): UserMessageData | undefined { if (merged.additionalContext.length === 0) return undefined const content: ContentBlock[] = merged.additionalContext.map(text => ({ type: 'text', text })) return { content, source: PLUGIN_SOURCE } } /** Prepend one context without flattening downstream provenance or metadata. */ - function prependContext(ours: AdditionalContext, theirs: AdditionalContext[] | undefined): AdditionalContext[] { + function prependContext(ours: UserMessageData, theirs: UserMessageData[] | undefined): UserMessageData[] { return [ours, ...theirs ?? []] } @@ -181,7 +188,7 @@ export function apply(ctx: Context, config: Config): void { detached.track(runPoint('SessionStart', source, { ...base(ctx, agent, 'SessionStart', model), source }, { agent, plainStdoutAsContext: true, signal: detached.signal }) .then((merged) => { const context = contextFrom(merged) - if (context) agent.inject(context.content, { source: context.source }) + if (context) agent.inject({ content: context.content, source: context.source }) }) .catch((error: unknown) => { ctx.logger.warn(`hooks-codex: SessionStart hook failed: ${String(error)}`) })) /* jscpd:ignore-end */ @@ -189,8 +196,12 @@ export function apply(ctx: Context, config: Config): void { // UserPromptSubmit → PromptDecision. Codex supports block, not allow or ask. ctx.on('agent/prompt-submit', async (agent, content, _source, signal, next): Promise => { - const turn = lastTurn(agent) - const merged = await runPoint('UserPromptSubmit', '', { ...turnBase(ctx, agent, 'UserPromptSubmit', model), prompt: blocksToText(content) }, { agent, turn, plainStdoutAsContext: true, signal }) + const payload = { + ...base(ctx, agent, 'UserPromptSubmit', model), + turn_id: String(lastTurn(agent) + 1), + prompt: blocksToText(content), + } + const merged = await runPoint('UserPromptSubmit', '', payload, { agent, plainStdoutAsContext: true, signal }) /* jscpd:ignore-start */ if (merged.decision === 'deny') return { kind: 'block', reason: merged.reason ?? 'blocked by UserPromptSubmit hook' } // Context alone is not a veto: DELEGATE so a later prompt-submit listener can @@ -249,7 +260,7 @@ export function apply(ctx: Context, config: Config): void { // empty stderr) still forces it — fall back to a generic steering line // rather than letting the turn stop. const text = merged.reason ?? 'continue: blocked by Stop hook' - agent.steer([{ type: 'text', text }], { source: PLUGIN_SOURCE }) + agent.steer({ content: [{ type: 'text', text }], source: PLUGIN_SOURCE }) } }) } @@ -263,9 +274,7 @@ export function apply(ctx: Context, config: Config): void { function lastTurn(agent: Agent | undefined): number { if (!agent) return 0 const last = [...agent.session.events].findLast(e => e.type === 'turn/start') - /* v8 ignore next -- the `: 0` arm is a defensive fallback: when an agent is - present, lastTurn is only called from the mid-turn seams, which always run - inside an open turn, so `last` is always a turn/start here. */ + /* v8 ignore next -- agent-present turnBase callers are tool/stop seams inside an open turn. */ return last?.type === 'turn/start' ? last.data.turn : 0 } diff --git a/packages/hooks/hooks-codex/tests/bridge.spec.ts b/packages/hooks/hooks-codex/tests/bridge.spec.ts index 64ec92af87..93c1f29975 100644 --- a/packages/hooks/hooks-codex/tests/bridge.spec.ts +++ b/packages/hooks/hooks-codex/tests/bridge.spec.ts @@ -73,7 +73,7 @@ describe('hooks-codex bridge', () => { let ran = false ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'no' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'run ls' }]) + agent.followup({ content: [{ type: 'text', text: 'run ls' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(ran).toBe(false) @@ -94,7 +94,7 @@ describe('hooks-codex bridge', () => { const adapter = new MockAdapter([textResponse('first answer'), textResponse('second answer after goal')]) const ctx = await harness(dir, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(2) @@ -111,7 +111,7 @@ describe('hooks-codex bridge', () => { const adapter = new MockAdapter([textResponse('must not run')]) const ctx = await harness(dir, adapter) const agent = ctx.agentLoop.create(SessionId('cancel-prompt-hook'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'cancel the hook' }]) + agent.followup({ content: [{ type: 'text', text: 'cancel the hook' }], source: { kind: 'user' } }) await waitFor(() => existsSync(marker)) const pid = Number(readFileSync(pidFile, 'utf8').trim()) @@ -122,7 +122,7 @@ describe('hooks-codex bridge', () => { expect(() => process.kill(pid, 0)).toThrow() expect(adapter.requests).toHaveLength(0) expect(events(agent).some(event => event.type === 'turn/start')).toBe(false) - expect(events(agent).some(event => event.type === 'hook/result' && event.data.point === 'UserPromptSubmit')).toBe(true) + expect(events(agent).some(event => event.type === 'hook/invoked' || event.type === 'hook/result')).toBe(false) }) it('only the five bridge-supported Codex events are honored — a SubagentStop entry is ignored', async () => { @@ -133,7 +133,7 @@ describe('hooks-codex bridge', () => { const adapter = new MockAdapter([textResponse('fine')]) const ctx = await harness(dir, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(1) }) @@ -143,7 +143,7 @@ describe('hooks-codex bridge', () => { const adapter = new MockAdapter([textResponse('ok')]) const ctx = await harness(dir, adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(1) }) @@ -163,7 +163,7 @@ describe('hooks-codex bridge', () => { await fiber.dispose() ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(1) // not blocked → the listener is gone expect(events(agent).some(e => e.type === 'hook/invoked')).toBe(false) // no hook ran diff --git a/packages/hooks/hooks-codex/tests/coverage-cases.ts b/packages/hooks/hooks-codex/tests/coverage-cases.ts index e75213bfd6..9f4c05de4b 100644 --- a/packages/hooks/hooks-codex/tests/coverage-cases.ts +++ b/packages/hooks/hooks-codex/tests/coverage-cases.ts @@ -65,7 +65,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(path, adapter, { ...sessionRoot !== undefined ? { sessionRoot } : {} }) ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('transcript'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) return { payload: JSON.parse(readFileSync(cap, 'utf8')) as { transcript_path: string | null }, @@ -84,7 +84,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const adapter = new MockAdapter([textResponse('no')]) const ctx = await harness(join(d, 'hooks.json'), adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(0) expect(events(agent).some(e => e.type === 'turn/start')).toBe(false) }) @@ -95,7 +95,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const adapter = new MockAdapter([textResponse('ok')]) const ctx = await harness(join(d, 'hooks.json'), adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(JSON.stringify(adapter.requests[0]!.messages)).toContain('ctx-x') }) @@ -108,7 +108,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) ctx.on('agent/prompt-submit', async () => ({ kind: 'block' as const, reason: 'policy veto' })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(0) expect(events(agent).some(e => e.type === 'user/message')).toBe(false) expect(events(agent).some(e => e.type === 'turn/start')).toBe(false) @@ -128,7 +128,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro }], })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const req = JSON.stringify(adapter.requests[0]!.messages) expect(req).toContain('from-bridge') expect(req).toContain('from-downstream') @@ -150,7 +150,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) ctx.on('tools/post-execute', async () => ({ kind: 'accept' as const, value: [{ type: 'text' as const, text: 'rewritten-result' }] })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const result = events(agent).find(e => e.type === 'tool/result') expect(result?.type === 'tool/result' && result.data.content.some(b => b.type === 'text' && b.text === 'rewritten-result')).toBe(true) expect(events(agent).some(e => e.type === 'user/message' && e.data.source.kind !== 'user' && e.data.content.some(b => b.type === 'text' && b.text.includes('bridge-note')))).toBe(true) @@ -170,7 +170,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro }], })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const contexts = events(agent).filter(event => event.type === 'user/message' && event.data.source.kind !== 'user') expect(contexts.map(event => event.type === 'user/message' && event.data.source)).toEqual([ @@ -187,7 +187,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) ctx.on('tools/post-execute', async () => ({ kind: 'block' as const, feedback: [{ type: 'text' as const, text: 'downstream-block' }] })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const result = events(agent).find(e => e.type === 'tool/result') expect(result?.type === 'tool/result' && result.data.isError).toBe(true) expect(result?.type === 'tool/result' && result.data.content.some(b => b.type === 'text' && b.text.includes('downstream-block'))).toBe(true) @@ -202,7 +202,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) await waitFor(() => events(agent).some(e => e.type === 'user/message' && e.data.content.some(b => b.type === 'text' && b.text.includes('start-ctx')))) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(JSON.stringify(adapter.requests[0]!.messages)).toContain('start-ctx') }) @@ -213,7 +213,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const r = events(agent).find(e => e.type === 'tool/result') expect(r?.type === 'tool/result' && r.data.isError).toBe(true) expect(r?.type === 'tool/result' && r.data.content.some(b => b.type === 'text' && b.text.includes('blocked by PostToolUse hook'))).toBe(true) @@ -226,7 +226,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(events(agent).some(e => e.type === 'user/message' && e.data.source.kind !== 'user' && e.data.content.some(b => b.type === 'text' && b.text.includes('post-ctx')))).toBe(true) }) }) @@ -240,7 +240,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro let ran = false ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(ran).toBe(true) // clean-exit hook allows; commandOf returned '' }) @@ -251,7 +251,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const res = events(agent).find(e => e.type === 'hook/result') expect(res?.type === 'hook/result' && res.data.exitCode).toBe(0) expect(res?.type === 'hook/result' && 'stderrSummary' in res.data).toBe(false) @@ -264,7 +264,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const res = events(agent).find(e => e.type === 'hook/result') expect(res?.type === 'hook/result' && res.data.stderrSummary?.endsWith('…')).toBe(true) expect(res?.type === 'hook/result' && res.data.stderrSummary?.length).toBe(501) // default 500-char cap + ellipsis @@ -287,7 +287,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter, { stderrSummaryMaxChars: 40 }) ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const res = events(agent).find(e => e.type === 'hook/result') expect(res?.type === 'hook/result' && res.data.stderrSummary).toBe('x'.repeat(40) + '…') }) @@ -310,7 +310,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro HooksCodex.apply(ctx, { configPath: join(d, 'hooks.json') }) ctx.llm.registerAdapter(['mock'], adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(existsSync(marker)).toBe(true) expect(warn).toHaveBeenCalledWith(expect.stringContaining('async hook')) }) @@ -323,7 +323,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro let ran = false ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(ran).toBe(true) }) @@ -338,7 +338,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) await waitFor(() => existsSync(marker)) // the clean no-output hook has finished - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(events(agent).some(e => e.type === 'user/message' && e.data.source.kind !== 'user')).toBe(false) }) @@ -364,7 +364,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro let ran = false ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(ran).toBe(true) }) @@ -377,7 +377,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro let ran = false ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(ran).toBe(true) // matcher didn't match → no hook ran → tool proceeded expect(events(agent).some(e => e.type === 'hook/invoked')).toBe(false) }) @@ -393,7 +393,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro let ran = false ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const res = events(agent).find(e => e.type === 'hook/result') expect(res?.type === 'hook/result' && res.data.decision).toBe('stop') // recorded expect(ran).toBe(true) // NOT honored: the tool still ran (halt is deferred) @@ -406,7 +406,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const r = events(agent).find(e => e.type === 'tool/result') expect(r?.type === 'tool/result' && r.data.content.some(b => b.type === 'text' && b.text.includes('blocked by PreToolUse hook'))).toBe(true) }) @@ -418,7 +418,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const r = events(agent).find(e => e.type === 'tool/result') expect(r?.type === 'tool/result' && r.data.isError).toBe(true) expect(r?.type === 'tool/result' && r.data.content.some(b => b.type === 'text' && b.text.includes('bad'))).toBe(true) @@ -435,7 +435,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'number' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const payload = JSON.parse(readFileSync(cap, 'utf8')) as { tool_input: { command: string } } expect(payload.tool_input.command).toBe('') }) @@ -471,7 +471,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro ctx.bash.run = (() => Promise.reject(new Error('executor down'))) ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const res = events(agent).find(e => e.type === 'hook/result') expect(res?.type === 'hook/result' && 'exitCode' in res.data).toBe(false) }) @@ -487,7 +487,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const adapter = new MockAdapter([textResponse('one'), textResponse('two')]) const ctx = await harness(join(d, 'hooks.json'), adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(2) // empty-reason block forced continuation expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('blocked by Stop hook') }) @@ -500,7 +500,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const adapter = new MockAdapter([textResponse('ok')]) const ctx = await harness(join(d, 'hooks.json'), adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(JSON.stringify(adapter.requests[0]!.messages)).toContain('extra guidance from a plain hook') }) @@ -528,7 +528,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const adapter = new MockAdapter([textResponse('ok')]) const ctx = await harness(join(d, 'hooks.json'), adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(adapter.requests).toHaveLength(1) // exit 1 is non-blocking → the turn ran expect(JSON.stringify(adapter.requests[0]!.messages)).not.toContain('stale') }) @@ -541,7 +541,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) await waitFor(() => events(agent).some(e => e.type === 'user/message' && e.data.content.some(b => b.type === 'text' && b.text.includes('session preamble')))) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(JSON.stringify(adapter.requests[0]!.messages)).toContain('session preamble') }) @@ -553,7 +553,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const adapter = new MockAdapter([textResponse('ok')]) const ctx = await harness(join(d, 'hooks.json'), adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(JSON.stringify(adapter.requests[0]!.messages)).not.toContain('unrelated') }) @@ -568,7 +568,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) ctx.tools.register(defineContentToolFixture({ name: 'shell', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) const payload = JSON.parse(readFileSync(cap, 'utf8')) as { tool_name: string; tool_input: { command: string } } expect(payload.tool_name).toBe('shell') expect(payload.tool_input.command).toBe('ls') @@ -584,7 +584,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro let ran = false ctx.tools.register(defineContentToolFixture({ name: 'shell', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'ok' }] } })) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(ran).toBe(false) // the matcher fired → the hook denied the tool expect(events(agent).some(e => e.type === 'hook/invoked' && e.data.point === 'PreToolUse')).toBe(true) }) @@ -596,7 +596,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), adapter) const warn = vi.fn(); ctx.logger.warn = warn as never const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'go' }]); await waitForIdle(ctx, agent) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }); await waitForIdle(ctx, agent) expect(warn).toHaveBeenCalledWith(expect.stringContaining('systemMessage')) expect(JSON.stringify(adapter.requests[0]!.messages)).not.toContain('heads up') }) @@ -619,7 +619,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) const { SessionId } = await import('@deepseek-ai/dsh-session') const handle = await ctx.agents.create({ sessionId: SessionId('s1'), meta: { cwd: sessionDir }, agentOptions: { provider: 'mock', model: 'mock' } }) - handle.agent.followup([{ type: 'text', text: 'go' }]) + handle.agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await waitForIdle(ctx, handle.agent) expect(existsSync(marker)).toBe(true) expect(readFileSync(marker, 'utf8').trim().endsWith(sessionDir.split('/').pop()!)).toBe(true) diff --git a/packages/host/runtime/src/api-proxy.ts b/packages/host/runtime/src/api-proxy.ts index 5e0d6b9641..02ed05270a 100644 --- a/packages/host/runtime/src/api-proxy.ts +++ b/packages/host/runtime/src/api-proxy.ts @@ -447,8 +447,8 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro // The rpcId rides MessageSource into user/message (merge declaration in api/sessions.ts; provisional correlation). const source: MessageSource = { kind: 'user', rpcId: request.rpcId } try { - if (mode === 'steer') agent.steer(content, { source }) - else agent.followup(content, { source }) + if (mode === 'steer') agent.steer({ content, source }) + else agent.followup({ content, source }) } catch (error: unknown) { // A synchronous throw from send/steer means disposed or invalid input; surface as agent-busy with the reason attached. return err(request, { code: 'agent-busy', message: 'prompt rejected', details: { reason: String(error) } }) diff --git a/packages/host/runtime/tests/host-runtime.spec.ts b/packages/host/runtime/tests/host-runtime.spec.ts index bc6fd44d8c..0c06830e2c 100644 --- a/packages/host/runtime/tests/host-runtime.spec.ts +++ b/packages/host/runtime/tests/host-runtime.spec.ts @@ -386,7 +386,7 @@ describe('sessions.prompt / cancel', () => { const { api, ctx } = running const { sessionId } = expectOk(await api.sessions.create(request({}))) const agent = ctx.agents.get(sessionId) as Agent - agent.followup([{ type: 'text', text: 'run forever' }]) + agent.followup({ content: [{ type: 'text', text: 'run forever' }], source: { kind: 'user' } }) expectOk(await api.sessions.cancel(request({ sessionId }))) const missing = await api.sessions.cancel(request({ sessionId: 'session-none' as SessionId })) @@ -405,7 +405,7 @@ describe('sessions.history', () => { const { sessionId } = expectOk(await first.api.sessions.create(request({}))) const agent = first.ctx.agents.get(sessionId) as Agent const idle = waitForIdle(first.ctx, agent) - agent.followup([{ type: 'text', text: 'save me' }]) + agent.followup({ content: [{ type: 'text', text: 'save me' }], source: { kind: 'user' } }) await idle const titleEvent = await appendTitle(first.ctx, agent, 'Persisted title') await first.dispose() @@ -454,7 +454,7 @@ describe('sessions.history', () => { const agent = ctx.agents.get(sessionId) as Agent for (const text of ['q1', 'q2', 'q3']) { const idle = waitForIdle(ctx, agent) - agent.followup([{ type: 'text', text }]) + agent.followup({ content: [{ type: 'text', text }], source: { kind: 'user' } }) await idle } @@ -528,7 +528,7 @@ describe('events streams', () => { const agent = ctx.agents.get(sessionId) as Agent const idle = waitForIdle(ctx, agent) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await idle const live = await stream.next() expect((live.value as RpcRequest).payload.type).toBe('session/event') @@ -591,7 +591,7 @@ describe('events streams', () => { const agent = ctx.agents.get(sessionId) as Agent const idle = waitForIdle(ctx, agent) - agent.followup([{ type: 'text', text: 'run' }]) + agent.followup({ content: [{ type: 'text', text: 'run' }], source: { kind: 'user' } }) await idle const runningFrame = await stream.next() expect((runningFrame.value as RpcRequest).payload).toMatchObject({ type: 'host/session-status', running: true }) diff --git a/packages/llm/llm-retry/tests/loader-composition.spec.ts b/packages/llm/llm-retry/tests/loader-composition.spec.ts index 240693c614..5c64b36097 100644 --- a/packages/llm/llm-retry/tests/loader-composition.spec.ts +++ b/packages/llm/llm-retry/tests/loader-composition.spec.ts @@ -101,7 +101,7 @@ describe('real Loader composition', () => { const adapter = new TransientOnceAdapter() loaded.llm.registerAdapter(['mock'], adapter) const agent = loaded.agentLoop.create(SessionId('loader-retry'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'recover' }]) + agent.followup({ content: [{ type: 'text', text: 'recover' }], source: { kind: 'user' } }) await agent.whenIdle() expect(adapter.requests).toBe(2) diff --git a/packages/llm/llm-retry/tests/retry.spec.ts b/packages/llm/llm-retry/tests/retry.spec.ts index c4c87388b0..d037b62e20 100644 --- a/packages/llm/llm-retry/tests/retry.spec.ts +++ b/packages/llm/llm-retry/tests/retry.spec.ts @@ -138,7 +138,7 @@ describe('bounded transient retry policy', () => { }) }) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) const event = await scheduled expect(event.data).toEqual({ @@ -187,7 +187,7 @@ describe('bounded transient retry policy', () => { const agent = context.agentLoop.create(SessionId('retry-partial'), { provider: 'mock', model: 'mock' }) const scheduled = waitForRetry(context, agent, 1) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await scheduled const idle = waitForIdle(context, agent) await vi.advanceTimersByTimeAsync(500) @@ -224,7 +224,7 @@ describe('bounded transient retry policy', () => { const agent = context.agentLoop.create(SessionId('retry-exhausted'), { provider: 'mock', model: 'mock' }) const first = waitForRetry(context, agent, 1) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) expect((await first).data.delayMs).toBe(450) const second = waitForRetry(context, agent, 2) @@ -255,14 +255,14 @@ describe('bounded transient retry policy', () => { const agent = context.agentLoop.create(SessionId('retry-reset'), { provider: 'mock', model: 'mock' }) const firstRetry = waitForRetry(context, agent, 1) - agent.followup([{ type: 'text', text: 'first' }]) + agent.followup({ content: [{ type: 'text', text: 'first' }], source: { kind: 'user' } }) await firstRetry const firstIdle = waitForIdle(context, agent) await vi.advanceTimersByTimeAsync(500) await firstIdle const secondRetry = waitForRetry(context, agent, 1) - agent.followup([{ type: 'text', text: 'second' }]) + agent.followup({ content: [{ type: 'text', text: 'second' }], source: { kind: 'user' } }) await secondRetry const secondIdle = waitForIdle(context, agent) await vi.advanceTimersByTimeAsync(500) @@ -296,7 +296,7 @@ describe('bounded transient retry policy', () => { }) const firstRetry = waitForRetry(context, agent, 1) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await firstRetry const secondRetry = waitForRetry(context, agent, 1) await vi.advanceTimersByTimeAsync(500) @@ -324,7 +324,7 @@ describe('bounded transient retry policy', () => { const agent = context.agentLoop.create(SessionId('retry-zero-delay'), { provider: 'mock', model: 'mock' }) const scheduled = waitForRetry(context, agent, 1) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) expect((await scheduled).data.delayMs).toBe(0) const idle = waitForIdle(context, agent) @@ -342,7 +342,7 @@ describe('bounded transient retry policy', () => { ;({ ctx: context } = await harness(accepted, { jitterRatio: 1 })) const acceptedAgent = context.agentLoop.create(SessionId('retry-after-accepted'), { provider: 'mock', model: 'mock' }) const scheduled = waitForRetry(context, acceptedAgent, 1) - acceptedAgent.followup([{ type: 'text', text: 'go' }]) + acceptedAgent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) expect((await scheduled).data.delayMs).toBe(2_000) const acceptedIdle = waitForIdle(context, acceptedAgent) await vi.advanceTimersByTimeAsync(2_000) @@ -356,7 +356,7 @@ describe('bounded transient retry policy', () => { ;({ ctx: context } = await harness(rejected)) const rejectedAgent = context.agentLoop.create(SessionId('retry-after-rejected'), { provider: 'mock', model: 'mock' }) const rejectedIdle = waitForIdle(context, rejectedAgent) - rejectedAgent.followup([{ type: 'text', text: 'go' }]) + rejectedAgent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await rejectedIdle expect(rejected.requests).toHaveLength(1) expect(rejectedAgent.session.events.some(event => event.type === 'llm/retry')).toBe(false) @@ -368,7 +368,7 @@ describe('bounded transient retry policy', () => { ;({ ctx: context } = await harness(adapter)) const agent = context.agentLoop.create(SessionId('retry-auth'), { provider: 'mock', model: 'mock' }) const idle = waitForIdle(context, agent) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await idle expect(adapter.requests).toHaveLength(1) expect(agent.session.events.some(event => event.type === 'llm/retry')).toBe(false) @@ -385,7 +385,7 @@ describe('bounded transient retry policy', () => { context = mounted.ctx const agent = context.agentLoop.create(SessionId('retry-hmr'), { provider: 'mock', model: 'mock' }) const scheduled = waitForRetry(context, agent, 1) - agent.followup([{ type: 'text', text: 'go' }]) + agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await scheduled const idle = waitForIdle(context, agent) await mounted.retryFiber.dispose() diff --git a/packages/llm/token-meter/tests/token-meter.spec.ts b/packages/llm/token-meter/tests/token-meter.spec.ts index e3618474e2..c41952bc8c 100644 --- a/packages/llm/token-meter/tests/token-meter.spec.ts +++ b/packages/llm/token-meter/tests/token-meter.spec.ts @@ -183,7 +183,7 @@ describe('TokenMeterService pricing', () => { expect(snapshot.nodes).toHaveLength(1) }) - it('prices header, prefix, tools, and surface when no reusable usage exists', () => { + it('prices header, tools, and surface when no reusable usage exists', () => { const service = meter() const session = new Session(SessionId('heuristic')) session.append('user/message', { @@ -192,7 +192,6 @@ describe('TokenMeterService pricing', () => { }, { surfaceOp: 'append' }) appendHeader(session, header('deepseek-v4-flash', { system: 'system', - messagePrefix: [textMessage('prefix')], tools: [{ name: 'read', description: 'read', parameters: { type: 'object' } }], })) const result = service.measure(session) @@ -354,10 +353,6 @@ describe('replay anchors and surface folds', () => { ...anchoredHeader, config: { ...anchoredHeader.config, temperature: 0.2 }, }).baseline.kind).toBe('estimated') - expect(service.measure(session, { - ...anchoredHeader, - messagePrefix: [textMessage('prefix')], - }).baseline.kind).toBe('estimated') expect(service.measure(session, { ...anchoredHeader, tools: [{ name: 'read', description: 'read', parameters: { type: 'object' } }], diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts index 7e051b7d5a..b9a48631d6 100644 --- a/packages/plan/plan-mode/src/index.ts +++ b/packages/plan/plan-mode/src/index.ts @@ -213,7 +213,7 @@ export class PlanModeService extends Service { return { kind: 'success', text: 'Plan mode is already inactive.' } } this.set(agent, true) - if (message !== '') agent.steer([{ type: 'text', text: message }]) + if (message !== '') agent.steer({ content: [{ type: 'text', text: message }], source: { kind: 'user' } }) return { kind: 'success', text: 'Entering plan mode (applies from the next step). Use /plan off to leave.', diff --git a/packages/plan/plan-mode/tests/integration.spec.ts b/packages/plan/plan-mode/tests/integration.spec.ts index 10b145c2d6..1246543094 100644 --- a/packages/plan/plan-mode/tests/integration.spec.ts +++ b/packages/plan/plan-mode/tests/integration.spec.ts @@ -75,7 +75,7 @@ describe('plan mode through the agent loop', () => { // the first prompt-submit, BEFORE the first assembly. ctx.planMode.set(agent, true) - agent.followup([{ type: 'text', text: 'explore the repo' }]) + agent.followup({ content: [{ type: 'text', text: 'explore the repo' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const log = agent.session.events @@ -103,14 +103,14 @@ describe('plan mode through the agent loop', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('it-plan-flip'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'hello' }]) + agent.followup({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) expect(foldPlanMode(agent.session.events)).toBe(false) const first = findEvent(agent.session.events, 'request/header') expect(first.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write']) ctx.planMode.set(agent, true) - agent.followup([{ type: 'text', text: 'now plan' }]) + agent.followup({ content: [{ type: 'text', text: 'now plan' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const log = agent.session.events @@ -143,7 +143,7 @@ describe('plan mode through the agent loop', () => { }) const idle = waitForIdle(ctx, agent) - agent.followup([{ type: 'text', text: 'plan after the transient failure' }]) + agent.followup({ content: [{ type: 'text', text: 'plan after the transient failure' }], source: { kind: 'user' } }) await idle expect(adapter.requests).toHaveLength(2) diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts index c944220ba1..c5111482a0 100644 --- a/packages/plan/plan-mode/tests/plan-mode.spec.ts +++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts @@ -516,7 +516,10 @@ describe('/plan', () => { text: 'Entering plan mode (applies from the next step). Use /plan off to leave.', }) expect(ctx.planMode.get(messageAgent)).toEqual({ active: false, pending: true }) - expect(messageSteer).toHaveBeenCalledExactlyOnceWith([{ type: 'text', text: 'draft the migration' }]) + expect(messageSteer).toHaveBeenCalledExactlyOnceWith({ + content: [{ type: 'text', text: 'draft the migration' }], + source: { kind: 'user' }, + }) }) it('leaves active plan mode, cancels a pending entry, and treats inactive exit as idempotent', async () => { diff --git a/packages/session-persistence/session-checkpoint-policy/tests/fixtures/crash-child.ts b/packages/session-persistence/session-checkpoint-policy/tests/fixtures/crash-child.ts index 412ff8203d..a7f937cd9b 100644 --- a/packages/session-persistence/session-checkpoint-policy/tests/fixtures/crash-child.ts +++ b/packages/session-persistence/session-checkpoint-policy/tests/fixtures/crash-child.ts @@ -56,5 +56,5 @@ const handle = await ctx.agents.create({ sessionId: SessionId('semantic-checkpoint-crash'), agentOptions: { provider: 'crash', model: 'crash' }, }) -handle.agent.followup([{ type: 'text', text: 'exercise the crash boundary' }]) +handle.agent.followup({ content: [{ type: 'text', text: 'exercise the crash boundary' }], source: { kind: 'user' } }) await waitForCrash() diff --git a/packages/session-query/session-query/tests/search-helpers.spec.ts b/packages/session-query/session-query/tests/search-helpers.spec.ts index fef9e25ad0..80f5da584f 100644 --- a/packages/session-query/session-query/tests/search-helpers.spec.ts +++ b/packages/session-query/session-query/tests/search-helpers.spec.ts @@ -46,21 +46,19 @@ describe('session-query semantic extraction', () => { { type: 'assistant/message', seq: 1, time: 2, data: { turn: 1, step: 1, content: messageContent, provenance: { provider: 'mock', model: 'mock' } }, surfaceOp: 'append' }, { type: 'user/message', seq: 2, time: 3, data: { content: messageContent, source: { kind: 'plugin', plugin: 'test' } }, surfaceOp: 'append' }, { type: 'steering/message', seq: 3, time: 4, data: { turn: 1, content: messageContent, source: { kind: 'user' } }, surfaceOp: 'append' }, - { type: 'prompt/blocked', seq: 4, time: 5, data: { content: [{ type: 'text', text: 'unsafe' }], source: { kind: 'user' }, reason: 'policy' } }, - { type: 'tool/call', seq: 5, time: 6, data: { turn: 1, step: 1, callId, name: 'bash', arguments: '{"cmd":"pwd"}' } }, - { type: 'tool/result', seq: 6, time: 7, data: { turn: 1, step: 1, callId, content: [{ type: 'text', text: 'failed' }], isError: true, error: { name: 'Oops', code: 'E_OOPS' } }, surfaceOp: 'append' }, - { type: 'tool/result', seq: 7, time: 8, data: { turn: 1, step: 1, callId, content: [], isError: false }, surfaceOp: 'append' }, - { type: 'todo/write', seq: 8, time: 9, data: { todos: [{ status: 'in_progress', content: 'ship search' }] } }, + { type: 'tool/call', seq: 4, time: 5, data: { turn: 1, step: 1, callId, name: 'bash', arguments: '{"cmd":"pwd"}' } }, + { type: 'tool/result', seq: 5, time: 6, data: { turn: 1, step: 1, callId, content: [{ type: 'text', text: 'failed' }], isError: true, error: { name: 'Oops', code: 'E_OOPS' } }, surfaceOp: 'append' }, + { type: 'tool/result', seq: 6, time: 7, data: { turn: 1, step: 1, callId, content: [], isError: false }, surfaceOp: 'append' }, + { type: 'todo/write', seq: 7, time: 8, data: { todos: [{ status: 'in_progress', content: 'ship search' }] } }, ] for (const event of events.slice(0, 4)) { expect(extractSessionEventText(event)).toBe('visible\nthought\nread\n{"path":"a"}\nnested') } - expect(extractSessionEventText(events[4]!)).toBe('unsafe\npolicy') - expect(extractSessionEventText(events[5]!)).toBe('bash\n{"cmd":"pwd"}') - expect(extractSessionEventText(events[6]!)).toBe('failed\nOops\nE_OOPS') - expect(extractSessionEventText(events[7]!)).toBe('') - expect(extractSessionEventText(events[8]!)).toBe('in_progress\nship search') + expect(extractSessionEventText(events[4]!)).toBe('bash\n{"cmd":"pwd"}') + expect(extractSessionEventText(events[5]!)).toBe('failed\nOops\nE_OOPS') + expect(extractSessionEventText(events[6]!)).toBe('') + expect(extractSessionEventText(events[7]!)).toBe('in_progress\nship search') }) it('extracts meaningful turn outcomes and skips structural or unknown events', () => { @@ -69,7 +67,6 @@ describe('session-query semantic extraction', () => { [{ kind: 'error', step: 2, message: 'boom' }, 'error\nboom'], [{ kind: 'error', step: 2, failure: { message: 'provider boom', code: 'SERVER' } }, 'error\nprovider boom\nSERVER'], [{ kind: 'aborted' }, 'aborted'], - [{ kind: 'rejected', reason: 'denied' }, 'rejected\ndenied'], [{ kind: 'disposed' }, 'disposed'], [{ kind: 'max-tokens' }, 'max-tokens'], [{ kind: 'interrupted' }, 'interrupted'], diff --git a/packages/skill/tool-skill/README.md b/packages/skill/tool-skill/README.md index 578c6e7300..63c2952b09 100644 --- a/packages/skill/tool-skill/README.md +++ b/packages/skill/tool-skill/README.md @@ -4,11 +4,11 @@ The model-facing skill catalog and `skill` tool. Requires `ctx.tools` and `ctx.skills` (`inject: ['tools', 'skills']`). -## Session-prefix catalog +## Session catalog -The plugin contributes one user-role `` catalog through `agent/session-prefix`. It resolves skills for the calling session's cwd, forwards the prefix abort signal to discovery, and lists only sorted `name` and `description` entries; skill bodies, paths, sources, providers, and `whenToUse` hints remain outside the catalog. The catalog is omitted when no model-invocable skills are available, and also when that agent's tool view restricts away the shipped `skill` tool or resolves a same-name scoped shadow instead. This exact-definition check keeps prompt guidance, the model-visible schema, and executable dispatch aligned. +The plugin injects one durable user-role `` catalog at the first `agent/step` of a live session. It resolves skills for the calling session's cwd, forwards the step abort signal to discovery, and lists only sorted `name` and `description` entries; skill bodies, paths, sources, providers, and `whenToUse` hints remain outside the catalog. The catalog is omitted when no model-invocable skills are available, and also when that agent's tool view restricts away the shipped `skill` tool or resolves a same-name scoped shadow instead. This exact-definition check keeps prompt guidance, the model-visible schema, and executable dispatch aligned. -`catalogDescriptionMaxLength` controls normalized, XML-escaped catalog descriptions. Its default is `500` and values must be integers of at least `3`, which reserves room for a truncation ellipsis. The [session-prefix Agent Note](../../../.agents/notes/implemented/feature/2026-07-07-session-prefix.md) defines the request-only, header-logged lifecycle of this message. +`catalogDescriptionMaxLength` controls normalized, XML-escaped catalog descriptions. Its default is `500` and values must be integers of at least `3`, which reserves room for a truncation ellipsis. The catalog is a sourced `user/message` injected before the first request and retained in ordinary session history. ## Tool: `skill` @@ -26,11 +26,11 @@ The tool does not call `agent.inject()` in v1. Its result is already recorded as ## Model Experience -### Session prefix +### Session catalog #### What the model sees -If model-invocable skills exist and this exact `skill` tool is visible, the agent receives the catalog template below, with one data-dependent entry per sorted skill. The catalog is a frozen user-role session prefix. +If model-invocable skills exist and this exact `skill` tool is visible, the agent receives the catalog template below, with one data-dependent entry per sorted skill. The catalog is one durable user-role message. ##### Skill catalog template @@ -52,7 +52,7 @@ Repeated input cost scales with skill count and `catalogDescriptionMaxLength`; n #### KV Cache effect -Prefix-stable within a loop instance once the session prefix is composed. A new or resumed instance with different providers, skills, descriptions, visibility, or catalog limits may invalidate reuse from the first changed catalog token. +Append-only after the existing reusable prefix. A new or resumed instance with different providers, skills, descriptions, visibility, or catalog limits may affect cache reuse from the newly appended catalog position. ### Tool schema diff --git a/packages/skill/tool-skill/src/index.ts b/packages/skill/tool-skill/src/index.ts index ae9439e060..7f3d7c0b7e 100644 --- a/packages/skill/tool-skill/src/index.ts +++ b/packages/skill/tool-skill/src/index.ts @@ -29,7 +29,7 @@ export const Config: z = z.object({ /** * Register the model-facing skill loader and its visibility-matched - * session-prefix catalog. The catalog is emitted only when the calling agent + * durable session catalog. The catalog is emitted only when the calling agent * resolves this plugin's exact tool registration; a restriction or scoped * same-name shadow therefore removes both the schema and its call guidance. */ @@ -126,7 +126,7 @@ export function apply(ctx: Context, config: Config = {}): void { const skills = await ctx.skills.list({ cwd: agent.session.header.cwd, signal }) if (skills.length > 0) { const catalog = renderCatalogMessage(skills, catalogDescriptionMaxLength) - agent.inject(catalog.content, { source: { kind: 'plugin', plugin: 'dsh-tool-skill' } }) + agent.inject({ content: catalog.content, source: { kind: 'plugin', plugin: 'dsh-tool-skill' } }) } catalogLoaded.add(agent.session) }) diff --git a/packages/skill/tool-skill/tests/tool-skill.spec.ts b/packages/skill/tool-skill/tests/tool-skill.spec.ts index 3c7f7f86f5..5d190e1f9c 100644 --- a/packages/skill/tool-skill/tests/tool-skill.spec.ts +++ b/packages/skill/tool-skill/tests/tool-skill.spec.ts @@ -48,11 +48,8 @@ function agentForCwd(cwd: string): Agent { send: () => AgentMessageId('stub'), followup: () => AgentMessageId('stub'), steer: () => AgentMessageId('stub'), - inject(content, options) { - session.append('user/message', { - content, - source: options?.source ?? { kind: 'user' }, - }, { surfaceOp: 'append' }) + inject(input) { + session.append('user/message', input, { surfaceOp: 'append' }) return AgentMessageId('stub') }, cancel() {}, @@ -147,9 +144,7 @@ describe('dsh-tool-skill', () => { content: 'A body.', }) ctx.on('agent/step', (agent) => { - agent.inject([{ type: 'text', text: 'later contribution' }], { - source: { kind: 'plugin', plugin: 'later-contribution' }, - }) + agent.inject({ content: [{ type: 'text', text: 'later contribution' }], source: { kind: 'plugin', plugin: 'later-contribution' } }) }) const prefix = await composePrefix(ctx, '/workspace') diff --git a/packages/subagent/subagent-fork/tests/multi-subagent.spec.ts b/packages/subagent/subagent-fork/tests/multi-subagent.spec.ts index 6882d324fc..86e2fd82d6 100644 --- a/packages/subagent/subagent-fork/tests/multi-subagent.spec.ts +++ b/packages/subagent/subagent-fork/tests/multi-subagent.spec.ts @@ -64,7 +64,7 @@ describe('multi-subagent coexistence (spawn + fork on one context)', () => { ]) // Parent does one real turn first, so the fork has a completed turn to seed. - parent.followup([{ type: 'text', text: 'parent q1' }]) + parent.followup({ content: [{ type: 'text', text: 'parent q1' }], source: { kind: 'user' } }) await parent.whenIdle() const parentPrefixLen = parent.session.events.length @@ -93,7 +93,7 @@ describe('multi-subagent coexistence (spawn + fork on one context)', () => { await forkRun.dispose() // The parent is unaffected and keeps working after both delegations. - parent.followup([{ type: 'text', text: 'parent q2' }]) + parent.followup({ content: [{ type: 'text', text: 'parent q2' }], source: { kind: 'user' } }) await parent.whenIdle() const lastParentMessage = parent.session.events.findLast(e => e.type === 'assistant/message') expect(lastParentMessage?.type === 'assistant/message' && text(lastParentMessage.data.content)).toBe('parent turn two') diff --git a/packages/subagent/subagent-fork/tests/subagent-fork.spec.ts b/packages/subagent/subagent-fork/tests/subagent-fork.spec.ts index f1684ba55f..947ffeea35 100644 --- a/packages/subagent/subagent-fork/tests/subagent-fork.spec.ts +++ b/packages/subagent/subagent-fork/tests/subagent-fork.spec.ts @@ -89,9 +89,9 @@ describe('dsh-subagent-fork', () => { it('seeds every completed parent turn through the last turn/end', async () => { const { ctx, parent } = await setup([textResponse('first'), textResponse('second'), textResponse('child')]) - parent.followup([{ type: 'text', text: 'q1' }]) + parent.followup({ content: [{ type: 'text', text: 'q1' }], source: { kind: 'user' } }) await parent.whenIdle() - parent.followup([{ type: 'text', text: 'q2' }]) + parent.followup({ content: [{ type: 'text', text: 'q2' }], source: { kind: 'user' } }) await parent.whenIdle() const parentPrefixLen = parent.session.events.length @@ -108,7 +108,7 @@ describe('dsh-subagent-fork', () => { // Parent runs one turn, then we fork. The child's seeded log should contain // the parent's first turn, and the child should run its own new turn on top. const { ctx, parent } = await setup([textResponse('parent answer'), textResponse('child answer')]) - parent.followup([{ type: 'text', text: 'parent question' }]) + parent.followup({ content: [{ type: 'text', text: 'parent question' }], source: { kind: 'user' } }) await parent.whenIdle() const parentPrefixLen = parent.session.events.length @@ -137,10 +137,10 @@ describe('dsh-subagent-fork', () => { // open (a hanging model call), and fork while it's in flight. The seed must stop after the // balanced first turn; including the open turn would fail invariant replay during start. const { ctx, parent } = await setup([textResponse('done'), 'hang', textResponse('child')]) - parent.followup([{ type: 'text', text: 'q1' }]) + parent.followup({ content: [{ type: 'text', text: 'q1' }], source: { kind: 'user' } }) await parent.whenIdle() // Start a second turn that hangs (open turn/start + open step, never ends). - parent.followup([{ type: 'text', text: 'q2' }]) + parent.followup({ content: [{ type: 'text', text: 'q2' }], source: { kind: 'user' } }) await new Promise(r => setTimeout(r, 20)) // let the hanging turn open // Forking now must NOT throw (the open second turn is excluded from the seed). @@ -164,7 +164,7 @@ describe('dsh-subagent-fork', () => { textResponse('parent turn'), toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 9 }), ]) - parent.followup([{ type: 'text', text: 'warm up' }]) + parent.followup({ content: [{ type: 'text', text: 'warm up' }], source: { kind: 'user' } }) await parent.whenIdle() const run = await start(ctx, 'fork', { prompt: [{ type: 'text', text: 'report structured' }], @@ -183,7 +183,7 @@ describe('dsh-subagent-fork', () => { // `readResult` must scan only child-owned events after the seed. The child emits no assistant // message, so scanning the whole log would incorrectly return the parent's distinctive text. const { ctx, parent } = await setup([textResponse('parent stale'), emptyStop]) - parent.followup([{ type: 'text', text: 'parent question' }]) + parent.followup({ content: [{ type: 'text', text: 'parent question' }], source: { kind: 'user' } }) await parent.whenIdle() const run = await start(ctx, 'fork', { prompt: [{ type: 'text', text: 'child question' }], parent }) diff --git a/packages/subagent/subagent-inprocess/README.md b/packages/subagent/subagent-inprocess/README.md index 48c72e9cf0..4e63136eab 100644 --- a/packages/subagent/subagent-inprocess/README.md +++ b/packages/subagent/subagent-inprocess/README.md @@ -36,7 +36,7 @@ Depth enforcement is internal to `startInProcessRun`: it reads the parent depth - An order-190 system-prompt section tells the child that the tool call is the terminal answer. - Both contributions are ordinary child-scoped registrations. An expert `system-prompt/assemble` listener may replace them and therefore owns preserving the structured-output protocol for that child. - A `tools/result` observer commits a staged value only after that execution's authoritative final tool result succeeds, including the enclosing `run_code` result for Code Mode sub-dispatch. -- A monotonic tool guard blocks later calls after capture, and `agent/turn-stop` ends the turn after the structured result commits. +- A monotonic tool guard blocks later calls after capture, and the structured-output execution's `concludeTurn()` marker ends the turn after the result commits. A clean turn that never commits the required structured value reports `error`; the driver does not re-prompt. All registrations ride the child fiber and disappear with it. diff --git a/packages/subagent/subagent-inprocess/src/index.ts b/packages/subagent/subagent-inprocess/src/index.ts index 71bc7e42a6..22ea0e547d 100644 --- a/packages/subagent/subagent-inprocess/src/index.ts +++ b/packages/subagent/subagent-inprocess/src/index.ts @@ -141,7 +141,7 @@ export async function startInProcessRun( const result: Promise = (async () => { try { - child.followup(request.prompt) + child.followup({ content: request.prompt, source: { kind: 'user' } }) await child.whenIdle() return readResult( child, diff --git a/packages/subagent/subagent-inprocess/tests/structured.spec.ts b/packages/subagent/subagent-inprocess/tests/structured.spec.ts index d67d5f3537..43306c1d61 100644 --- a/packages/subagent/subagent-inprocess/tests/structured.spec.ts +++ b/packages/subagent/subagent-inprocess/tests/structured.spec.ts @@ -463,7 +463,7 @@ describe('in-process structured output', () => { textResponse('parent answer'), toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 1 }), ]) - parent.followup([{ type: 'text', text: 'hello' }]) + parent.followup({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }) await parent.whenIdle() expect(adapter.requests[0]!.system ?? '').not.toContain(STRUCTURED_OUTPUT_INSTRUCTION) const run = await ctx.subagents.start('spawn', structuredRequest(parent)) @@ -479,7 +479,7 @@ describe('in-process structured output', () => { describe('scoped registration (each child owns its capture tool)', () => { it('a plain agent never sees the tool: nothing is registered globally at all', async () => { const { ctx, parent, adapter } = await setup([textResponse('parent answer')]) - parent.followup([{ type: 'text', text: 'hello' }]) + parent.followup({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }) await parent.whenIdle() // Scoped registration: the global view has no capture tool, ever. expect(ctx.tools.get(STRUCTURED_OUTPUT_TOOL)).toBeUndefined() @@ -493,7 +493,7 @@ describe('in-process structured output', () => { // Child turn: must see it, with the run's schema. toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 42 }), ]) - parent.followup([{ type: 'text', text: 'hello' }]) + parent.followup({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } }) await parent.whenIdle() expect(toolNames(adapter.requests[0]!)).not.toContain(STRUCTURED_OUTPUT_TOOL) @@ -571,7 +571,7 @@ describe('in-process structured output', () => { it('a non-structured agent request keeps tools ABSENT when it had none (no tools: [] materialized)', async () => { const { parent, adapter } = await setup([textResponse('plain')]) - parent.followup([{ type: 'text', text: 'q' }]) + parent.followup({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }) await parent.whenIdle() const request = adapter.requests[0]! expect(request.tools).toBeUndefined() diff --git a/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts b/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts index 3186b4551a..d2ef7e1643 100644 --- a/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts +++ b/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts @@ -87,7 +87,7 @@ describe('startInProcessRun', () => { it('seeds a forked child but reads only the child-owned output', async () => { const { ctx, parent } = await setup([textResponse('parent answer'), textResponse('child answer')]) - parent.followup([{ type: 'text', text: 'parent question' }]) + parent.followup({ content: [{ type: 'text', text: 'parent question' }], source: { kind: 'user' } }) await parent.whenIdle() const seed = parent.session.events.slice() const run = await startInProcessRun(request(parent), { seed }) diff --git a/packages/subagent/subagent-spawn/tests/spawn.e2e.ts b/packages/subagent/subagent-spawn/tests/spawn.e2e.ts index 8fbecdc976..90945a1bd6 100644 --- a/packages/subagent/subagent-spawn/tests/spawn.e2e.ts +++ b/packages/subagent/subagent-spawn/tests/spawn.e2e.ts @@ -31,10 +31,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('spawn backend with-key smoke', ( ctx = await spawnHarness(workdir) const parent = ctx.agentLoop.create(SessionId('e2e-parent'), { provider: 'deepseek', model: 'deepseek-v4-flash' }) - parent.followup([{ type: 'text', text: + parent.followup({ content: [{ type: 'text', text: 'Use the subagent tool to delegate this exact task: "Use the bash tool to write the text ' + 'SUBAGENT_WAS_HERE into a file named proof.txt in the current directory." ' - + 'After the subagent finishes, tell me it is done.' }]) + + 'After the subagent finishes, tell me it is done.' }], source: { kind: 'user' } }) await waitForIdle(ctx, parent) // Verify the WORLD: the child actually wrote the file. diff --git a/packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts b/packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts index e72ed07b69..57e39f4684 100644 --- a/packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts +++ b/packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts @@ -106,7 +106,7 @@ describe('dsh-subagent-spawn', () => { it('a fresh child does NOT inherit the parent conversation (its log starts empty before the prompt)', async () => { // Drive the parent through one real turn so it has history, THEN spawn. const { ctx, parent } = await setup([textResponse('parent turn'), textResponse('child sees nothing')]) - parent.followup([{ type: 'text', text: 'parent prompt' }]) + parent.followup({ content: [{ type: 'text', text: 'parent prompt' }], source: { kind: 'user' } }) await parent.whenIdle() const parentEventCount = parent.session.events.length expect(parentEventCount).toBeGreaterThan(0) @@ -383,7 +383,7 @@ describe('dsh-subagent-spawn', () => { textResponse('parent answer'), textResponse('child answer'), ]) - parent.followup([{ type: 'text', text: 'hi' }]) + parent.followup({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }) await parent.whenIdle() const run = await start(ctx, 'spawn', { diff --git a/packages/support/acp-snapshot/README.md b/packages/support/acp-snapshot/README.md index 05a4974c19..082be0e9ef 100644 --- a/packages/support/acp-snapshot/README.md +++ b/packages/support/acp-snapshot/README.md @@ -6,7 +6,7 @@ Four layers, importable separately: - **`launchAcpTestAgent` (launcher)** — boots a source agent under tsx or a built `lib` agent under plain Node from a supplied cwd, connects the SDK client over a raw-byte stdout tee, collects session updates and stderr, surfaces asynchronous spawn failures through startup, fails closed on unhandled permission requests, and owns graceful or signalled shutdown. Shutdown waits for process exit, inherited stdio closure, and ACP parser exhaustion before resolving or propagating a child error, so captures are complete and callers can remove owned paths after either outcome. When Windows accepts forced termination but publishes its exit marker asynchronously, shutdown gives that marker a bounded grace before treating fallback refusal as a second failure. Snapshot and ordinary e2e suites share this process boundary; a test supplies only agent paths, cwd, environment overrides, and any permission policy. - **`runScenario` (harness)** — drives ACP JSON-RPC stdio from a deterministic `input.json` script through the launcher, tees raw stdout for the expected-output and purity checks, and harvests every persisted raw JSONL session log (parent and subagent children, primary-first) after graceful stdin EOF. `AgentUnderTest` supplies absolute `binScript`, optional `libBinScript`, `configPath`, and `tsconfigPath` paths because the subprocess cwd is outside the repo; `workspaceParent` may move the generated child cwd from the platform temp directory when that grant is itself under test. Startup failures preserve captured agent stderr in the rejected diagnostic. -- **Normalizers** — pure functions turning the two captured surfaces into stable text: `normalizeStdout` (JSON-RPC ids → first-seen sequence; UUIDs/cwd → tokens; cwd-rooted separators selected as canonical `/` or host-native; `session_info_update.updatedAt` → `{{updatedAt}}`; doubles as the stdout-purity check), `normalizeSessionLog` (times zeroed, `seq` kept, the same cwd-path policy), `scrubSystemPrompts` (prompt text → `{{system}}`), `scrubToolSchemas` (schema bulk → `{{tools}}`), and `scrubRequestHeaders` (all header bulk → `{{system}}`/`{{tools}}`/`{{messagePrefix}}` outside each pin, structure kept — [pinned-header Agent Note](../../../.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)). +- **Normalizers** — pure functions turning the two captured surfaces into stable text: `normalizeStdout` (JSON-RPC ids → first-seen sequence; UUIDs/cwd → tokens; cwd-rooted separators selected as canonical `/` or host-native; `session_info_update.updatedAt` → `{{updatedAt}}`; doubles as the stdout-purity check), `normalizeSessionLog` (times zeroed, `seq` kept, the same cwd-path policy), `scrubSystemPrompts` (prompt text → `{{system}}`), `scrubToolSchemas` (schema bulk → `{{tools}}`), and `scrubRequestHeaders` (all header bulk → `{{system}}`/`{{tools}}` outside each pin, structure kept — [pinned-header Agent Note](../../../.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)). - **`defineAcpSnapshotSuite` (factory)** — registers the whole describe/it tree for a scenario table: per-scenario expected-output and re-persisted-log comparisons, record/refresh fixture write-back, rejection of structured `UNKNOWN_TOOL` results, the per-header-class pin (`system-prompt.expected.md` plus `tool-schemas.expected.json`) with its live uniformity guard, and the fixture guard block (no orphan scenario dirs, required files present, exactly one pin per class, every JSONL prompt/schema-scrubbed, non-pinning fixtures fully header-scrubbed). Refresh expands packed timing envelopes before aligning existing volatile event times, so switching between packed and unpacked layouts cannot shift later records; fresh chunk-fragment arrays remain authoritative. A newly inserted `session/title` receives its preceding event's time so feature-driven insertions do not churn the remainder of a fixture. Each scenario directory's `session.jsonl` plus contiguous `session..jsonl` siblings are the ordered primary/child inventory; the scenario table does not duplicate their count. Must be called at vitest collection time. A consuming `*.snapshot.ts` is the scenario table plus one factory call: diff --git a/packages/support/acp-snapshot/src/normalize.ts b/packages/support/acp-snapshot/src/normalize.ts index b32c5c0574..9b3611a503 100644 --- a/packages/support/acp-snapshot/src/normalize.ts +++ b/packages/support/acp-snapshot/src/normalize.ts @@ -2,7 +2,7 @@ * Pure ACP transcript and session-log normalizers. They scrub session ids, run cwd, RPC ids, * timestamps, and hook duration while preserving deterministic event sequence numbers. * Request-header scrubbers stay composable so one scenario per header class can pin prompt and - * tool-schema sidecars while retaining any model-visible prefix in the session log. + * tool-schema sidecars. * @module @deepseek-ai/dsh-acp-snapshot/normalize */ @@ -10,7 +10,6 @@ const SESSION_ID = '{{sessionId}}' const CWD = '{{cwd}}' const SYSTEM = '{{system}}' const TOOLS = '{{tools}}' -const MESSAGE_PREFIX = '{{messagePrefix}}' const UPDATED_AT = '{{updatedAt}}' /** A cwd-rooted path after volatile cwd replacement, through its last separator-delimited segment. */ @@ -221,14 +220,13 @@ export function scrubToolSchemas(rawLog: string): string { * @returns The JSONL with all header bulk tokenized, other lines byte-identical. */ export function scrubRequestHeaders(rawLog: string): string { - return scrubHeaderContent(rawLog, { system: true, tools: true, prefix: true }) + return scrubHeaderContent(rawLog, { system: true, tools: true }) } /** Which independent request-header payloads a scrubber replaces. */ interface HeaderScrubOptions { system?: boolean tools?: boolean - prefix?: boolean } /** Transform the selected request-header payloads. */ @@ -245,10 +243,6 @@ function scrubHeaderContent(rawLog: string, options: HeaderScrubOptions): string let touched = false if (options.system === true && 'system' in header) { header.system = SYSTEM; touched = true } if (options.tools === true && 'tools' in header) { header.tools = TOOLS; touched = true } - if (options.prefix === true && Array.isArray(header.messagePrefix)) { - header.messagePrefix = header.messagePrefix.map(() => MESSAGE_PREFIX) - touched = true - } return touched ? JSON.stringify(record) : line } return line diff --git a/packages/support/acp-snapshot/src/suite.ts b/packages/support/acp-snapshot/src/suite.ts index 51e958a106..3445293799 100644 --- a/packages/support/acp-snapshot/src/suite.ts +++ b/packages/support/acp-snapshot/src/suite.ts @@ -53,9 +53,7 @@ export interface Scenario { * Whether the run persists a comparable session log to diff against the * `session.jsonl` fixture. Defaults to {@link hasModelTurn} (a model turn * always produces a log worth comparing). Set it independently for a scenario - * that produces a non-trivial log WITHOUT a model turn — e.g. a prompt blocked - * by a `UserPromptSubmit` hook, which opens a `rejected` turn carrying `hook/*` - * events but never calls the model. + * that produces a non-trivial durable log without calling the model. */ comparesLog?: boolean /** @@ -701,8 +699,8 @@ export function defineAcpSnapshotSuite(options: SnapshotSuiteOptions): void { await expect(stdout, `${expected.file} mismatch`).toMatchFileSnapshot(join(dir, expected.file)) } - // A model turn always produces a log worth comparing; a hook scenario can - // produce one without a model turn (a `rejected` turn carrying `hook/*`). + // A model turn always produces a log worth comparing; an explicitly + // authored non-model scenario may opt in independently. if (comparesLog) { // The harvested logs (primary-first) must match their committed fixtures 1:1. expect(result.sessionLogs.length, 'this scenario must persist one log per session fixture').toBe(fixtureFiles.length) diff --git a/packages/support/acp-snapshot/tests/normalize.spec.ts b/packages/support/acp-snapshot/tests/normalize.spec.ts index bdd85491d2..8ae3db48a4 100644 --- a/packages/support/acp-snapshot/tests/normalize.spec.ts +++ b/packages/support/acp-snapshot/tests/normalize.spec.ts @@ -339,25 +339,6 @@ describe('scrubRequestHeaders', () => { expect(toolsOnly).not.toContain('{{system}}') }) - it('scrubs the header session prefix to one token per message, keeping the count', () => { - const ev = headerEvent({ - config: { model: 'm' }, - messagePrefix: [ - { role: 'user', content: [{ type: 'text', text: 'workspace AGENTS digest' }] }, - { role: 'user', content: [{ type: 'text', text: 'skills catalog' }] }, - ], - }) - const out = scrubRequestHeaders(`${headerLine}\n${ev}\n`) - expect(out).toContain('"messagePrefix":["{{messagePrefix}}","{{messagePrefix}}"]') - expect(out).not.toContain('AGENTS digest') - expect(out).not.toContain('skills catalog') - // Absence stays absent — a prefix-less header gains no token… - expect(scrubRequestHeaders(`${headerLine}\n${headerEvent({ system: 's' })}\n`)).not.toContain('{{messagePrefix}}') - // …and a non-array shape passes through untouched. - const odd = JSON.stringify({ type: 'request/header', seq: 4, time: 9, data: { header: { config: { model: 'm' }, messagePrefix: 'weird' }, reason: 'initial' } }) - expect(scrubRequestHeaders(`${headerLine}\n${odd}\n`)).toContain('"messagePrefix":"weird"') - }) - it('leaves malformed headers with no scrubbable payload byte-identical', () => { const headerless = JSON.stringify({ type: 'request/header', seq: 10, time: 9, data: { reason: 'initial' } }) const nullData = JSON.stringify({ type: 'request/header', seq: 11, time: 9, data: null }) @@ -376,14 +357,13 @@ describe('scrubRequestHeaders', () => { }) describe('scrubSystemPrompts', () => { - it('scrubs only system prompt payloads while keeping tools and prefixes verbatim', () => { + it('scrubs only system prompt payloads while keeping tools verbatim', () => { const header = JSON.stringify({ type: 'request/header', seq: 1, time: 2, data: { header: { system: 'full prompt', tools: [{ name: 'read', description: 'full schema' }], - messagePrefix: [{ role: 'user', content: [{ type: 'text', text: 'full prefix' }] }], }, reason: 'initial', }, @@ -394,7 +374,6 @@ describe('scrubSystemPrompts', () => { header: { system: 'new prompt', tools: [{ name: 'read', description: 'changed schema' }], - messagePrefix: [{ role: 'user', content: [{ type: 'text', text: 'changed prefix' }] }], }, reason: 'change', }, @@ -409,23 +388,20 @@ describe('scrubSystemPrompts', () => { expect(out).not.toContain('full prompt') expect(out).not.toContain('new prompt') expect(out).toContain('full schema') - expect(out).toContain('full prefix') expect(out).toContain('changed schema') - expect(out).toContain('changed prefix') expect(out.split('\n')[2]).toBe(toolsOnly) expect(scrubSystemPrompts(out)).toBe(out) }) }) describe('scrubToolSchemas', () => { - it('scrubs only tool-schema payloads while keeping prompts and prefixes verbatim', () => { + it('scrubs only tool-schema payloads while keeping prompts verbatim', () => { const header = JSON.stringify({ type: 'request/header', seq: 1, time: 2, data: { header: { system: 'full prompt', tools: [{ name: 'read', description: 'full schema', parameters: { type: 'object' } }], - messagePrefix: [{ role: 'user', content: [{ type: 'text', text: 'full prefix' }] }], }, reason: 'initial', }, @@ -436,7 +412,6 @@ describe('scrubToolSchemas', () => { header: { system: 'new prompt', tools: [{ name: 'grep', description: 'new schema' }], - messagePrefix: [{ role: 'user', content: [{ type: 'text', text: 'changed prefix' }] }], }, reason: 'change', }, @@ -452,8 +427,6 @@ describe('scrubToolSchemas', () => { expect(out).not.toContain('new schema') expect(out).toContain('full prompt') expect(out).toContain('new prompt') - expect(out).toContain('full prefix') - expect(out).toContain('changed prefix') expect(out.split('\n')[2]).toBe(systemOnly) expect(scrubToolSchemas(out)).toBe(out) }) diff --git a/packages/tasks/tool-tasks/src/index.ts b/packages/tasks/tool-tasks/src/index.ts index ad8e9af674..e8dba6d539 100644 --- a/packages/tasks/tool-tasks/src/index.ts +++ b/packages/tasks/tool-tasks/src/index.ts @@ -221,13 +221,13 @@ export function apply(ctx: Context, config: Config): void { ctx.tasks.onTaskDone((snapshot, owner) => { if (snapshot.reported || owner === undefined) return try { - owner.inject( - [{ + owner.inject({ + content: [{ type: 'text', text: fitCompletionNotice(snapshot), }], - { source: { kind: 'plugin', plugin: 'tool-tasks' } }, - ) + source: { kind: 'plugin', plugin: 'tool-tasks' }, + }) } catch (error: unknown) { // Disposal may win the race after settlement; other injection failures surface. if (error instanceof Error && error.message.includes('is disposed')) return diff --git a/packages/tasks/tool-tasks/tests/tool-tasks.spec.ts b/packages/tasks/tool-tasks/tests/tool-tasks.spec.ts index c41f498472..7f2ddd718e 100644 --- a/packages/tasks/tool-tasks/tests/tool-tasks.spec.ts +++ b/packages/tasks/tool-tasks/tests/tool-tasks.spec.ts @@ -455,10 +455,10 @@ describe('completion notices', () => { p.settle({ status: 'completed', detail: 'exit code: 0' }) await tick() expect(inject).toHaveBeenCalledTimes(1) - expect(inject).toHaveBeenCalledWith( - [{ type: 'text', text: 'background task bash-1 (bash: pnpm test) finished [status: completed, exit code: 0]. Read its output with task_output.' }], - { source: { kind: 'plugin', plugin: 'tool-tasks' } }, - ) + expect(inject).toHaveBeenCalledWith({ + content: [{ type: 'text', text: 'background task bash-1 (bash: pnpm test) finished [status: completed, exit code: 0]. Read its output with task_output.' }], + source: { kind: 'plugin', plugin: 'tool-tasks' }, + }) }) it('preserves task ids and collection guidance in bounded completion notices', async () => { @@ -477,8 +477,10 @@ describe('completion notices', () => { expect(inject).toHaveBeenNthCalledWith( 1, - [{ type: 'text', text: 'background task subagent-1\n[notice truncated]\nDone; task_output.' }], - { source: { kind: 'plugin', plugin: 'tool-tasks' } }, + { + content: [{ type: 'text', text: 'background task subagent-1\n[notice truncated]\nDone; task_output.' }], + source: { kind: 'plugin', plugin: 'tool-tasks' }, + }, ) const second = producer({ @@ -491,7 +493,7 @@ describe('completion notices', () => { second.settle({ status: 'completed', detail: 'd'.repeat(1_000) }) await tick() - const content = inject.mock.calls[1]?.[0] as Array<{ type: string; text?: string }> | undefined + const content = (inject.mock.calls[1]?.[0] as { content?: Array<{ type: string; text?: string }> } | undefined)?.content const notice = content?.[0]?.text ?? '' expect(Buffer.byteLength(notice)).toBeLessThanOrEqual(80) expect(notice).toContain('background task subagent-2 (subagent: xxxx') @@ -518,7 +520,7 @@ describe('completion notices', () => { target.settle({ status: 'completed', detail: 'd'.repeat(1_000) }) await tick() - const content = inject.mock.calls[0]?.[0] as Array<{ type: string; text?: string }> | undefined + const content = (inject.mock.calls[0]?.[0] as { content?: Array<{ type: string; text?: string }> } | undefined)?.content const notice = content?.[0]?.text ?? '' expect(Buffer.byteLength(notice)).toBeLessThanOrEqual(64) expect(notice).toBe('background task pty-send-100\nDone; task_output.') @@ -537,8 +539,8 @@ describe('completion notices', () => { short.settle({ status: 'completed' }) await tick() - const tinyNotice = (inject.mock.calls[0]?.[0] as Array<{ text?: string }> | undefined)?.[0]?.text ?? '' - const shortNotice = (inject.mock.calls[1]?.[0] as Array<{ text?: string }> | undefined)?.[0]?.text ?? '' + const tinyNotice = (inject.mock.calls[0]?.[0] as { content?: Array<{ text?: string }> } | undefined)?.content?.[0]?.text ?? '' + const shortNotice = (inject.mock.calls[1]?.[0] as { content?: Array<{ text?: string }> } | undefined)?.content?.[0]?.text ?? '' expect(Buffer.byteLength(tinyNotice)).toBeLessThanOrEqual(8) expect(tinyNotice).toBe('_output.') expect(Buffer.byteLength(shortNotice)).toBeLessThanOrEqual(32) diff --git a/packages/todo/tool-todo/tests/integration.spec.ts b/packages/todo/tool-todo/tests/integration.spec.ts index fe09af9730..4a8d1a2aa7 100644 --- a/packages/todo/tool-todo/tests/integration.spec.ts +++ b/packages/todo/tool-todo/tests/integration.spec.ts @@ -59,7 +59,7 @@ describe('todo_write tool through the agent loop', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('it-todo'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'plan a two-step task' }]) + agent.followup({ content: [{ type: 'text', text: 'plan a two-step task' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const log = agent.session.events @@ -87,7 +87,7 @@ describe('todo_write tool through the agent loop', () => { const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('it-todo-2'), { provider: 'mock', model: 'mock' }) - agent.followup([{ type: 'text', text: 'plan then update' }]) + agent.followup({ content: [{ type: 'text', text: 'plan then update' }], source: { kind: 'user' } }) await waitForIdle(ctx, agent) const todoEvents = agent.session.events.filter(e => e.type === 'todo/write') diff --git a/packages/ui/acp/acp-feature-support.md b/packages/ui/acp/acp-feature-support.md index 3292ee17c4..79d5774108 100644 --- a/packages/ui/acp/acp-feature-support.md +++ b/packages/ui/acp/acp-feature-support.md @@ -111,7 +111,7 @@ Tool-call presentation is **owned by each tool** (`presentCall` / `presentResult ## 6. Session modes / config options / models -Session modes ✅ (the [plan-mode Agent Note](../../../.agents/notes/implemented/feature/2026-07-07-plan-mode.md)): ACP owns the fixed `default` / `plan` wire vocabulary and projects it onto `ctx.planMode`'s boolean `{ active, pending? }` state; `session/set_mode` calls `set()` and `current_mode_update` tracks the optimistic selection plus each distinct committed `plan/mode` flip. Config options ✅: the bridge advertises a `model` select from the advisory LLM provider/model catalog, preserving each provider/model pair in an opaque value and grouping multiple providers. A selected pair is isolated to one session, snapshotted with the prompt for each step, applied through `agent/request`, and restored from the logged request header on load. When `ctx.permission` is composed, the bridge also advertises one `permission` select whose values come from the deployment preset table and whose current value derives from the session log; idle permission switches anchor at the next `agent/prompt-submit` inside its open turn. The division is picker-to-collaboration-state / knobs-to-config-options: individual environment knobs and the provider/model selector are not modes. See the [model-catalog Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md) and [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). +Session modes ✅ (the [plan-mode Agent Note](../../../.agents/notes/implemented/feature/2026-07-07-plan-mode.md)): ACP owns the fixed `default` / `plan` wire vocabulary and projects it onto `ctx.planMode`'s boolean `{ active, pending? }` state; `session/set_mode` calls `set()` and `current_mode_update` tracks the optimistic selection plus each distinct committed `plan/mode` flip. Config options ✅: the bridge advertises a `model` select from the advisory LLM provider/model catalog, preserving each provider/model pair in an opaque value and grouping multiple providers. A selected pair is isolated to one session, snapshotted with the prompt for each step, applied through `agent/request`, and restored from the logged request header on load. When `ctx.permission` is composed, the bridge also advertises one `permission` select whose values come from the deployment preset table and whose current value derives from the session log; idle permission switches anchor at the next `agent/step` inside its open turn. The division is picker-to-collaboration-state / knobs-to-config-options: individual environment knobs and the provider/model selector are not modes. See the [model-catalog Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md) and [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). ## 7. Content blocks diff --git a/packages/ui/acp/src/index.ts b/packages/ui/acp/src/index.ts index a62def17b9..b14049fc04 100644 --- a/packages/ui/acp/src/index.ts +++ b/packages/ui/acp/src/index.ts @@ -54,14 +54,13 @@ import { assertNever, CallId } from '@deepseek-ai/dsh-llm' import type {} from '@deepseek-ai/dsh-llm-retry' import { installAgentLlmTarget, - type AdditionalContext, type Agent, type AgentLlmTarget as LlmTarget, type AgentLlmTargetRef as LlmTargetRef, } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-commands' import { encodeSessionReferenceUri } from '@deepseek-ai/dsh-session-reference' -import { SessionId, type JsonValue } from '@deepseek-ai/dsh-session' +import { SessionId, type JsonValue, type UserMessageData } from '@deepseek-ai/dsh-session' // Side-effect type import: resolves `ctx.get('permission')` to the service. import type {} from '@deepseek-ai/dsh-permission' import type { SessionEvent, TodoItem, TurnEndReason } from '@deepseek-ai/dsh-session' @@ -1056,7 +1055,7 @@ export function apply(ctx: Context, config: AcpConfig): void { } const { text } = referencedPrompt let preparedContent: ContentBlock[] = [{ type: 'text', text }] - let additionalContext: AdditionalContext | undefined + let additionalContext: UserMessageData | undefined if (referencedPrompt.references.length > 0) { const sessionReferences = ctx.get('sessionReferences') if (sessionReferences === undefined) { @@ -1083,15 +1082,22 @@ export function apply(ctx: Context, config: AcpConfig): void { } // Install the in-flight slot BEFORE send() (send does not synchronously // flip status to running; the session/event listener records the turn - // number and settle/rejects it). Capture the log length now as the - // A turn that ends in error rejects this promise (the codec never - // produces an error stop reason). + // number and settles or rejects it). Admission may also finish without + // opening a turn; the idle waiter closes that RPC without inventing a + // durable turn boundary. A turn that ends in error rejects this promise + // because the codec has no error stop reason. const stopReason = await new Promise((resolve, reject) => { - rec.inflight = { resolve, reject, turn: undefined } + const inflight: NonNullable = { resolve, reject, turn: undefined } + rec.inflight = inflight if (additionalContext !== undefined) { - rec.agent.inject(additionalContext.content, { source: additionalContext.source }) + rec.agent.inject({ content: additionalContext.content, source: additionalContext.source }) } - rec.agent.followup(preparedContent, { source: { kind: 'user' } }) + rec.agent.followup({ content: preparedContent, source: { kind: 'user' } }) + void rec.agent.whenIdle().then(() => { + if (rec.inflight !== inflight || inflight.turn !== undefined) return + rec.inflight = undefined + inflight.resolve('cancelled') + }) }) return { stopReason } }, diff --git a/packages/ui/acp/tests/config-options.spec.ts b/packages/ui/acp/tests/config-options.spec.ts index 6108836cda..4bf6013649 100644 --- a/packages/ui/acp/tests/config-options.spec.ts +++ b/packages/ui/acp/tests/config-options.spec.ts @@ -406,7 +406,7 @@ describe('acp bridge — session config options', () => { const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] }) const agent = h.ctx.agents.list()[0] if (agent === undefined) throw new Error('expected an agent') - agent.inject([{ type: 'text', text: 'checkpoint' }], { source: { kind: 'plugin', plugin: 'test' } }) + agent.inject({ content: [{ type: 'text', text: 'checkpoint' }], source: { kind: 'plugin', plugin: 'test' } }) await agent.whenIdle() await h.dispose() h = undefined diff --git a/packages/ui/acp/tests/dispose.spec.ts b/packages/ui/acp/tests/dispose.spec.ts index c95e4970ca..e1f6f3edb5 100644 --- a/packages/ui/acp/tests/dispose.spec.ts +++ b/packages/ui/acp/tests/dispose.spec.ts @@ -264,7 +264,7 @@ describe('acp bridge — disposal & HMR safety', () => { const handle = await harness.ctx.agents.create({ sessionId: SessionId('guard-a'), agentOptions: { provider: 'mock', model: 'mock' }, }) - handle.agent.followup([{ type: 'text', text: 'go' }]) + handle.agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await handle.agent.whenIdle() expect(harness.ctx.sessions.get(SessionId('guard-a'))).toBeDefined() @@ -288,7 +288,7 @@ describe('acp bridge — disposal & HMR safety', () => { // Drive a turn that hangs in the model stream, so the loop is mid-turn when // disposed — its exit runs a final session/flush we can gate to hold the // teardown observably in-flight. - handle.agent.followup([{ type: 'text', text: 'go' }]) + handle.agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }) await new Promise(r => setTimeout(r, 30)) expect(handle.agent.status).toBe('running') // Both callers join the same teardown and observe registry removal. diff --git a/packages/ui/acp/tests/edges.spec.ts b/packages/ui/acp/tests/edges.spec.ts index ac85fbd7e0..6db5a10a6a 100644 --- a/packages/ui/acp/tests/edges.spec.ts +++ b/packages/ui/acp/tests/edges.spec.ts @@ -30,7 +30,7 @@ describe('acp bridge — demux & config edges', () => { const before = harness.updates.length const { agent: foreign } = await harness.ctx.agents.create({ sessionId: SessionId('foreign-session'), agentOptions: { provider: 'mock', model: 'mock' } }) - foreign.followup([{ type: 'text', text: 'hi' }]) + foreign.followup({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }) await foreign.whenIdle() await new Promise(r => setTimeout(r, 10)) diff --git a/packages/ui/acp/tests/turns.spec.ts b/packages/ui/acp/tests/turns.spec.ts index c8823413e7..451e50aa90 100644 --- a/packages/ui/acp/tests/turns.spec.ts +++ b/packages/ui/acp/tests/turns.spec.ts @@ -51,11 +51,50 @@ describe('acp bridge — turn outcomes', () => { it('rejects an ordinary plugin turn failure through the same ACP boundary', async () => { harness = await makeBridgeHarness({ storageDir, script: [textResponse('must not run')] }) - harness.ctx.on('agent/step', () => { throw new Error('plugin pre-step failed') }) + harness.ctx.on('agent/step', () => { throw new Error('plugin step failed') }) const sessionId = await newSession(harness) await expect(harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })) - .rejects.toThrow(/turn failed: plugin pre-step failed/) + .rejects.toThrow(/turn failed: plugin step failed/) + }) + + it('settles a prompt rejected during admission without opening a turn', async () => { + harness = await makeBridgeHarness({ storageDir, script: [textResponse('must not run')] }) + harness.ctx.on('agent/prompt-submit', async () => ({ kind: 'block', reason: 'policy veto' })) + const sessionId = await newSession(harness) + + await expect(harness.client.prompt({ + sessionId, + prompt: [{ type: 'text', text: 'blocked' }], + })).resolves.toEqual({ stopReason: 'cancelled' }) + const agent = harness.ctx.agents.get(SessionId(sessionId)) + expect(agent?.session.events.some(event => event.type === 'turn/start')).toBe(false) + }) + + it('does not classify an asynchronous allowed admission as a no-turn rejection', async () => { + harness = await makeBridgeHarness({ storageDir, script: [textResponse('ok')] }) + const entered = Promise.withResolvers() + const release = Promise.withResolvers() + harness.ctx.on('agent/prompt-submit', async () => { + entered.resolve(true) + await release.promise + return { kind: 'allow' } + }) + const sessionId = await newSession(harness) + let settled = false + const prompt = harness.client.prompt({ + sessionId, + prompt: [{ type: 'text', text: 'allowed' }], + }).then((result) => { + settled = true + return result + }) + + await entered.promise + await Promise.resolve() + expect(settled).toBe(false) + release.resolve(true) + await expect(prompt).resolves.toEqual({ stopReason: 'end_turn' }) }) it('streams a tool call as tool_call then tool_call_update', async () => { @@ -291,7 +330,7 @@ describe('acp bridge — turn outcomes', () => { harness.ctx.on('agent/inbox/enqueue', (subject) => { if (subject === agent && !injected) { injected = true - agent.inject([{ type: 'text', text: 'ctx note' }], { source: { kind: 'plugin', plugin: 'test' } }) + agent.inject({ content: [{ type: 'text', text: 'ctx note' }], source: { kind: 'plugin', plugin: 'test' } }) } }) const res = await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] }) diff --git a/packages/ui/jsonrpc/src/server.ts b/packages/ui/jsonrpc/src/server.ts index 5200a2de13..46168551b7 100644 --- a/packages/ui/jsonrpc/src/server.ts +++ b/packages/ui/jsonrpc/src/server.ts @@ -152,7 +152,7 @@ export class HarnessSdkServer { rec.activePrompt = true try { rec.lastTurnEnd = undefined - rec.handle.agent.followup(params.contentBlocks) + rec.handle.agent.followup({ content: params.contentBlocks, source: { kind: 'user' } }) await rec.handle.agent.whenIdle() const status = this.finishedStatus(rec.lastTurnEnd) this.transport.notify('session.finished', { diff --git a/packages/ui/jsonrpc/tests/server.spec.ts b/packages/ui/jsonrpc/tests/server.spec.ts index 91d3503263..9c11816ade 100644 --- a/packages/ui/jsonrpc/tests/server.spec.ts +++ b/packages/ui/jsonrpc/tests/server.spec.ts @@ -152,7 +152,7 @@ describe('HarnessSdkServer', () => { meta: { cwd: storageDir }, agentOptions: { provider: 'deepseek', model: 'dsagent-model' }, }) - orphanHandle.agent.followup([{ type: 'text', text: 'outside the sdk session map' }]) + orphanHandle.agent.followup({ content: [{ type: 'text', text: 'outside the sdk session map' }], source: { kind: 'user' } }) await orphanHandle.agent.whenIdle() await orphanHandle.dispose() expect(llmServer.requests).toHaveLength(3) @@ -227,15 +227,12 @@ describe('HarnessSdkServer', () => { const session = ctx.sessions.create(SessionId('message-outcome')) const agent = ({ session, - followup(content: { type: 'text'; text: string }[]) { + followup(input: { content: { type: 'text'; text: string }[]; source: { kind: 'user' } }) { session.append('turn/start', { turn: 1, - trigger: { kind: 'message', source: { kind: 'user' } }, + trigger: { kind: 'message', source: input.source }, }) - session.append('user/message', { - content, - source: { kind: 'user' }, - }, { surfaceOp: 'append' }) + session.append('user/message', input, { surfaceOp: 'append' }) session.append('turn/end', { turn: 1, reason: { kind: 'max-tokens' } }) session.append('turn/start', { turn: 2, diff --git a/packages/ui/tui/src/index.ts b/packages/ui/tui/src/index.ts index 8597150846..95fe78ef4a 100644 --- a/packages/ui/tui/src/index.ts +++ b/packages/ui/tui/src/index.ts @@ -2781,11 +2781,10 @@ export function createTuiChat( if (disposed) { appendNotice(`Agent "${agent.id}" is disposed.`, 'error') } else if (agent.status === 'running') { - const source = { kind: 'user' } as const - pendingSteering.add(agent.steer(content, { source })) + pendingSteering.add(agent.steer({ content, source: { kind: 'user' } })) refreshStatus() } else { - agent.followup(content, { source: { kind: 'user' } }) + agent.followup({ content, source: { kind: 'user' } }) } } @@ -3073,7 +3072,7 @@ export function createTuiChat( editor.addToHistory(text) if (editor.getText() === value) editor.setText('') if (prepared.additionalContext !== undefined) { - agent.inject(prepared.additionalContext.content, { source: prepared.additionalContext.source }) + agent.inject({ content: prepared.additionalContext.content, source: prepared.additionalContext.source }) } dispatchMessage(prepared.content) }, (error: unknown) => { diff --git a/packages/ui/tui/tests/harness.ts b/packages/ui/tui/tests/harness.ts index 5517ba0ecb..03c373b0a6 100644 --- a/packages/ui/tui/tests/harness.ts +++ b/packages/ui/tui/tests/harness.ts @@ -4,14 +4,13 @@ import AgentRegistry, { AgentMessageId, type Agent, type AgentCancelCause, - type AliasSendOptions, type AgentOptions, type AgentStatus, type SendOptions, } from '@deepseek-ai/dsh-agent' import type { ContentBlock, LlmModelContext, LlmModelInfo, LlmProviderInfo } from '@deepseek-ai/dsh-llm' import CommandService from '@deepseek-ai/dsh-commands' -import SessionStore, { SessionId, type Session, type SessionHeader } from '@deepseek-ai/dsh-session' +import SessionStore, { SessionId, type Session, type SessionHeader, type UserMessageData } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import type { ToolDefinition } from '@deepseek-ai/dsh-tools' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' @@ -21,12 +20,12 @@ import { TestSessionQueryService } from './session-query.ts' interface FakeAgent extends Agent { status: AgentStatus sent: ContentBlock[][] - sentOptions: (SendOptions | AliasSendOptions | undefined)[] + sentOptions: (SendOptions | undefined)[] steered: ContentBlock[][] steeredIds: AgentMessageId[] - steeredOptions: (AliasSendOptions | undefined)[] + steeredOptions: UserMessageData[] injected: ContentBlock[][] - injectedOptions: (AliasSendOptions | undefined)[] + injectedOptions: UserMessageData[] cancelled: AgentCancelCause[] } @@ -163,10 +162,10 @@ export async function createTuiTestHarness -4| "▌ " +4| " Referenced sessions · Source session (source-session) " + style 1-53 dim +5| +6| "▌ " style 0-0 fg=bright-blue -5| "▌ You " +7| "▌ You " style 0-0 fg=bright-blue style 2-4 fg=bright-blue bold -6| "▌ Use @Source session " +8| "▌ Use @Source session " style 0-0 fg=bright-blue -7| "▌ " +9| "▌ " style 0-0 fg=bright-blue -8| -9| " Referenced sessions · Source session (source-session) " - style 1-53 dim 10| 11| " Assistant " style 1-9 fg=bright-magenta bold diff --git a/packages/ui/tui/tests/snapshots/untrusted-controls.expected.txt b/packages/ui/tui/tests/snapshots/untrusted-controls.expected.txt index fd2437a492..d476aa5c1d 100644 --- a/packages/ui/tui/tests/snapshots/untrusted-controls.expected.txt +++ b/packages/ui/tui/tests/snapshots/untrusted-controls.expected.txt @@ -1,7 +1,7 @@ -terminal 100x34 buffer=normal length=38 base=4 viewport=4 +terminal 100x34 buffer=normal length=36 base=2 viewport=2 lifecycle started=1 stopped=0 progress=inactive title "Unsafe terminal title \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m" -cursor hidden column=100 viewportRow=33 bufferRow=37 +cursor hidden column=100 viewportRow=33 bufferRow=35 buffer 0| " DEEPSEEK HARNESS" style 1-8 fg=bright-blue bold @@ -55,23 +55,20 @@ buffer 24| " Unsafe context \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " style 1-60 fg=bright-black 25| -26| " Prompt blocked: Unsafe policy \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " - style 1-75 fg=yellow -27| -28| " Unsafe turn error \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " +26| " Unsafe turn error \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " style 1-63 fg=red -29| -30| " " -31| " Question 1/1 (1 unanswered) · Unsafe header \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " +27| +28| " " +29| " Question 1/1 (1 unanswered) · Unsafe header \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " style 2-90 fg=bright-black -32| " Unsafe question \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " -33| " " -34| " › 1. Unsafe option \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m Unsafe detail \\x1b]2;snapshot-c " +30| " Unsafe question \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m " +31| " " +32| " › 1. Unsafe option \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m Unsafe detail \\x1b]2;snapshot-c " style 2-65 fg=bright-blue bold style 67-97 fg=bright-black -35| " Tab custom answer • ↑/↓ navigate • Enter submit • Esc interrupt " +33| " Tab custom answer • ↑/↓ navigate • Enter submit • Esc interrupt " style 2-64 dim -36| " " -37| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed" +34| " " +35| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed" style 0-43 dim style 73-99 dim diff --git a/packages/ui/tui/tests/tui.snapshot.ts b/packages/ui/tui/tests/tui.snapshot.ts index d837841286..4eee75d664 100644 --- a/packages/ui/tui/tests/tui.snapshot.ts +++ b/packages/ui/tui/tests/tui.snapshot.ts @@ -475,11 +475,6 @@ describe('TUI terminal-state snapshots', () => { content: [{ type: 'text', text: `Unsafe context ${CONTROL_PROBE}` }], source: { kind: 'plugin', plugin: `unsafe-${CONTROL_PROBE}` }, }, { surfaceOp: 'append' }) - session.append('prompt/blocked', { - content: [{ type: 'text', text: 'blocked' }], - source: { kind: 'user' }, - reason: `Unsafe policy ${CONTROL_PROBE}`, - }) session.append('step/end', { turn: 1, step: 1 }) session.append('turn/end', { turn: 1, diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts index bd66cece8b..2dc8955e73 100644 --- a/packages/ui/tui/tests/tui.spec.ts +++ b/packages/ui/tui/tests/tui.spec.ts @@ -417,7 +417,6 @@ describe('resume command and /resume', () => { [{ kind: 'error', step: 1, message: 'failed' }, 'error'], [{ kind: 'disposed' }, 'disposed'], [{ kind: 'max-tokens' }, 'max tokens'], - [{ kind: 'rejected', reason: 'policy' }, 'rejected'], [{ kind: 'interrupted' }, 'interrupted'], [{ kind: 'future-result' } as unknown as TurnEndReason, 'unknown result'], ] as const)('renders the last turn result %s', async (reason, label) => { @@ -1182,7 +1181,6 @@ describe('pi-tui chat lifecycle and transcript', () => { // A non-plugin injected source (goal) has no `plugin` field, so its context // card label falls back to the source kind. result.session.append('user/message', { content: [{ type: 'text', text: 'goal context' }], source: { kind: 'goal', goalId: 'g1', revision: 1, round: 0 } as never }, { surfaceOp: 'append' }) - result.session.append('prompt/blocked', { content: [{ type: 'text', text: 'blocked' }], source: { kind: 'user' }, reason: 'test policy' }) appendAssistant(result.session, []) result.session.append('step/end', { turn: 1, step: 1 }) result.session.append('turn/end', { turn: 1, reason: { kind: 'aborted' } }) @@ -1263,7 +1261,6 @@ describe('pi-tui chat lifecycle and transcript', () => { expect(result.terminal.output).toContain('Steering') expect(result.terminal.output).toContain('user context') expect(result.terminal.output).toContain('Context · goal') // goal-sourced injected context labels by kind - expect(result.terminal.output).toContain('Prompt blocked') expect(result.terminal.output).toContain('Turn cancelled') expect(result.terminal.progress).toContain(true) @@ -2610,12 +2607,10 @@ describe('pi-tui chat lifecycle and transcript', () => { events.session.append('turn/start', { turn: 4, trigger: { kind: 'message', source: { kind: 'user' } } }) events.session.append('turn/end', { turn: 4, reason: { kind: 'max-tokens' } }) events.session.append('turn/start', { turn: 5, trigger: { kind: 'message', source: { kind: 'user' } } }) - events.session.append('turn/end', { turn: 5, reason: { kind: 'rejected', reason: 'policy' } }) + events.session.append('turn/end', { turn: 5, reason: { kind: 'interrupted' } }) events.session.append('turn/start', { turn: 6, trigger: { kind: 'message', source: { kind: 'user' } } }) - events.session.append('turn/end', { turn: 6, reason: { kind: 'interrupted' } }) - events.session.append('turn/start', { turn: 7, trigger: { kind: 'message', source: { kind: 'user' } } }) events.session.append('turn/end', { - turn: 7, + turn: 6, reason: { kind: 'error', step: 1, failure: { message: 'structured provider failure', code: 'SERVER' } }, }) agentEvents(events.ctx, events.agent).emit('agent/disposed') @@ -2625,7 +2620,6 @@ describe('pi-tui chat lifecycle and transcript', () => { expect(events.terminal.output).toContain('Turn cancelled') expect(events.terminal.output).toContain('structured provider failure') expect(events.terminal.output).toContain('output-token limit') - expect(events.terminal.output).toContain('Turn rejected') expect(events.terminal.output).toContain('previous process ended') expect(events.terminal.output).toContain('was disposed') await dispose(events) diff --git a/packages/ui/user-approval/src/index.ts b/packages/ui/user-approval/src/index.ts index a71638bb1a..5bdf470169 100644 --- a/packages/ui/user-approval/src/index.ts +++ b/packages/ui/user-approval/src/index.ts @@ -269,10 +269,10 @@ export class ApprovalService extends Service { // to go out states the truth, and there is no delta to explain. if (told === undefined || told === current) return const cause = overrideIndex > headerIndex ? 'changed by the user' : 'changed by the operator/config' - agent.inject( - [{ type: 'text', text: `The approval policy changed from "${told}" to "${current}" (${cause}).` }], - { source: { kind: 'plugin', plugin: 'user-approval' } }, - ) + agent.inject({ + content: [{ type: 'text', text: `The approval policy changed from "${told}" to "${current}" (${cause}).` }], + source: { kind: 'plugin', plugin: 'user-approval' }, + }) }) } diff --git a/packages/ui/user-approval/tests/approval.spec.ts b/packages/ui/user-approval/tests/approval.spec.ts index 0df183ca6a..e3dd4e245a 100644 --- a/packages/ui/user-approval/tests/approval.spec.ts +++ b/packages/ui/user-approval/tests/approval.spec.ts @@ -365,7 +365,9 @@ describe('approval policy (the approval/policy fold)', () => { const agent = { id, session, - inject: (content: { type: string; text: string }[]) => { injected.push(content[0]?.text ?? '') }, + inject: (input: { content: Array<{ type: string; text: string }> }) => { + injected.push(input.content[0]?.text ?? '') + }, } as unknown as Agent return { agent, session, injected } } diff --git a/packages/workflow/tool-ralph/tests/integration.spec.ts b/packages/workflow/tool-ralph/tests/integration.spec.ts index c9492b9450..deb87cedbd 100644 --- a/packages/workflow/tool-ralph/tests/integration.spec.ts +++ b/packages/workflow/tool-ralph/tests/integration.spec.ts @@ -70,7 +70,7 @@ describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => { agentOptions: { provider: 'mock', model: 'mock' }, }) const parent = parentHandle.agent - parent.followup([{ type: 'text', text: 'PARENT_PROMPT_MARKER' }]) + parent.followup({ content: [{ type: 'text', text: 'PARENT_PROMPT_MARKER' }], source: { kind: 'user' } }) await parent.whenIdle() const children: Agent[] = [] diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 6d8890aa2e..4f46cbf35a 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -732,8 +732,6 @@ def scrub_snapshot_header(value: dict[object, object]) -> None: tool.get("name") if isinstance(tool, dict) else "{{tools}}" for tool in tools ] - if isinstance(header.get("messagePrefix"), list): - header["messagePrefix"] = ["{{messagePrefix}}" for _ in header["messagePrefix"]] def render_jsonl(records: list[object]) -> str: diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 664cbdb1db..922921385c 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -151,16 +151,6 @@ "symbol": "SendOptions", "source": "packages/core/agent/src/types.ts" }, - { - "doc": "docs/core-data-structures/core.md", - "symbol": "AliasSendOptions", - "source": "packages/core/agent/src/types.ts" - }, - { - "doc": "docs/core-data-structures/core.zh.md", - "symbol": "AliasSendOptions", - "source": "packages/core/agent/src/types.ts" - }, { "doc": "docs/core-data-structures/core.md", "symbol": "AgentMessageId", @@ -211,16 +201,6 @@ "symbol": "Agent", "source": "packages/core/agent/src/types.ts" }, - { - "doc": "docs/core-data-structures/core.md", - "symbol": "AdditionalContext", - "source": "packages/core/agent/src/types.ts" - }, - { - "doc": "docs/core-data-structures/core.zh.md", - "symbol": "AdditionalContext", - "source": "packages/core/agent/src/types.ts" - }, { "doc": "docs/core-data-structures/core.md", "symbol": "PromptDecision",