diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml index 60f33a0e1e..0379a79e52 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.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-package-invariant-runtime-contracts.md: 7d1fb1ad5a2e7563bdddffde1f49368b9f0c13f7 -2026-07-19-package-invariant-runtime-contracts.zh.md: 669eb02221aea4b0654497bb81327d725648dabe +2026-07-19-package-invariant-runtime-contracts.md: 40d152b2320ac65f9ea7d8732b1a667236d2780a +2026-07-19-package-invariant-runtime-contracts.zh.md: bd2f440d5dce15b352e7bcea0d1243400d290f11 diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md index 7d1fb1ad5a..40d152b232 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md +++ b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md @@ -59,7 +59,7 @@ Session-backed companions validate existing durable events when they load, using `verify-package-invariants` discovers every workspace package and enforces companion source, exact-name registration, named-only Loader shape, `./invariant` exports, publication files, dependencies, TypeScript references, and bundle entries. Its AST rule rejects generated markers, default exports, and unexplained empty installers. A non-empty installer must accept and use the failure reporter, and registration must pass that checked local `install` function. The gate deliberately does not infer semantic quality from method names or helper calls. -Vitest mounts `InvariantService` with `{ enabled: true }` for every package test topology and loads the owning companion. The invariant subpath path mapping resolves source companions instead of stale built output. Focused suites cover every executable companion's valid and invalid observations, and the exhaustive topology runs every source companion through the real Loader namespace normalization. An artifact gate stages each package's exact `npm pack` file inventory, imports its compiled `./invariant` self-reference under plain Node, and repeats that Loader-shape check, so an unpublished shared runtime chunk fails before release. Tests that synthesize event streams must produce a valid surrounding lifecycle unless the test is intentionally asserting a violation. +Vitest mounts `InvariantService` with `{ enabled: true }` for every package test topology and loads the owning companion. The invariant subpath path mapping resolves source companions instead of stale built output. Focused suites cover every executable companion's valid and invalid observations, and the exhaustive topology runs every source companion through the real Loader namespace normalization. After the structural gate validates each publication map, an artifact gate stages its manifest-declared `lib/` files, imports the compiled `./invariant` self-reference under plain Node, and repeats that Loader-shape check, so a companion that imports an undeclared runtime chunk fails before release. Tests that synthesize event streams must produce a valid surrounding lifecycle unless the test is intentionally asserting a violation. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md index 669eb02221..bd2f440d5d 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.zh.md @@ -59,7 +59,7 @@ Status: implemented `verify-package-invariants` 发现每个 workspace 包,并强制 companion 源文件、完整名称注册、仅含具名 export 的 Loader 形状、`./invariant` export、发布文件、依赖、TypeScript reference 和 bundle entry 完整。其 AST 规则拒绝生成标记、默认导出和没有解释的空安装器。非空安装器必须接收并使用失败报告器,注册时还必须传入该经检查的本地 `install` 函数。门禁不会通过方法名或 helper 调用推断语义质量。 -Vitest 为每个包测试拓扑使用 `{ enabled: true }` 挂载 `InvariantService`,并加载所有者 companion。不变量 subpath 的 path mapping 会解析源 companion,而不是陈旧的构建输出。聚焦 suite 覆盖每个可执行 companion 的有效和无效观测;穷举拓扑通过真实 Loader 命名空间归一化运行每个源 companion。产物门禁会按每个包的精确 `npm pack` 文件清单暂存文件,在 plain Node 下导入该包已编译的 `./invariant` 自引用,并重复执行该 Loader 形状检查;这样,未发布的共享运行时分片会在正式发布前导致门禁失败。合成事件流的测试必须构造有效的外围生命周期,除非测试本身就是在断言违规。 +Vitest 为每个包测试拓扑使用 `{ enabled: true }` 挂载 `InvariantService`,并加载所有者 companion。不变量 subpath 的 path mapping 会解析源 companion,而不是陈旧的构建输出。聚焦 suite 覆盖每个可执行 companion 的有效和无效观测;穷举拓扑通过真实 Loader 命名空间归一化运行每个源 companion。结构门禁验证每个包的发布映射后,产物门禁会暂存其 manifest(元数据清单)声明的 `lib/` 文件,在 plain Node 下导入已编译的 `./invariant` 自引用,并重复执行该 Loader 形状检查;这样,若 companion 导入未声明的运行时分片,门禁就会在发布前失败。合成事件流的测试必须构造有效的外围生命周期,除非测试本身就是在断言违规。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/feature/2026-07-07-plan-mode.md b/.agents/notes/implemented/feature/2026-07-07-plan-mode.md new file mode 100644 index 0000000000..06fd50e1f0 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-07-plan-mode.md @@ -0,0 +1,192 @@ +# Agent Note: Plan mode — a logged per-agent session mode + +Status: implemented + +> **Superseded vocabulary (2026-07-22):** [Collapse named session modes into plan mode](../simplification/2026-07-22-plan-specific-collaboration-state.md) replaces this note's generic `dsh-mode`, `mode/set`, definition map, and `ctx.modes` design with the current plan-specific `dsh-plan-mode`, `plan/mode`, `{ section }`, and `ctx.planMode` contract. The review, boundary, reconstructability, and sandbox-orthogonality decisions below remain in force; generic API examples are retained as the historical design this simplification removed. + +## Problem + +Before this change, the harness had no durable way to put one agent into a distinct working stance. Plan mode needs the agent to explore and design under planning guidance, produce a reviewable artifact, cross an explicit approval boundary, and restore that state across resume and fork without making the model-visible request diverge from the session log. + +The extension seams already supplied the surrounding pieces: [`system-prompt/assemble`](../../../../packages/core/system-prompt/README.md) shapes guidance per step and the shipped request is logged in `request/header*` events ([reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md)); [`ctx.userInteraction`](../../../../packages/ui/user-interaction/README.md) carries the approval question and corrective feedback ([ask-user precedent](../../implemented/feature/2026-06-25-ask-user-question.md)); `SessionEventMap` carries durable per-agent facts ([the `todo/write` precedent](../../implemented/feature/2026-06-29-todo-write-tool.md)). The missing piece was the named session state that joins those seams while leaving execution enforcement on the independent sandbox and approval axes. + +## Decision + +The deliverable is **plan mode**. It ships as the first **session mode** — a named, logged, per-agent COLLABORATION state: a mode definition is deployment-configured guidance the model sees, while the mode IN FORCE for an agent is session state folded from its log. Modes are one axis and the enforcement knobs — the sandbox mode, the approval policy — are others: they never read or write each other, matching how Codex keeps its Plan/Default collaboration presets separate from its sandbox and approval settings. One new product package, `@deepseek-ai/dsh-mode` at `packages/mode/mode/`, owns the event vocabulary, a thin `ctx.modes` service, and every listener; the loop does not change. `plan` is the only required definition — the mode-shaped vocabulary exists so a second mode never renames durable event types, not because more modes ship now. + +The state is one `SessionEventMap` member: **`mode/set`**, a log-only, non-surface event carrying `{ mode: string }` with whole-value-replace semantics, plus a pure `foldMode(events)` that returns the mode in force — the last `mode/set`, or the default mode when none exists. Because [the log is the fact channel](../../implemented/architecture/2026-06-30-event-domain-semantics.md), resume, fork, and compaction restore the mode with no extra machinery, and UIs read flips off `session/event`. The default mode is the absence of mode guidance — no section, filtering, or gate. Loading `dsh-mode` still contributes one stable `exit_plan_mode` schema in every mode; that fixed cost avoids tool-catalog churn at mode boundaries. + +A mode's whole surface is soft: a `mode:policy` prompt section renders the active definition's guidance, while `exit_plan_mode` remains in the registered tool catalog across every mode and rejects at execution unless the folded mode is `plan`. A transition therefore changes only the system-prompt portion of the attributable `request/header` on the next step, keeping [reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md) green without changing native schemas or Code Mode's SDK. A mode deliberately enforces NOTHING: no execution gate, no tool filtering, no reach into the sandbox or approval knobs — a user who wants a hard read-only floor while planning switches the sandbox-mode option beside the mode picker, in either order, and neither axis disturbs the other. There is likewise NO per-mode tool allow/deny list — which tools a mode admits is an effects question, parked until tool definitions declare their effects ([Deferred](#deferred)); a mode's restraint is its section's guidance plus the exit review. + +The model leaves plan mode through the **`exit_plan_mode`** tool: its single argument is the plan text, which makes the plan reconstructable from the log, and the tool conducts the review itself through the user-interaction seam — a question whose supporting detail carries the exact plan, with options and a free-text channel, not a bare permission — so an approval flips the logged mode back to the default, and a rejection becomes the corrective error carrying the user's feedback verbatim, which keeps the model planning with direction. A user flips the mode from any surface through `ctx.modes.set()`; the flip is applied at the next turn boundary (session events are turn-enclosed) and narrated to the model once, only when the model-visible state actually changed. + +## High-level API + +### A plan-mode session end to end + +The user switches the session to plan mode through the ACP mode picker or `/plan [message]` in a terminal front door, and from the next step every request ships the configured plan guidance section. When the optional message is present, that same command submits it into the affected step. The `exit_plan_mode` schema was already present in default and remains byte-identical. + +The model explores and designs; the section's guidance is what defers changes into the plan. The sandbox and approval knobs keep whatever the user set them to — a deployment (or user) that wants kernel-enforced read-only during planning pairs plan mode with the independent sandbox-mode option. + +When ready, the model calls `exit_plan_mode` with the plan markdown as its argument; the review question carries that exact markdown as supporting detail — approve, or keep planning, with free-text feedback welcome. A native call also renders the plan card; a Code Mode nested dispatch has no native card, so the review detail is the common presentation surface. + +On approve, the tool flips the logged mode back to the default: the next step drops the plan section while retaining the same tool catalog (the changed header is in the log), and execution tracking from there is already `todo_write`'s job. On keep-planning, the model receives a corrective error carrying the user's feedback text, revises, and re-presents. + +### Deployment configuration + +Mode definitions are validated plugin Config — per repo convention, changeable from `cordis.yml` with no code edit. The deployment must provide the complete `plan` section; the package embeds no model instructions. Additional modes use the same config map: + +```yaml +- id: mode + name: '@deepseek-ai/dsh-mode' + config: + modes: + plan: + section: | + You are in plan mode: explore and design, then present the + plan for approval through exit_plan_mode. +``` + +A definition is exactly `{ section }` — there is deliberately no per-mode tool list and no enforcement field ([FAQ](#faq)). Definition names use the lowercase slash-command subset `/^[a-z][a-z0-9_-]*$/u`; `default` is reserved (the absence of policy) and rejected as a key. An invalid name or unknown definition key — a `tools` list or an `access` cap included — fails validation at load; an unknown mode name fails loudly at `set()` time. + +### In the terminal + +Terminal front doors get one entry command per configured definition through the plugin-owned command registry (`@deepseek-ai/dsh-commands`): `dsh-mode` registers `/plan [message]` for the required definition and, for example, `/review [message]` when `review` is configured. Each command records its named switch; a non-empty optional message is trimmed and passed to `agent.steer()`, which places it in a running agent's next step or delegates to `send()` for a new idle turn. The command name and result stay out of model history, while that explicit message is logged as an ordinary user message under the selected mode. The synthetic `default` entry contributes no command. The exit review prompts right in the terminal with no new machinery: it is an ordinary user-interaction question, so it rides the composed user-interaction provider's prompt queue that `ask_user_question` already uses. + +### Over ACP + +The mode PICKER is this package's surface: `session/new`/`session/load` advertise `availableModes`/`currentModeId` from `ctx.modes` (consumed opportunistically via `ctx.get`, the `tool-bash` pattern), `session/set_mode` calls `set()` and notifies `current_mode_update` optimistically (the pending mode IS the user's selection; the logged `mode/set` follows at the boundary), and a `session/event` listener re-notifies on each logged flip that differs from the last sent. The exit tool reuses the user-interaction ACP provider's elicitation flow; its ACP mapping carries the review `detail` because Code Mode nested dispatches have no native plan card, while native calls may additionally stream the plan card. Individual environment knobs — sandbox mode, approval policy, the model — are NOT modes and belong to `session/set_config_option` ([FAQ](#faq)). + +### For agent creators + +`ctx.modes` is the whole programmatic surface: `list()` returns the configured definitions plus the synthetic `default` entry (for pickers), `get(agent)` returns the folded mode plus any pending intent, and `set(agent, mode)` validates the name against `list()`'s vocabulary and records the boundary-applied intent — `default` is always a valid target, so exiting a mode is the same call as entering one. There is no creation-time mode option — a caller selects through `set()` before the first turn, which flushes identically. There is no live `agent/*` mirror to subscribe: UIs read `mode/set` off `session/event`, per [event-domain semantics](../../implemented/architecture/2026-06-30-event-domain-semantics.md). + +## Detailed design + +### Vocabulary + +```text +'mode/set': { mode: string } // SessionEventMap merge in dsh-mode: log-only, non-surface, + // whole-value replace — the last one in the log wins +DEFAULT_MODE = 'default' // the fold of a log with no mode/set; reserved, not definable +``` + +The payload carries no reason/provenance field: a tool-driven flip sits next to its `tool/call` in the log and a user flip sits at its turn boundary, so the cause is log-adjacent — the same "narrative fields are derivable" call the [reconstructability Agent Note](../architecture/2026-07-05-reconstructable-requests.md) made for request-header facts (the in-flight `env/state` event carries a `source` precisely because its drift variant has NO log-adjacent cause — a contrast, not a conflict). Mode names are config-declared vocabulary, not opaque cross-boundary ids, so they stay bare strings (no `Branded`). + +### Config and the resolve step + +```text +interface ModeDefinition { section: string } // prompt text — a mode's whole vocabulary +interface ModeConfig { modes: Record } // plan is required and owns its complete prompt +resolveConfig(config): ResolvedModes // explicit resolve (the dsh-bash template), fail-loud: + // missing plan, 'default', blank sections, and unknown keys rejected +``` + +The one-field shape is deliberate minimalism, not the final vocabulary: a per-tool policy dimension returns as effects metadata on tool definitions ([Deferred](#deferred)), read here rather than re-declared per mode — the config shape must not need a migration when it arrives. + +### The fold, the service, and the flush + +`foldMode(events)` is pure (exported for reconstructors and tests) and folds the append-only session log directly; `mode/set` is not a surface node, so compaction cannot shadow it. `set(agent, mode)` validates the name against `list()`'s vocabulary — the configured definitions plus the reserved `default`, which is rejected as a config KEY but always accepted as a `set()` TARGET — drops a no-op (target equals pending, else current), and otherwise records `{ mode, narrate }` in a `WeakMap` pending-intent slot. It cannot append immediately because [every session event is turn-enclosed](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md) and an idle agent has no open turn. + +Contained listeners on the loop's interception seams ([defensive patterns](../../../../docs/defensive-patterns.md): a policy plugin must not block a prompt or a turn) flush the pending intent as a `mode/set` append — `agent/prompt-submit` fires inside the just-opened turn before its first assembly, and `agent/turn-continuation` fires after an ordinary step closes before its successor. Automatic request recovery bypasses continuation, so a prepended `agent/request-error` wrapper delegates through the composed policy and asynchronous backoff, then flushes only a `retry` decision before the waterfall returns to the loop; an effect-scoped lifetime guard suppresses a captured wrapper that resumes after plugin disposal. All three paths sit outside tool execution and log publication (post-commit `session/event` observers are observe-only), so every step runs under the mode its assembly folded. When the flushed mode differs from the fold at the last `request/header`, the flush appends one coalesced `context/message` notice in the same frame ("The user switched this session to plan mode."); the user-visible narration cases are enumerated in the [FAQ](#faq). + +### The soft layer: a computed section and a stable exit schema + +The registered prompt section reads the calling agent's mode from `AssembleContext.agent` and resolves to the active definition's guidance or `''`. The loop renders per step and logs a complete `request/header` whenever the rendered header changes, so entering or leaving a mode is attributable. The section is static per mode and the plan itself stays in the conversation as messages and tool arguments; re-injecting separate plan state on every request ([Prior art](#prior-art)'s compaction-survival hack) is unnecessary prompt churn. + +The guidance contribution is `{ name: 'mode:policy', order: 50, text: context => … }`: after persona (0), before tool guidance (100–199), and empty for default or agent-less assembly. `exit_plan_mode` is registered once through `ctx.tools` and never filtered, so native schemas and Code Mode's generated SDK remain byte-identical across mode switches; a deployment without `dsh-mode` lacks that one binding. There is NO `tools/pre-execute` listener: a mode gates nothing, while the exit tool's own folded-mode check rejects out-of-plan calls. The exit review is a question with options and feedback, not a permission, so it lives inside the tool's execution over the user-interaction seam. + +### `exit_plan_mode` + +`defineTool` has one required `plan: string` argument. Native execution records it in the ordinary `tool/call`; Code Mode records the outer `run_code` source before execution and appends the normalized nested arguments in `tool/code-dispatch` after the dispatch settles. `execute` rejects an agent-less call (the [`todo_write` precedent](../../implemented/feature/2026-06-29-todo-write-tool.md)), rejects any folded mode other than `plan`, rejects an empty or heading-less plan before asking the reviewer, then conducts one single-select `ctx.userInteraction.ask()` review whose `detail` is the exact plan — approve or keep planning — with free-text feedback open. Only exactly one `Approve` selection consents; every other shape fails closed. Approval records a SILENT boundary-applied intent to switch to `default` and returns a short confirmation. The deployment guidance tells the model to make this the only and final tool call in its response; if a model violates that rule, the runtime still holds plan guidance for the rest of the batch, and the next step logs a changed header with the guidance removed and tool schemas unchanged. Every non-approval outcome returns a corrective `isError` and leaves the mode in `plan`. + +Its [render intent](../../implemented/architecture/2026-07-02-tool-render-intent-union.md), decided up front: `presentCall` is a `generic` card titled by the plan's first heading with the plan markdown as content, plus a `generic` result card. Native front doors show that card before the question; Code Mode nested dispatches do not produce native call-card events, so the user-interaction `detail` independently carries the same plan on every provider. The seam is consumed opportunistically (`ctx.get('userInteraction')`), so `dsh-mode` composes without it and degrades to the manual exit pinned in the [FAQ](#faq). + +### Dependencies and surfaces + +`dsh-mode` is one product package, not a capability-seam trio ([Alternatives considered](#alternatives-considered)): it peers on `cordis`, `dsh-session`, `dsh-agent`, `dsh-tools`, and `dsh-system-prompt`, injects `['tools', 'systemPrompt']`, and reads `ctx.userInteraction` opportunistically at execute time (a type-only peer edge on `dsh-user-interaction`); its only UI-facing edges are optional type-only peers (`dsh-commands` for the per-definition entry commands). Beyond the `ctx.modes` call surface everything participates through listeners, so dropping the package gracefully removes modes rather than breaking a consumer. Terminal front doors need no mode-specific code: `dsh-mode` itself registers each definition's command on the command registry when one is composed (an optional type-only peer edge on `dsh-commands`), and the exit review rides the composed user-interaction provider's prompt queue. The ACP wire mapping is pinned in [High-level API](#over-acp); package-wise the bridge takes a type-only peer edge on `dsh-mode` and reads the service opportunistically, so a bridge without the plugin behaves exactly as today. + +### The recorded scenario and the harness op + +`input.json` gains one step op, `{ "op": "setMode", "modeId": "plan" }`, driven through the real `session/set_mode` RPC, and a scripted `elicitationAnswers` queue. The `plan-mode` scenario enters plan before turn 1, runs a real `cat` under the independently configured sandbox, presents a plan through `exit_plan_mode`, receives scripted approval, then edits on the next step. The first `request/header` contains the full stable toolset plus the configured mode section; the post-approval changed header retains byte-identical tool schemas and removes only that section. `plan-mode-reject` pins corrective free-text feedback and the unchanged plan state. Both recordings replay host commands under Seatbelt or bwrap; backend-specific sandbox denial stays at the bash-tool unit tier. + +### The mechanical tail + +No new cordis event is declared (`mode/set` rides `session/event`; the listeners attach to existing waterfalls), so the events catalog is untouched. Regenerated in the same change: the persistence log catalog (`mode/set`), the services catalog (`ctx.modes`, JSDoc-complete), the config catalog (`ModeConfig`), the tool catalog (`exit_plan_mode`), the producer/consumer map and doc graphs, and the module graph. Repo plumbing: a root tsconfig `paths` entry, the new group's README plus a [packages map](../../../../packages/README.md) row (a new top-level group is the deliberate act that table names), an `architecture.md` capability-services row for `ctx.modes` (budget-checked), and the cookbook row upgrade. + +## Deferred + +Each behind its own decision: subagent mode inheritance via a forwarded creation-time mode option (removed as unconsumed; it returns with its first consumer), preset modes beyond `plan` (read-only, accept-edits), the idle-record primitive if pending-intent loss proves real, and — the big one — **effects self-declaration on tool definitions**: a per-tool read-only/mutating classification (the MCP `ToolAnnotations` vocabulary — `readOnlyHint`/`destructiveHint` — is the natural template, with its untrusted-hint caveat implying trust tiers). That item is what a general per-mode tool policy waits on: this Agent Note first shipped an interim per-mode name allowlist and removed it before release — a hand-maintained list mislabels the effects question, must track every tool a deployment composes, and rots silently as tools arrive — so mode-scoped tool availability (and per-tool `ask` policies) returns as a CONSUMER of declared effects, which is its restart trigger. + +The canonical [`examples/acp-agent`](../../../../examples/acp-agent/) composition mounts the mode and question-tool plugins on the full ACP coding server; plan mode is an additive session feature, not a second server profile. Its snapshot suite pins the plan-shaped initial header, a real read, scripted approval, stable tool schemas across the pure-removal header delta, a subsequent edit, rejection feedback, and the keyless mode wire. A self-skipping real-API smoke boots that same leaf, verifies the file before approving the review, and verifies the approved implementation afterward. + +## FAQ + +Behavioral clarifications of the chosen design; rejected designs live in [Alternatives considered](#alternatives-considered), accepted costs in [Consequences](#consequences). + +**When does a user's mode flip take effect?** At the next pre-assembly boundary: `agent/prompt-submit` covers the first step, `agent/turn-continuation` covers a normal successor, and the post-composed `agent/request-error` retry decision covers automatic recovery. A mode selected while a request or retry backoff is in flight therefore shapes the following model request. This is the "applies to subsequent requests" semantics every product in [Prior art](#prior-art) ships. + +**When is a mode change narrated to the model?** Only when the model-visible state actually changed: the flush compares the flushed mode against the fold at the last `request/header` and narrates once, coalesced. A net-zero flip sequence (plan then back, all before the boundary) narrates nothing; a tool-driven exit narrates through its own tool result instead; a mode set before the first turn narrates nothing — the section is the state statement. The principle is the in-flight env-state proposal's boundary narration: a silently flipped prompt surface leaves the transcript arguing from a state the header no longer has. + +**What happens on resume when the config no longer defines the folded mode?** A folded mode name the current config no longer defines behaves as the default mode without a notice, so the session neither gains a substitute restriction nor becomes unusable. `set()`'s loud validation covers only the write path; a resumed log answers to the config it finds. + +**What if a deployment composes no user-interaction provider?** Plan mode stays safe but manual: `ctx.userInteraction.ask()` throws `NO_PROVIDER` (and an absent seam never resolves at all), the tool returns the corrective `isError`, and the exit degrades to the user toggling modes — never to an unreviewed exit. The mode section tells the model to present its plan through `exit_plan_mode` — and to ask the user in prose if that fails — so it keeps presenting instead of stalling. + +**Why is there no per-mode tool allowlist?** Because "which tools are safe in a planning mode" is a property of each TOOL (its effects), not of the mode — a per-mode name list re-declares that fact in the wrong home, must enumerate every tool the deployment composes (MCP servers included), and rots silently as tools arrive. Until tool definitions declare their effects ([Deferred](#deferred), where the removed interim allowlist is archived with its restart trigger), a mode restrains by its section and the exit review; the exposure is an accepted cost ([Consequences](#consequences)). + +**Do subagents inherit the parent's mode?** A fork child inherits for free — the parent's `mode/set` is inside the seeded prefix. A spawn child starts in the default mode; a creation-time mode option and automatic forwarding by subagent providers are deferred together ([Deferred](#deferred)). + +**How does plan mode relate to the sandbox's read-only mode?** They are separate axes that never touch: the mode is the collaboration stance (a `mode/set` fold), the sandbox mode is an enforcement knob (a `bash/sandbox-mode` fold, [the sandbox Agent Note](2026-07-06-sandbox.md)) — plan mode neither reads nor caps it, exactly as Codex keeps its Plan/Default presets separate from its sandbox and approval settings. A user who wants kernel-enforced read-only while planning sets both: flip the mode picker AND the sandbox-mode option, in either order; each switch changes only its own fold, so there is no interference and no restore step to crash out of. The log attributes each axis to its own event — the stance to `mode/set`, the confinement to `bash/sandbox-mode`. + +**Why aren't sandbox mode, approval policy, or the model themselves modes?** They are individual environment knobs and belong to ACP's `session/set_config_option`; the division this proposal pins is picker-to-modes / knobs-to-config-options, recorded in [the feature matrix](../../../../packages/ui/acp/acp-feature-support.md) now that both this stack's picker and the sandbox stack's config options are landed. A mode definition may later bundle env facts (applied through `ctx.envState` where mounted) so a Codex-style preset stays a single mode; fusing approval policy into the mode CONCEPT itself is rejected in [Alternatives considered](#alternatives-considered). + +## Prior art + +A survey of shipped plan modes (Claude Code, Cursor, Copilot, OpenCode, Gemini CLI, Cline, Windsurf, Codex) shows the same five parts everywhere — the low-authority tool policy, plan artifact, approval moment, execution-state switch, and durable state that [Problem](#problem) builds on. + +The mode surface is a LIST everywhere it is advertised, never a boolean: Claude Code's picker offers `plan` beside `acceptEdits` (plus an auto-mode entry into plan), and Codex exposes `Plan` beside `Default` as collaboration-mode presets while keeping approval and sandbox settings separate. This is the surface [the ACP feature matrix](../../../../packages/ui/acp/acp-feature-support.md) records as the gap, and what sizes the vocabulary as named modes rather than a flag. + +The deployment-owned example prompt borrows the instrumental behavior, not product-specific mechanics. From Codex: remain in plan mode despite imperative implementation language, explore before asking, distinguish repository facts from user-owned choices, and make the plan decision-complete across APIs, data flow, failures, tests, and assumptions. From Claude Code: prohibit mutations and commits, prefer existing patterns, use questions only for requirements or approach choices, and finish through the exit tool rather than a prose approval request. It deliberately omits Codex protocol tags and Claude's plan-file or phased-subagent machinery because those belong to their runtimes, not this plugin contract. + +The ecosystems that leave modes to convention show the failure shapes to avoid. Pi-style mode extensions fight over a last-wins global active-tool list, enforce "read-only" by prompt text alone (a hallucinated call to a still-registered tool executes), and re-inject plan state into every request to survive compaction. The contested global list and the re-injection hack close structurally here — per-agent folded state, and a log-only non-surface event compaction cannot shadow. The prompt-only shape, by contrast, is deliberately KEPT — it is what Codex ships for Plan, and it is why the mode axis composes freely with the enforcement axes: a deployment that wants a hard floor pairs the mode with the independent sandbox knob instead of the mode carrying its own enforcement ([FAQ](#faq)). + +## Alternatives considered + +**Permission modes as the concept (the Claude Code shape).** One `permissionMode` fusing approval policy and tool policy. Here those are two axes with two owners: the approval seam owns "who answers this question", modes own "what surface does the model get". ACP models them as related but distinct (a mode may select an approval policy later — a mode definition gains a field, not a merger). + +**A capability-seam trio.** Interface/implementation/consumer fits a swappable backend; a mode's variable parts are config values, not implementations. Splitting would manufacture an empty implementation package — the same "don't split preemptively" call the approval seam and [`todo/`](../../implemented/feature/2026-06-29-todo-write-tool.md) made. + +**Loop-owned mode state.** Rejected on the standing rule (plugins, not loop changes): every hook the feature needs — assemble, pre-execute, turn boundaries, session events — is already a documented seam, so a loop edit would buy nothing but coupling. + +**A per-mode tool allowlist with a deny-by-default gate (the first shipped shape).** Removed before release. A hand-maintained name list re-declares a per-TOOL fact (its effects) per MODE: it must enumerate every tool the deployment composes — MCP servers and future registrations included — and it rots silently as tools arrive (a new read-only tool is blocked until someone edits every mode; the author burden lands on whoever knows the mode, not whoever knows the tool). It also over-promises: the list looks like a security boundary while the real boundary for anything non-shell does not exist. The general dimension is parked on effects self-declaration ([Deferred](#deferred)); the consequence — plan mode is guidance-only, the very Pi hole the gate once closed — is accepted deliberately, priced in [Consequences](#consequences). + +**An `access` sandbox cap on the mode (the second shipped shape).** Also removed before release. `ModeDefinition.access` clamped the bash seam's per-call sandbox resolution to a mode-declared ceiling (a `bash/resolve-mode` waterfall + ladder-min listener, with guards withholding bash under an unconfinable executor and denying escalation mid-mode). The state stayed orthogonal — the clamp never wrote the sandbox knob — but the AXES did not: entering plan changed what the sandbox enforced, fusing the collaboration stance with an enforcement level and contradicting the Codex-shaped separation the review converged on (Plan/Default presets never touch sandbox or approval settings). One user-visible symptom of the fusion: flipping the sandbox option to `workspace-write` while planning silently did nothing. The cap, the waterfall, and the mode→bash dependency edge were removed together; a deployment gets kernel-enforced read-only planning by pairing the mode with the independent sandbox-mode option, and a mode-triggered PRESET (a mode definition bundling suggested knob values, applied as ordinary knob switches) can return later without re-fusing the axes. + +**Runtime-only mode (UI- or bridge-local, unlogged).** Resume and fork would silently drop the mode, and the header deltas a mode causes would have no attributable cause in the log. Logged state is what makes the mode auditable and restorable for free. + +**Mode flips as `context/message` via `agent.inject()`.** Reuses an existing turn-enclosure path, but puts policy state into the model transcript — the model does not need to be told twice (the section already tells it), and a log-only fact should not occupy surface. + +**A plan-file store (`.plans/` directory).** A second durable home for what the log already carries replayably; a deployment wanting files can add a tool that writes them. One home per fact. + +**A boolean `planMode` instead of named modes.** Too narrow for the surface the repo already tracks: ACP advertises a mode LIST and the shipped pickers fill it with more than plan ([Prior art](#prior-art)); generalizing later would rename durable event vocabulary. The string-shaped mechanism costs nothing extra now; only `plan` ships as a definition. + +**A tool-policy-stack service (the Pi-critique remedy).** A dedicated composition service for tool policies is premature: this implementation performs no mode-scoped tool filtering, and future effect policies can compose through the existing guarded execution seams. Formalize only when declared tool effects create a concrete composition requirement. + +**Exit approval through the approval seam (a `{ kind: 'ask' }` gate decision).** The original sketch, natural while the approval seam was the only asking machinery in flight — but it seats a review in a permission chair: the seam's outcome vocabulary is deliberately closed and one-shot (`allowed-once`/`rejected`), so a rejection carries no feedback and an approval can never grow options (approve-and-accept-edits). The exit moment is a question, not a permission — the user-interaction seam gives it options plus the free-text channel, and the rejection feedback reaches the model verbatim. The approval seam remains the right seat for genuine permission gates (the sandbox escalation), and the registry's `ask` vocabulary stays available to deployments that want one there. + +**Exit by prose or steering instead of a tool.** No artifact and no approval moment — the tool's argument IS the reviewable plan, and its review question is what gives the human a structured yes/no attached to the exact transition. + +## Consequences + +What holds now, pinned by the unit, protocol, snapshot, and real-API tiers: + +- The mode in force is a pure function of the session log: resume and fork restore it with no extra machinery, and a `mode/set` is followed by a matching complete `request/header` on the next changed step. +- A user-driven flip narrates exactly once at the next boundary and a net-zero flip sequence narrates nothing; a tool-driven exit narrates only through its tool result. +- In default mode the plugin contributes no mode section but does contribute the stable `exit_plan_mode` schema; a deployment without `dsh-mode` lacks that binding. +- Native tool schemas and Code Mode's SDK stay byte-identical across default, plan, and custom-mode transitions; only the configured guidance section changes. +- Plan mode changes nothing on the enforcement axes: the toolset, the sandbox mode, escalation, and the approval policy behave identically in plan and default — pairing the mode with the independent sandbox/approval knobs is how a deployment hardens planning. +- Mode definitions are changeable from `cordis.yml` with no code edit; the complete plan instructions are required there, while missing plan config, malformed definitions, and unknown keys fail at load and unknown mode names fail at `set()`. +- `exit_plan_mode` is always advertised, rejects outside plan, drops only plan guidance after approval, and carries keep-planning feedback in a corrective `isError`; ACP mode updates and each surface's user-interaction provider carry the human side. +- The docs tail shipped with the landing: READMEs, regenerated catalogs (persistence log, config, cordis services, tools), the packages map and architecture rows, and the cookbook row. + +The accepted costs: a pending user flip set while idle is lost if the process dies before the next turn (the UI re-applies; the idle-record primitive is the escape hatch if this bites in practice). A mode transition changes the system prompt at order 50, so the cache path from that point onward changes, but the tool schemas and Code Mode SDK no longer churn. **A mode restrains by guidance alone**: a model that ignores the section CAN mutate during plan — the review moment, the session log, and independent sandbox, approval, and filesystem policies are the containment surface. Hardening planning means setting those knobs, not widening the mode; the removed enforcement shapes and their effects-declaration restart trigger remain in [Alternatives considered](#alternatives-considered) and [Deferred](#deferred). The ACP mode surface carries the picker while sandbox, approval, and model selectors remain config options under the division pinned in the [FAQ](#faq) and [feature matrix](../../../../packages/ui/acp/acp-feature-support.md). If ACP removes session modes in favor of config options, the picker mapping can migrate without changing the logged mode state or model surface. diff --git a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.i18n.yaml b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.i18n.yaml index 0addad9e97..9789d902fc 100644 --- a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.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-plugin-command-registration.md: a207c6257bd4e9e4013f1abb661dd967ed2a52dc -2026-07-19-plugin-command-registration.zh.md: e21d187ded0ee0f4daa370655994eb306fb1c5fd +2026-07-19-plugin-command-registration.md: bc3d33f9abf7cd87b78aac8f7d36ac9c021a7910 +2026-07-19-plugin-command-registration.zh.md: 054ab3a90eeecc8c5ddc2ff072b53112fd8e7845 diff --git a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md index a207c6257b..bc3d33f9ab 100644 --- a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md +++ b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md @@ -8,7 +8,7 @@ English | [中文](2026-07-19-plugin-command-registration.zh.md) The TUI owns seven slash commands, while ACP defines a standard command catalog and invocation shape. Keeping command names, help text, autocomplete, dispatch, and cancellation inside each adapter makes every new command an adapter edit, prevents optional plugins from contributing commands, and lets the two front doors drift. Treating slash input as an ordinary model prompt is also unsafe: a user-visible direct action can unexpectedly consume tokens or let the model reinterpret an unknown command. -A shared mechanism must remain a UI concern rather than a model tool or agent-loop branch. It also needs exact per-agent visibility, HMR-safe removal, per-session ACP discovery, direct result rendering, and request-scoped cancellation without adding command text or output to model history. +A shared mechanism must remain a UI concern rather than a model tool or agent-loop branch. It also needs exact per-agent visibility, HMR-safe removal, per-session ACP discovery, direct result rendering, and request-scoped cancellation without automatically adding command text or output to model history. ## Decision @@ -30,7 +30,7 @@ Registration and removal emit the unfiltered, non-vetoing `commands/change` regi ### Direct dispatch and cancellation -Commands run in a human-only command plane. Their input does not become `user/message`, their output does not become a session event, and neither is sent to the model. A handler receives the exact target agent, raw input, and request-owned `AbortSignal`. The registry stops awaiting an uncooperative handler when the signal aborts; the handler remains responsible for stopping external side effects already started. +Commands run in a human-only command plane. The registry does not turn their input into `user/message`, their output does not become a session event, and neither is sent to the model implicitly. A handler receives the exact target agent, raw input, and request-owned `AbortSignal`; a producer may explicitly schedule separate model-visible work through that agent and then owns its logging and lifecycle contract. The registry stops awaiting an uncooperative handler when the signal aborts; the handler remains responsible for stopping external side effects already started. Expected handler failures return `CommandResult.error`. Thrown or malformed results remain adapter-visible command failures, not model messages. This boundary deliberately separates UI output from durable domain mutation: a goal command may change `ctx.goals`, for example, but the goal service owns that persisted state. diff --git a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.zh.md b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.zh.md index e21d187ded..054ab3a90e 100644 --- a/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.zh.md +++ b/.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.zh.md @@ -8,7 +8,7 @@ Status: implemented TUI 拥有七个斜杠命令,而 ACP 定义了标准命令目录与调用形态。如果命令名、帮助文本、自动补全、分派和取消都留在各适配器内部,每个新命令都需要修改适配器,可选插件无法贡献命令,两个前端也会逐渐偏离。把斜杠输入当作普通模型提示同样不安全:用户可见的直接操作可能意外消耗 token,或让模型重新解释未知命令。 -共享机制必须仍是 UI 关注点,而不是模型工具或智能体循环分支。它还需要精确的逐智能体可见性、可安全 HMR 移除、逐会话 ACP 发现、直接结果渲染和请求作用域取消,同时不得把命令文本或输出加入模型历史。 +共享机制必须仍是 UI 关注点,而不是模型工具或智能体循环分支。它还需要精确的逐智能体可见性、可安全 HMR 移除、逐会话 ACP 发现、直接结果渲染和请求作用域取消,同时不会自动把命令文本或输出加入模型历史。 ## 决策 @@ -30,7 +30,7 @@ TUI 拥有七个斜杠命令,而 ACP 定义了标准命令目录与调用形 ### 直接分派与取消 -命令在仅面向人类的命令平面中运行。输入不会成为 `user/message`,输出不会成为会话事件,两者都不会发送给模型。处理器接收准确的目标智能体、原始输入和请求拥有的 `AbortSignal`。信号中止时,注册表不再等待不合作的处理器;处理器仍负责停止已经启动的外部副作用。 +命令在仅面向人类的命令平面中运行。注册表不会把输入转成 `user/message`,输出不会成为会话事件,两者都不会隐式发送给模型。处理器接收准确的目标智能体、原始输入和请求拥有的 `AbortSignal`;生产者可以通过该智能体显式调度单独的模型可见工作,随后由生产者负责其日志记录和生命周期契约。信号中止时,注册表不再等待不合作的处理器;处理器仍负责停止已经启动的外部副作用。 预期的处理器失败返回 `CommandResult.error`。抛出的异常或格式错误的结果仍是适配器可见的命令失败,而不是模型消息。该边界有意分离 UI 输出与持久领域变更:例如目标命令可以改变 `ctx.goals`,但持久状态由目标服务拥有。 diff --git a/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md b/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md index 65ad437ca9..fef5852153 100644 --- a/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md +++ b/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md @@ -4,36 +4,45 @@ Status: implemented ## Problem -The keyless GitHub CI gates are mostly orthogonal: typecheck, lint, documentation freshness, coverage, snapshot replay, build, package-publication hygiene, demo smoke, and built-bin smoke fail for different reasons and do not need each other's runtime state. Running them as one ordered command chain makes the workflow wall clock equal the sum of those gates, while splitting every leaf gate into its own GitHub job repeats checkout, Node setup, pnpm restore, and install work until orchestration overhead becomes the bottleneck. +The keyless GitHub CI gates are mostly orthogonal: typecheck, lint, documentation freshness, coverage, snapshot replay, build, package-publication hygiene, demo smoke, and built-bin smoke fail for different reasons and do not need each other's runtime state. Running them as one ordered command chain makes the workflow wall clock equal the sum of those gates, while splitting every short leaf into its own GitHub job repeats checkout, Node setup, pnpm restore, and install work until orchestration overhead becomes the bottleneck. -The hard part is the artifact boundary. `publint`, `verify-node-next-types`, and built-bin smoke tests need the built `lib/` outputs, while most gates only need source and dependencies. A blind fan-out either races those artifact consumers before `pnpm run build` has emitted declarations and bundles, or repeats the build in every artifact-dependent job. +The original broad-lane split stopped meeting that balance as the workspace grew. On the merge of PR #404, Linux static, coverage, snapshot, and artifact jobs took 148, 195, 94, and 230 seconds; Windows static and artifacts took 251 and 482 seconds. Package-manager packing once per package dominated both artifact validators, coverage needlessly rebuilt output before a source-only suite, and CPU-heavy gates contended inside the static and coverage lanes. + +The artifact boundary remains load-bearing. `publint`, `verify-node-next-types`, compiled invariant loading, and built-bin smoke tests need emitted `lib/` output. Sharding cannot race those consumers ahead of build or replace their published-artifact signal with source execution. ## Decision -[CI](../../../../.github/workflows/ci.yml) groups keyless checks into broad primary-runtime lanes plus a compatibility matrix. The workflow file owns the current lane and runtime inventory. +The production topology below is historical and is superseded by [Evidence-based larger hosted runners](2026-07-22-evidence-based-larger-hosted-runners.md). The larger-runner decision removes its shard selectors and workflow jobs; this note preserves why that earlier topology was implemented. -Each lane delegates to [scripts/run-gates.ts](../../../../scripts/run-gates.ts), which schedules independent gates with bounded concurrency and prints an attributable result block for each one. Artifact consumers depend on one build within their lane, while compatibility jobs combine typechecking with a real unbuilt worker launch to cover runtime-specific loader behavior. +[CI](../../../../.github/workflows/ci.yml) treats one minute for non-Windows jobs and three minutes for Windows jobs as observed performance targets, not cancellation deadlines. Hosted-runner variance should leave complete timing evidence and useful failure logs instead of cancelling an otherwise-correct gate. The [serial cross-platform CI reference](2026-07-21-serial-cross-platform-ci-reference.md) independently runs the complete unsharded primary Node aggregate on Linux, macOS, and Windows so the optimized lane inventory is not its own completeness oracle. -Generated `.sessions/` logs and `.doc-typecheck-*` temp directories are ignored by lint. The aggregate local CI mode still runs demo smoke after lint, while the split GitHub static lane can run demo smoke directly because lint is isolated in its own lane. +In that topology, [scripts/run-gates.ts](../../../../scripts/run-gates.ts) was the common bounded scheduler and GitHub supplied explicit shard names for the expensive gate families. `scripts/static-shards.ts` partitioned static gates into foundation, documentation-type, API-contract, catalog, prose, documentation-projection, and documentation-build ownership and rejected a missing or duplicate gate assignment. Linux lint used disjoint A-C, D-M, N-S, and T-Z package-source and package-test lanes, while Windows used complete package-source and package-test lanes; both included a repository complement starting from `.` so new top-level targets could not disappear between shards and owned the single cross-file duplication run. `scripts/coverage-shards.ts` assigned every workspace package to exactly one source-coverage lane. Directory filters retained a trailing separator because Vitest positional filters match substrings and would otherwise admit prefix-named siblings. Each coverage lane included only its owned source files, repeated the exhaustive companion topology test, and ran without a preceding build because the complete coverage suite passes from a tree with every generated `lib/` removed. -Build output is produced once inside the Node 24 artifact lane. The artifact consumers (`publint`, `verify-node-next-types`, and built-bin smoke) declare a dependency on `build`, so there is no upload/download handoff and no consumer can race ahead of declarations or bundles. The CI coverage reporter is text-only while local coverage keeps the HTML report. +Snapshot replay used two explicit multi-file lanes and eight scenario partitions of the large ACP file. `scripts/snapshot-shards.ts` owned that inventory, and its test discovered every file admitted by the snapshot config. Each snapshot job installed dependencies while its Linux runner prepared Bubblewrap, built the shipped runtime, and ran only its assigned replay surface. The suite retained bounded concurrency of five subprocesses because replay spent most of its time waiting on child protocol I/O. Fixture guards still inspected the complete ACP scenario table in every partition. -Both workflows cache the pnpm store. The real-API workflow uses the shared bounded Vitest file pool rather than a separate job per test group. +Cold standalone documentation typechecking rebuilds the complete project-reference graph, so a dedicated documentation-type lane builds once and checks Markdown blocks against those declarations. The Linux documentation lane uses VitePress's MPA build to retain page rendering and dead-link validation within the observed non-Windows target; separate blocking Windows build and production-site lanes preserve the emitted-package and shipped-site checks without putting both critical paths in one job. + +Artifacts use two lanes: one metadata lane for `publint`, NodeNext declarations, and compiled invariant loading, plus one built-bin smoke lane. Each lane produces its own build before its consumers. Repeating the short build costs runner minutes but avoids an upload/download dependency and keeps each job's critical path bounded. + +[scripts/publint-all.ts](../../../../scripts/publint-all.ts) calls publint's supported API in-process against an in-memory publication view made from each manifest's declared files and npm's mandatory metadata files. This preserves the distinction between workspace files and published files without spawning a package-manager pack command 103 times. [scripts/verify-built-package-invariants.mjs](../../../../scripts/verify-built-package-invariants.mjs) stages those structurally validated manifest-declared `lib/` files below the real package, then imports the compiled self-reference through plain Node and Cordis Loader normalization. A companion that reaches an undeclared runtime chunk still fails. + +Compatibility lanes run the source worker and Zstandard runtime smokes on every advertised Node line. TypeScript checks the source graph once in a dedicated primary Node 24 lane; repeating the same compiler analysis in runtime compatibility jobs added time without runtime-specific signal. + +The workflow caches the pnpm store, keys each immutable ESLint cache to its owning lint shard, preserves native PowerShell for Windows measurements, and retains one aggregate `all checks passed` status for branch protection. Windows reuses the three exhaustive lint partitions and groups foundation/catalog/prose plus documentation-type/API-contract gates behind shared runner setups; only scheduling differs from the Linux partitions. Windows build and production-site validation remain blocking, while the wider Windows static, lint, and artifact matrix remains observational. ## Alternatives considered -- **Keep the full serial chain in a Node matrix** - simplest to reason about, but it duplicates repo-wide gates that do not produce Node-version-specific signal and leaves every PR waiting for the sum of all gates. -- **Run every gate as a separate GitHub job** - maximizes GitHub-visible fan-out, but it creates too many checks and pays repeated setup/install overhead for gates whose runtime is shorter than the runner preparation. -- **Upload build artifacts to artifact-dependent jobs** - preserves correctness across many jobs, but it adds artifact upload/download time and keeps the workflow wide when the artifact consumers can run behind a local dependency in the primary job. -- **Run `typecheck` and `build` concurrently** - exposes more work to the scheduler, but both commands invoke `tsc -b`; sharing incremental build state between them is a needless race for a small wall-clock gain. -- **Use unbounded real-API e2e parallelism** - rejected because the suite includes many live model/tool scenarios; the worker pool needs an explicit `DSH_E2E_MAX_WORKERS` cap so CI and local runs can fan out without hiding quota or resource problems behind flaky rate-limit failures. +- **Keep the broad lanes** - minimizes workflow YAML, but it preserves the measured multi-minute feedback loop. +- **Run every leaf gate as a separate GitHub job** - maximizes fan-out, but short generators and prose checks would spend more time preparing a runner than checking the repository. +- **Upload one build to artifact consumers** - avoids repeated compilation, but upload/download and dependency scheduling lengthen wall time; the clean build is short enough to repeat inside bounded lanes. +- **Keep package-manager packing in both publication gates** - delegates inventory selection to pnpm, but repeats more than 200 package-manager processes. The manifest structural gate plus publication-view fixtures make the optimized inventory contract explicit and fail on an on-disk but unpublished dependency. +- **Keep build before coverage** - provides emitted output the source suite no longer consumes; a clean-tree coverage proof showed it was pure latency. +- **Typecheck on every Node version** - repeats compiler work while the compatibility smokes already exercise actual Node-specific loading and compression behavior. ## Consequences -PR feedback arrives as a few GitHub checks with structured per-gate log blocks inside each broad job. That keeps runner setup overhead bounded and the Actions UI compact, at the cost of losing one status check per leaf gate. +The shard inventories and matrix jobs described above are not part of the current repository contract. The superseding larger-runner decision keeps the complete primary inventory in one process and uses the serial suite as its independent completeness oracle. -The broad-lane split repeats checkout, setup, and install more often than a single primary job. That setup cost is intentional: on GitHub's hosted runner, running lint, coverage, and snapshot replay in one process pool oversubscribes CPU badly enough that the single-job critical path is longer than the repeated setup. +The optimized publication validators rely on the manifest `files` contract enforced by `verify-package-invariants`. If publication rules grow beyond that contract, the structural gate and both staged views must change together. -The split introduces a maintenance obligation: when `package.json` adds or removes a gate that belongs in CI, [scripts/run-gates.ts](../../../../scripts/run-gates.ts) needs the matching leaf. That obligation is intentional because the runner is the parallel execution plan for the same gate vocabulary, not a separate quality policy. - -The compatibility signal is narrower than the primary Node 24 signal. It proves that the source graph typechecks and that the real unbuilt workflow-worker launch path executes on every advertised runtime line without doubling documentation, coverage, publication, snapshot replay, and unrelated smoke checks whose failures are not expected to vary by Node version. +Compatibility jobs no longer claim that TypeScript itself was exercised under every Node runtime. They prove runtime-sensitive source loading on Node 22, 24, and 26, while the primary runtime owns the single source-graph typecheck. diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml new file mode 100644 index 0000000000..9922018569 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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-serial-cross-platform-ci-reference.md: ffc1fd5b37bc6c9e3427ee55a55300f93a1292f3 +2026-07-21-serial-cross-platform-ci-reference.zh.md: d7f87916865b83973abe6b0708203618cf536c8e diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md new file mode 100644 index 0000000000..ffc1fd5b37 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.md @@ -0,0 +1,39 @@ +# Agent Note: Serial cross-platform CI reference + +Status: implemented + +English | [中文](2026-07-21-serial-cross-platform-ci-reference.zh.md) + +## Problem + +The pull-request workflow reaches its latency targets by scheduling the complete primary Node inventory concurrently inside one larger runner. The optimized scheduler still should not be its own only completeness oracle: a defect in its gate inventory or dependency graph could omit work while the optimized job stays green. + +Encoding the one-minute non-Windows target and three-minute Windows target as job timeouts creates a separate failure mode. Hosted-runner startup and performance vary, so a correct gate can be cancelled at the target boundary before it emits useful diagnostics. The performance objective needs measurement against GitHub timestamps, while correctness needs enough time to finish. + +Reviewers also need a direct answer to a simpler question: what happens when the repository's complete primary Node CI aggregate runs without matrix selection, shard variables, or concurrent gates on each selected hosted operating system? + +## Decision + +[CI](../../../../.github/workflows/ci.yml) gives pull-request and master-push events complementary responsibilities. Pull requests run only the optimized larger-runner and compatibility jobs. A push to `master` skips those jobs and runs three explicit references named `serial / linux`, `serial / macos`, and `serial / windows`. They intentionally duplicate their short checkout, runtime setup, and immutable install sequences instead of hiding the operating systems behind a matrix or reusable workflow. `workflow_dispatch` is reserved for runner benchmarks. + +Each reference job runs `pnpm run check:ci` without any shard selector. `DSH_GATE_CONCURRENCY=1` makes the top-level aggregate execute one ready gate at a time; coverage, snapshot replay, built-bin smoke, and publication validation also receive worker counts of one. The three operating-system jobs may run beside one another, but each host's repository gates are serial and complete. Linux installs bubblewrap before replaying snapshots, and Windows enables Developer Mode before installing the symlinked workspace. + +Master reference jobs are diagnostic and do not participate in the pull request's required `all checks passed` result. A pull request runs only the optimized jobs; a master push runs only the three serial references. The one-minute non-Windows and three-minute Windows objectives are evaluated from completed hosted-job timestamps and reported as measurements; they are not `timeout-minutes` values. + +The portable reference uses GitHub's standard `ubuntu-latest`, `macos-latest`, and `windows-2025` labels. A higher-core hosted runner remains a possible future benchmark, but it is not the default: larger runners require organization-owned labels and provisioning, while a reference oracle should remain runnable without repository-external runner configuration. Provisioning one later can change the performance experiment without changing this correctness baseline. + +## Alternatives considered + +- **Set each timeout equal to its latency target** - rejected because scheduling variance would cancel correct work and suppress the evidence needed to diagnose a regression. +- **Trust only the concurrent primary inventory** - rejected because scheduling and validation share implementation assumptions; a serial aggregate is an independent completeness check. +- **Run the serial references on every pull request** - rejected because they deliberately trade wall time and runner consumption for simplicity and are not needed in the fast feedback loop. +- **Use one operating-system matrix** - rejected because three named jobs make the reference surface visible without another selection mechanism. +- **Run the serial reference on larger runners** - rejected because the reference is the portable fallback for the organization-specific pull-request topology. The fast pull-request path uses provisioned larger runners; the serial master path keeps standard labels. + +## Consequences + +The workflow contains duplicated setup steps and a master reference run can take much longer than the optimized pull-request path. That duplication is deliberate: reviewers can inspect each operating system's complete command without resolving a matrix or concurrent scheduler. + +The reference may expose platform failures that the optimized blocking set does not yet claim to support, especially on Windows. Such a failure is evidence about current cross-platform behavior rather than a reason to weaken or silently skip the aggregate. + +Removing strict duration timeouts means a latency regression is observed rather than automatically cancelled. Hosted measurements must therefore accompany performance changes, while the completed logs retain the information needed to optimize the slow lane. diff --git a/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md new file mode 100644 index 0000000000..d7f8791686 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-21-serial-cross-platform-ci-reference.zh.md @@ -0,0 +1,39 @@ +# Agent Note: 跨平台串行 CI 参考流程 + +Status: implemented + +[English](2026-07-21-serial-cross-platform-ci-reference.md) | 中文 + +## 问题 + +拉取请求工作流通过在一台更大型运行器内并发调度完整的主 Node 门禁清单来达到延迟目标。优化调度器仍不应成为自身唯一的完整性判定基准:如果其门禁清单或依赖图存在缺陷,即使优化作业保持绿灯,也可能漏掉部分工作。 + +将非 Windows 作业的 1 分钟目标和 Windows 作业的 3 分钟目标写成作业超时,会引入另一种失败模式。托管运行器的启动时间和性能会波动,因此即使门禁本身正确,也可能在到达目标时间边界时被取消,来不及输出有用的诊断信息。性能目标需要根据 GitHub 时间戳衡量,而正确性验证需要给门禁留足完成时间。 + +评审人还需要直接回答一个更简单的问题:在每个选定的托管操作系统上,如果仓库完整的主 Node CI 聚合流程不使用矩阵选择、分片变量或并发门禁,运行结果会怎样? + +## 决策 + +[CI](../../../../.github/workflows/ci.yml) 为拉取请求事件与 master 推送事件赋予互补的职责。拉取请求只运行使用更大型运行器的优化作业和兼容性作业。向 `master` 推送时会跳过这些作业,改为运行三个显式参考作业,名称分别为 `serial / linux`、`serial / macos` 和 `serial / windows`。这些作业有意分别重复简短的代码检出、运行时设置和依赖锁定的安装步骤,不用矩阵或可复用工作流把操作系统差异隐藏起来。`workflow_dispatch` 仅用于运行器基准测试。 + +每个参考作业均在不设置任何分片选择器的情况下运行 `pnpm run check:ci`。`DSH_GATE_CONCURRENCY=1` 使顶层聚合每次只执行一个已经就绪的门禁;覆盖率、快照回放、built-bin 冒烟测试和发布验证的并发数也设为 1。三种操作系统的作业可以彼此并行,但每台主机上的仓库门禁都串行运行且完整执行。Linux 在回放快照前安装 bubblewrap,Windows 则在安装采用符号链接的工作区前启用开发人员模式。 + +master 分支的参考作业仅用于诊断,不参与拉取请求所要求的 `all checks passed` 结果。拉取请求只运行优化作业;向 master 推送时只运行三个串行参考作业。系统根据已完成托管作业的时间戳评估非 Windows 作业的 1 分钟目标和 Windows 作业的 3 分钟目标,并将其报告为测量结果,而不是写成 `timeout-minutes` 值。 + +可移植的参考流程使用 GitHub 标准的 `ubuntu-latest`、`macos-latest` 和 `windows-2025` 标签。仍可将更高核心数的托管运行器作为未来的基准测试,但不将其设为默认选择:更大型运行器需要组织自有的标签和预配,而参考判定基准应无需仓库外部的运行器配置即可运行。日后完成这类预配,可以改变性能实验而无需改变该正确性基线。 + +## 曾考虑的替代方案 + +- **将每个超时值设为相应延迟目标**:不予采纳,因为调度波动会中止原本正确的执行,并使诊断回归所需的证据无法产生。 +- **仅信任并发执行的主门禁清单**:不予采纳,因为调度逻辑与校验逻辑共享实现假设;串行聚合流程是一项独立的完整性检查。 +- **在每个拉取请求上运行串行参考作业**:不予采纳,因为这些作业有意以更长的总耗时和更多运行器用量换取简单性,快速反馈循环不需要它们。 +- **使用一个操作系统矩阵**:不予采纳,因为三个具名作业无需另一套选择机制,就能让参考流程的构成清晰可见。 +- **在更大型运行器上运行串行参考流程**:不予采纳,因为该参考流程是特定组织拉取请求拓扑的可移植后备方案。快速拉取请求路径使用已预配的更大型运行器;串行 master 路径保留标准标签。 + +## 后果 + +工作流包含重复的设置步骤,master 参考运行也可能比优化后的拉取请求路径耗时长得多。这些重复是有意保留的:评审人无需解析矩阵或并发调度器,就能直接检查每种操作系统执行的完整命令。 + +参考流程可能暴露某些平台上的故障,而优化后的阻塞门禁集合尚未声明支持这些平台,Windows 尤其如此。这类失败反映了当前的跨平台行为,不应成为削弱或静默跳过该聚合流程的理由。 + +移除严格的时长超时后,系统会观测到延迟回归,而不是在发生回归时自动取消运行。因此,性能改动必须附带托管环境测量结果,已完成的日志则保留优化最慢通道所需的信息。 diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml new file mode 100644 index 0000000000..6277617a73 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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-evidence-based-larger-hosted-runners.md: c0fae2841f21c431d6416cd5d421929d70197abb +2026-07-22-evidence-based-larger-hosted-runners.zh.md: 51c73a8a631af4f1254c795d09585770fc4e68bb diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md new file mode 100644 index 0000000000..c0fae2841f --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md @@ -0,0 +1,75 @@ +# Agent Note: Evidence-based larger hosted runners + +Status: implemented + +English | [中文](2026-07-22-evidence-based-larger-hosted-runners.zh.md) + +## Problem + +The shard-heavy CI topology met its latency targets by spreading primary Node work across 40 Linux jobs and Windows work across nine jobs. Most gates were shorter than checkout, runner setup, cache restore, and dependency installation, so repeated setup waves created both cost and latency variance. One hosted run finished its slowest Linux job in 49 seconds yet took 231 seconds for a Windows lint shard whose checkout, cache restore, and install alone consumed 158 seconds. + +Larger runners make it possible to pay setup once and parallelize inside the repository scheduler, but the useful size cannot be selected from core counts alone. Critical-lane benchmarks did not scale monotonically, and a whole-repository aggregate exposed different bottlenecks from isolated typecheck or site builds. + +## Decision + +The organization keeps twelve x64 larger-runner pools in the repo-restricted `dsh-larger-ci` group: Ubuntu 24.04 and Windows 2025 at 4, 8, 16, 32, 64, and 96 cores. Public IPs are disabled. Each pool has an autoscaling ceiling of 256; the ceiling does not allocate idle machines or remove the need to bound workflow demand. + +Production CI uses five larger-runner executions and one standard-runner aggregator. The primary Node inventory is not sharded: + +- `node 24 / complete` uses one 96-core Linux runner. One checkout, direct selection of the image's preinstalled Node 24 toolcache, pnpm- and ESLint-cache restore, and install feeds all 42 primary gates. `run-gates` starts up to 10 independent gates; ESLint and coverage use at most 16 workers, and snapshot replay uses at most 8. Build starts as soon as the first short gates release scheduler slots, while snapshot replay and publication consumers retain explicit dependencies on emitted `lib/` output. Pull requests restore both caches without saving them, so cache compression and upload do not extend the required job; the master serial reference refreshes those caches outside the pull-request critical path. An uncached exact-head trace put ESLint at 38.11 seconds and coverage at 37.10 seconds, so the small ESLint restore remains useful on the critical path. The read-only job does not persist checkout credentials. +- Node 22.19 and Node 26 use the 4- and 32-core Linux pools for their runtime compatibility smokes. Python 3.10 uses the 8-core Linux pool for the complete keyless SDK suite. These are environment contracts, not slices of the primary Node gate inventory. +- `windows node 24 / complete` uses one 32-core Windows runner. One preparation wave feeds the required package build, required production site build, and complete observational portability inventory. Required failures fail the job; observational failures are reported as non-blocking. ESLint stays single-threaded because 16 ESLint workers took 174.54 seconds, coverage uses at most 12 workers, and the outer scheduler retains 16 slots. The job restores only the small master-refreshed ESLint cache and performs a clean pnpm install instead of restoring or saving the many-file package store. All six Windows larger-runner sizes completed install and the production-site benchmark without mutating the machine-wide Developer Mode registry key, so the pull-request critical path omits that redundant step. + +The former gate-level and coarse primary shard jobs are absent from the workflow. Their static, lint, coverage, snapshot, and scenario shard selectors are also absent from the repository, so an unused diagnostic path cannot preserve a second CI architecture. + +An [exact-head all-size benchmark](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29908491351) ran the complete unsharded primary Node aggregate on every Linux pool before the eager-build correction: + +| Complete Linux primary | 4 cores | 8 cores | 16 cores | 32 cores | 64 cores | 96 cores | +|---|---:|---:|---:|---:|---:|---:| +| Active time | 243 s | 144 s | 103 s | 87 s | 62 s | 65 s | + +The 96-core trace spent 39.14 seconds in repository gates. Typecheck occupied 25.71 seconds, then a scheduler dependency delayed the 2.13-second build and 11.29-second snapshot replay until it finished. The same run already proved build and typecheck independently, and the former CPU lane ran them concurrently. Removing that dependency makes lint at 33.30 seconds the measured critical gate while preserving dependencies only for consumers of build output. The 64-core trace exposed the same idle chain: typecheck, build, and snapshot consumed 44.85 seconds in sequence while its independent lint and documentation builds finished in 36.83 and 36.15 seconds. More cores therefore become useful only after the repository scheduler can feed them. + +The same benchmark measured the required Windows build surfaces across every provisioned size: + +| Windows blocking builds | 4 cores | 8 cores | 16 cores | 32 cores | 64 cores | 96 cores | +|---|---:|---:|---:|---:|---:|---:| +| Active time | 152 s | 104 s | 104 s | 92 s | 103 s | 110 s | + +Repository work gains little above 16 Windows cores, but the 32-core pool can start the complete outer inventory together. A [retargeted production validation](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29907581119/attempts/2) completed the full one-box Windows inventory in 173 seconds, including coverage and snapshot replay, so Windows remains consolidated. + +The larger client package graph makes cache mechanics and scheduler pressure part of the measured workload. In [one exact-head production run](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29912577681), Linux spent 39 seconds in repository gates but 69 seconds in the complete job, while Windows spent 117 seconds in repository gates and 228 seconds in the complete job. The Windows pnpm cache downloaded its 154 MB archive in about two seconds but spent 27 seconds extracting it, followed by a 23-second install and a 14-second post-job save. A [cacheless all-size trace](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29913033155) completed the same 32-core Windows install in 27 seconds. Production therefore avoids the Windows package-store cache, uses restore-only caches on latency-critical pull-request jobs, and bounds outer concurrency so typecheck, lint, coverage, and build do not oversubscribe one host. + +Three host effects remain part of the decision. A standard Node 26 job once spent 36 of its 67 seconds in `Set up job`, which is why environment contracts use distinct larger-runner pools instead of standard capacity. The setup-node action later spent 3.68 seconds printing cached Linux environment details and 46.56 seconds doing the same on Windows after both had already found Node 24.18.0 in the hosted toolcache. The two latency-critical jobs select the newest preinstalled 24.x directory directly, verify its major, and fail loud if the image no longer carries it; compatibility jobs retain setup-node because selecting a non-default runtime is their contract. A Linux candidate also spent 18 seconds registering a 50 KB Bubblewrap package because the hosted image scanned 202,507 package-database files. [`scripts/prepare-ci-bubblewrap.sh`](../../../../scripts/prepare-ci-bubblewrap.sh) instead verifies and extracts the pinned payload into the ephemeral runner directory, runs a functional confinement probe, and overlaps that preparation with dependency installation. + +Inner and outer worker limits are separate controls. An [exact-head 32-worker ESLint experiment](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29918329463) slowed lint to 52.28 seconds and coverage to 42.71 seconds, where an adapter idle-timeout test failed. A later 8-gate trace reduced coverage to 35.17 seconds but delayed the production-site build until the aggregate reached 41.06 seconds. Production therefore retains 16 ESLint workers and admits 10 independent repository gates at once, leaving capacity for the worker pools owned by those gates without starving later independent work. + +Linux coverage caps each project at 16 workers, while Windows keeps the 12-worker cap. The process-bound project contains exactly five suite files, so its fork count cannot reach either cap. Thirty-two forks crashed Node 24's CJS lexer twice, and a later 16-fork run reproduced the worker loss and invalid coverage result. The single Vitest invocation therefore uses threads for the broad inventory and reserves forks for suites that exercise process-global state, `process` APIs, or timing-sensitive process I/O. That narrow fork inventory includes the local bash process-plumbing suite: under aggregate gate contention its thread worker completed every test but intermittently missed the stdin-error callback needed for per-file function coverage. It also includes the pi-ai adapter suite after two hosted aggregate runs delayed an idle-watchdog socket-close observation past its 100-millisecond test deadline. A 32-worker all-gate run on the 96-core host slowed coverage to 44.6 seconds and made a compute-budget regression cross its one-second threshold, so production stops at 16. This preserves the suites' isolation contracts and deterministic coverage while avoiding forked execution for ordinary test files. + +The workflow retains two manual measurement suites. `suite=larger-runner-benchmark` compares isolated critical lanes across every size, and `suite=consolidated-runner-benchmark` compares whole aggregates. Complete serial Linux, macOS, and Windows references run only when `master` moves; pull requests run only the optimized jobs. + +## Alternatives considered + +**Keep the three coarse primary Linux lanes.** The core, CPU, and production-site jobs met the latency targets, but they paid three setup waves and left primary Node work sharded after larger runners were available. The all-size trace showed that one unnecessary dependency, not a lack of host capacity, kept the single-box aggregate above one minute. + +**Keep the former gate-level shard topology as a manual reference.** A dormant second topology kept hundreds of workflow lines, selector modules, and scenario-partition behavior alive. The all-size and serial suites provide timing and completeness controls without preserving production code that no required job exercises. + +**Use the 64-core pool for the complete primary aggregate.** Its sampled active time was three seconds lower than the 96-core result because hosted setup was nine seconds faster, but its repository gates were 5.72 seconds slower. Production uses 96 cores for the shorter controllable critical path; the benchmark suite retains both pools so a sustained image or pricing change can reverse that choice with evidence. + +**Keep build behind typecheck.** This orders independent compiler invocations and turns snapshot replay into a three-stage critical chain. Build output has its own success dependency, so only snapshot and publication consumers wait for it. + +**Keep compatibility and Python on standard runners.** Warm standard runs can fit, but runner setup alone has crossed the non-Windows target. Distinct larger pools isolate these environment contracts from that allocation lottery. + +**Keep required and observational Windows checks in separate jobs.** The split preserves status semantics at the workflow level but pays setup twice. `run-gates` preserves the same required versus non-blocking distinction inside one process. + +**Install Bubblewrap through the system package manager.** This uses the host's package database and can dominate the job even when the payload is tiny. Pinned extraction plus a confinement probe preserves the runtime contract without mutating the hosted image. + +## Consequences + +Primary Node CI has one job, one setup wave, one complete gate inventory, and no shard selectors. Together with two Node compatibility executions, Python, and Windows, production has five paid larger-runner executions instead of seven coarse-lane executions or 49 gate-level executions. + +GitHub rounds each larger-runner execution up to a whole minute, so eliminating setup waves reduces billed time as well as workflow complexity. The final aggregator remains on a standard runner because it begins only after the paid jobs release capacity. + +The current targets are observed performance contracts, not cancellation deadlines. Exact-head production runs must show every non-Windows job below one minute and the consolidated Windows job below three minutes; manual all-size and serial suites remain available when image, dependency, scheduler, or pricing changes need remeasurement. + +Production CI depends on the organization-owned runner labels in [`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml). Missing or renamed pools leave jobs queued instead of falling back to standard capacity. All twelve pools remain provisioned so the manual benchmarks can re-evaluate the production size without an administrative setup cycle. diff --git a/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md new file mode 100644 index 0000000000..51c73a8a63 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.zh.md @@ -0,0 +1,75 @@ +# Agent Note: 基于实证选用 GitHub 托管大型运行器 + +Status: implemented + +[English](2026-07-22-evidence-based-larger-hosted-runners.md) | 中文 + +## 问题 + +高度分片的 CI 拓扑通过把主 Node 工作分散到 40 个 Linux 作业、把 Windows 工作分散到 9 个作业来达到延迟目标。大多数门禁本身的耗时短于代码检出、运行器设置、缓存恢复和依赖安装这些准备阶段,因此反复执行多轮设置既增加成本,也带来延迟波动。一次托管运行中最慢的 Linux 作业用时 49 秒,而一个 Windows lint 分片却耗时 231 秒,其中仅代码检出、缓存恢复和安装就占了 158 秒。 + +大型运行器可以让 CI 只承担一次设置开销,再由仓库调度器在内部并行执行,但无法仅凭核心数选出有实际价值的规格。关键通道基准测试的性能提升不呈单调变化,完整仓库聚合流程暴露出的瓶颈也不同于单独运行类型检查或网站构建时的瓶颈。 + +## 决策 + +组织在仅限本仓库使用的 `dsh-larger-ci` 运行器组中保留 12 个 x64 大型运行器池:Ubuntu 24.04 和 Windows 2025 各设 4、8、16、32、64、96 核规格。公网 IP 已禁用。每个池的自动扩缩容上限为 256;该上限既不会分配闲置机器,也不能免除限制工作流需求的必要性。 + +生产 CI 包含 5 次大型运行器执行和 1 个标准运行器聚合作业。主 Node 门禁清单不再分片: + +- `node 24 / complete` 使用一台 96 核 Linux 运行器。只需执行一次代码检出、直接选择托管映像中预装的 Node 24 toolcache、恢复 pnpm 和 ESLint 缓存以及安装,即可供全部 42 项主门禁使用。`run-gates` 最多同时启动 10 项相互独立的门禁;ESLint 和覆盖率最多使用 16 个工作线程,快照回放最多使用 8 个。第一批短门禁释放调度器槽位后,构建会立即启动,而快照回放和发布消费方仍显式依赖生成的 `lib/` 输出。拉取请求会恢复这两项缓存但不保存,因此缓存压缩和上传不会延长必需作业;master 上的串行参考会在拉取请求关键路径之外刷新这两项缓存。一次未使用缓存的分支头精确运行轨迹显示,ESLint 耗时 38.11 秒,覆盖率耗时 37.10 秒,因此在关键路径上恢复这个较小的 ESLint 缓存仍有价值。该只读作业不会持久化代码检出凭据。 +- Node 22.19 和 Node 26 分别使用 4 核和 32 核 Linux 池运行各自的运行时兼容性冒烟测试。Python 3.10 使用 8 核 Linux 池运行完整的无密钥 SDK 套件。这些作业属于环境契约,并非主 Node 门禁清单的分片。 +- `windows node 24 / complete` 使用一台 32 核 Windows 运行器。一轮准备工作供必需的包构建、必需的生产网站构建以及完整的观测性可移植性清单共用。任何必需项失败都会使作业失败;观测项失败则报告为非阻塞。ESLint 保持单线程,因为 16 个 ESLint 工作线程耗时 174.54 秒;覆盖率最多使用 12 个工作线程,外层调度器则保留 16 个槽位。该作业仅恢复由 master 刷新的较小 ESLint 缓存,并在干净环境中执行 pnpm 安装,而不恢复或保存包含大量文件的包存储。全部 6 种 Windows 大型运行器规格都在未修改系统级 Developer Mode 注册表项的情况下完成了安装和生产网站基准测试,因此拉取请求关键路径省略了这个多余步骤。 + +原有的门禁级和粗粒度主流程分片作业已从工作流中移除。相应的静态、lint、覆盖率、快照和场景分片选择器也已从仓库中移除,因此未使用的诊断路径无法继续维系第二套 CI 架构。 + +一次[分支头精确的全规格基准测试](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29908491351)在修正构建尽早启动逻辑前,对每种 Linux 池都运行了完整且未分片的主 Node 聚合流程: + +| Linux 完整主流程 | 4 核 | 8 核 | 16 核 | 32 核 | 64 核 | 96 核 | +|---|---:|---:|---:|---:|---:|---:| +| 活动耗时 | 243 秒 | 144 秒 | 103 秒 | 87 秒 | 62 秒 | 65 秒 | + +96 核运行轨迹中的仓库门禁耗时 39.14 秒。类型检查占用 25.71 秒,随后一项调度器依赖使耗时 2.13 秒的构建和耗时 11.29 秒的快照回放都要等到类型检查结束后才启动。同一次运行已经分别证明构建和类型检查可以独立执行,原 CPU 通道也曾让二者并发运行。移除这项依赖后,耗时 33.30 秒的 lint 成为实测关键门禁,而只有构建输出的消费方仍保留依赖关系。64 核运行轨迹暴露了相同的空闲链:类型检查、构建和快照依次执行,共耗时 44.85 秒,而相互独立的 lint 和文档构建分别在 36.83 秒和 36.15 秒内完成。因此,只有仓库调度器能够为更多核心持续提供工作时,增加核心数才有价值。 + +同一项基准测试还测量了每种已预配规格上的 Windows 必需构建项: + +| Windows 阻塞性构建 | 4 核 | 8 核 | 16 核 | 32 核 | 64 核 | 96 核 | +|---|---:|---:|---:|---:|---:|---:| +| 活动耗时 | 152 秒 | 104 秒 | 104 秒 | 92 秒 | 103 秒 | 110 秒 | + +Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完整的外层清单同时启动。一次[重新定向的生产验证](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29907581119/attempts/2)在 173 秒内完成了单机 Windows 完整清单,其中包括覆盖率和快照回放,因此 Windows 继续采用合并执行方式。 + +客户端包依赖图增大后,缓存机制和调度器压力也成为实测工作负载的一部分。在[一次分支头精确的生产运行](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29912577681)中,Linux 的仓库门禁耗时 39 秒,完整作业耗时 69 秒;Windows 的仓库门禁耗时 117 秒,完整作业耗时 228 秒。Windows pnpm 缓存的 154 MB 归档下载耗时约 2 秒,但解压耗时 27 秒,随后安装耗时 23 秒,作业结束后的保存又耗时 14 秒。一次[无缓存的全规格运行轨迹](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29913033155)在 27 秒内完成了同一台 32 核 Windows 运行器上的安装。因此,生产环境不使用 Windows 包存储缓存,在对延迟敏感的拉取请求作业中使用只恢复不保存的缓存,并限制外层并发度,以免类型检查、lint、覆盖率和构建在同一台主机上过度争用资源。 + +3 项主机效应仍构成这项决策的依据。一个标准 Node 26 作业曾在总共 67 秒的耗时中,把 36 秒用在 `Set up job` 上,因此各项环境契约使用不同的大型运行器池,而非标准容量。setup-node action 在 Linux 和 Windows 均已从托管 toolcache 找到 Node 24.18.0 后,仍分别花费 3.68 秒和 46.56 秒输出缓存的环境详情。两个延迟关键作业会直接选择最新的预装 24.x 目录并验证其主版本号;如果映像不再提供该目录,作业会明确报错并失败。兼容性作业仍使用 setup-node,因为选择非默认运行时正是它们的契约。一个 Linux 候选作业还在注册 50 KB 的 Bubblewrap 包时耗时 18 秒,因为托管映像扫描了 202,507 个包数据库文件。[`scripts/prepare-ci-bubblewrap.sh`](../../../../scripts/prepare-ci-bubblewrap.sh) 改为验证固定包内容并将其解压到临时运行器目录,执行功能性隔离探针,并让这项准备工作与依赖安装重叠执行。 + +内层与外层工作线程上限是相互独立的控制机制。一次[分支头精确、使用 32 个工作线程的 ESLint 实验](https://github.com/deepseek-harness/deepseek-harness/actions/runs/29918329463)使 lint 耗时增至 52.28 秒、覆盖率耗时增至 42.71 秒;同一次运行中,一项适配器空闲超时测试失败。后来一次同时运行 8 项门禁的运行轨迹将覆盖率耗时降至 35.17 秒,但生产网站构建被延后,直到聚合流程耗时达到 41.06 秒时才完成。因此,生产环境将 ESLint 工作线程上限维持在 16 个,并且同时最多运行 10 项相互独立的仓库门禁,既为这些门禁自身的工作线程池留出容量,又避免后续独立工作因资源不足而迟迟无法启动。 + +Linux 覆盖率把每个项目的工作线程上限设为 16 个,Windows 则保留 12 个工作线程的上限。进程约束项目恰好包含 5 个套件文件,因此它的 fork 数量不可能达到任一上限。32 个 fork 曾两次导致 Node 24 的 CJS 词法分析器崩溃,后来一次使用 16 个 fork 的运行又复现了工作进程丢失和无效的覆盖率结果。因此,单次 Vitest 调用会对大范围测试清单使用线程,只为涉及进程全局状态、`process` API 或对时间敏感的进程 I/O 的套件保留 fork。这份有限的 fork 清单还包含本地 bash 进程通路套件:在聚合门禁争用资源时,该套件的工作线程虽然完成了所有测试,却会间歇性漏记逐文件函数覆盖率所需的 stdin 错误回调。两次托管聚合运行都将空闲看门狗对套接字关闭的观测延迟到超过其 100 毫秒测试截止时间,因此这份清单还包含 pi-ai 适配器套件。在 96 核主机上使用 32 个工作线程运行全部门禁时,覆盖率耗时变慢至 44.6 秒,还使一项计算预算回归超过其 1 秒阈值,因此生产环境将工作线程数限制在 16 个以内。这样既能保留这些套件的隔离契约和覆盖率结果的确定性,又能避免以 fork 方式执行普通测试文件。 + +工作流保留 2 项手动测量套件。`suite=larger-runner-benchmark` 比较所有规格下相互独立的关键通道,`suite=consolidated-runner-benchmark` 比较完整聚合流程。只有在 `master` 移动时,才运行完整的 Linux、macOS 和 Windows 串行参考;拉取请求只运行优化后的作业。 + +## 曾考虑的替代方案 + +**保留 3 个粗粒度 Linux 主流程通道。** 核心、CPU 和生产网站作业均达到延迟目标,但它们需要 3 轮设置,而且在大型运行器已经可用后仍对主 Node 工作进行分片。全规格运行轨迹表明,让单机聚合流程超过 1 分钟的是一项不必要的依赖,而非主机容量不足。 + +**将原有的门禁级分片拓扑保留为手动参考。** 一套闲置的第二拓扑会让数百行工作流、选择器模块和场景分区行为继续存活。全规格和串行套件无需保留任何必需作业都不执行的生产代码,也能提供计时与完整性对照。 + +**使用 64 核池运行完整主聚合流程。** 由于托管设置快了 9 秒,其采样活动耗时比 96 核结果少 3 秒,但仓库门禁慢了 5.72 秒。生产环境使用 96 核来缩短可控的关键路径;基准测试套件保留两种规格,因此如果映像或定价发生持续性变化,仍可根据证据反转这项选择。 + +**让构建继续等待类型检查。** 此方案会给相互独立的编译器调用排定先后顺序,并把快照回放变成 3 阶段关键链。构建输出本身有独立的成功依赖关系,因此只有快照和发布消费方需要等待它。 + +**让兼容性和 Python 继续使用标准运行器。** 标准运行器热运行可以达到目标,但仅运行器设置一项就曾超过非 Windows 目标。不同的大型运行器池可以让这些环境契约免受这种分配波动影响。 + +**将必需的 Windows 检查和观测性 Windows 检查保留在不同作业中。** 这种拆分在工作流层保留状态语义,却需要支付两次设置开销。`run-gates` 在一个进程内保留了相同的必需与非阻塞区别。 + +**通过系统包管理器安装 Bubblewrap。** 此方案会使用主机的包数据库,即使包内容很小,也可能主导整个作业耗时。固定版本的解压方式配合隔离探针,无需修改托管映像即可保留运行时契约。 + +## 后果 + +主 Node CI 只有 1 个作业、1 轮设置、1 份完整门禁清单,而且没有分片选择器。加上 2 次 Node 兼容性执行、Python 和 Windows,生产环境共有 5 次付费大型运行器执行,而非 7 次粗粒度通道执行或 49 次门禁级执行。 + +GitHub 会把每次大型运行器执行向上取整到整分钟计费,因此消除设置轮次既能减少计费时长,也能降低工作流复杂度。最终聚合作业仍使用标准运行器,因为它只会在付费作业释放容量后启动。 + +当前目标是基于观测得到的性能契约,而非取消截止时间。分支头精确的生产运行必须表明每个非 Windows 作业都低于 1 分钟,合并后的 Windows 作业低于 3 分钟;当映像、依赖、调度器或定价发生变化而需要重新测量时,仍可使用手动全规格和串行套件。 + +生产 CI 依赖 [`.github/workflows/ci.yml`](../../../../.github/workflows/ci.yml) 中由组织持有的运行器标签。池缺失或改名会让作业一直排队,不会回退到标准容量。全部 12 个池均保持已预配状态,因此手动基准测试无需再次经过管理配置周期,就能重新评估生产规格。 diff --git a/.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.i18n.yaml new file mode 100644 index 0000000000..bf64296c60 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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-plan-specific-collaboration-state.md: 8a7caf9b1150cb6d3ea2c8ed52e42751f30c773c +2026-07-22-plan-specific-collaboration-state.zh.md: c4d2528cc06a74ce8c152199bc2503daff315dbf diff --git a/.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md b/.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md new file mode 100644 index 0000000000..8a7caf9b11 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md @@ -0,0 +1,47 @@ +# Agent Note: Collapse named session modes into plan mode + +Status: implemented + +English | [中文](2026-07-22-plan-specific-collaboration-state.zh.md) + +## Problem + +The first plan-mode implementation introduced a generic named-mode registry even though the product shipped only `plan`. `ModeConfig.modes`, definition-name validation, `ctx.modes.list()`, retired-definition fallback, and a synthetic `review` mode in tests existed only to support hypothetical future collaboration modes. The production-specific behavior—plan guidance, `/plan`, and `exit_plan_mode`—still lived in the same package, so the generic API did not isolate a reusable mechanism from plan policy. + +The word “mode” also spans unrelated domains. Sandbox mode is an enforcing policy owned by `ctx.sandboxPolicy` and logged as `sandbox/mode`; plan mode is a collaboration stance that contributes guidance and a reviewed exit. Treating both as instances of one named-mode abstraction would obscure their independent ownership. ACP's protocol happens to expose a generic mode picker, but that is an adapter vocabulary rather than evidence that the harness needs a generic mode domain. + +## Decision + +Plan mode owns a plan-specific product package: `@deepseek-ai/dsh-plan-mode` at `packages/plan/plan-mode/`. The durable fact is `plan/mode: { active: boolean }`, folded by `foldPlanMode(events)` with `false` as the empty-log value. `ctx.planMode.get(agent)` returns `{ active, pending? }`, and `set(agent, active)` records the boundary-applied selection. The existing prompt-submit, continuation, retry, append-failure, and disposal fences remain unchanged in meaning. + +Configuration is exactly `{ section: string }`. The package registers the fixed `plan:policy` section, `/plan [message]`, and `exit_plan_mode` itself. Bare `/plan` selects the state; a non-empty argument selects it first and then sends the trimmed text through `agent.steer()`, making the text an ordinary logged user message in the affected step. The exit tool remains registered while plan mode is inactive so the request tool catalog stays stable. + +ACP keeps its protocol-level `default` and `plan` ids. The bridge maps those two ids to the boolean service, advertises only that fixed pair, rejects every other id at the adapter boundary, and maps committed `plan/mode` events back to `current_mode_update`. The protocol remains generic without forcing genericity into the product domain. + +Sandbox mode and approval policy remain separate enforcement axes. Plan mode neither reads nor writes them, and the simplification introduces no shared base type, registry, or preset abstraction across those concepts. + +## Deleted surface + +- The arbitrary definition map, mode-name regular expression, reserved-name rules, and per-definition command loop. +- `ModeDefinition`, the resolved definition map, `ctx.modes.list()`, string-valued get/set state, and unknown or retired mode handling. +- Test-only `review` mode cases and claims that additional modes can be added through configuration. +- Generic `mode/set` and `mode:policy` names; the plan package now owns `plan/mode` and `plan:policy`. + +## Alternatives considered + +**Keep a private generic registry and expose only plan today.** Rejected because the unused name/config machinery would still be maintained and tested without a second production consumer. A future collaboration state can establish the right shared seam from two concrete cases. + +**Fold sandbox mode into the same service.** Rejected because collaboration guidance and execution confinement have different owners, lifecycle semantics, and consumers. Their shared English noun is not a domain relationship. + +**Let ACP own plan state.** Rejected because TUI, resume, fork, prompt assembly, and the exit tool need the same logged fact independently of ACP. ACP owns only the wire projection. + +## Verification + +- Package tests retain boundary ordering, retry, append-failure, HMR disposal, prompt assembly, stable native and Code Mode schemas, review outcomes, and invariant coverage through the boolean service. +- Command tests cover bare `/plan`, `/plan `, absence of `/mode` and `/review`, and effect-scoped removal. +- ACP tests cover fixed advertisement, both ids, unknown-id rejection, optimistic updates, committed exits, and load replay. +- The keyless TUI scenario enters through `/plan ` and proves `plan/mode` precedes the first request header and that the message is logged under plan guidance. + +## Consequences + +The implementation has one vocabulary for one shipped feature. Adding another collaboration stance is now an explicit design decision instead of a config entry, while ACP clients continue to see their standard mode picker. The migration intentionally rejects old `mode/set` logs and old `modes.plan.section` configuration under the repository's pre-release format policy. diff --git a/.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.zh.md b/.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.zh.md new file mode 100644 index 0000000000..c4d2528cc0 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.zh.md @@ -0,0 +1,47 @@ +# Agent Note: 将具名会话模式收敛为 plan mode + +Status: implemented + +[English](2026-07-22-plan-specific-collaboration-state.md) | 中文 + +## 问题 + +产品只交付了 `plan`,首个 plan mode 实现却引入了通用的具名模式注册表。`ModeConfig.modes`、定义名称校验、`ctx.modes.list()`、已退役定义的回退逻辑,以及测试中合成的 `review` 模式,都只为支持假想中的未来协作模式而存在。plan 引导、`/plan` 和 `exit_plan_mode` 这些生产专用行为仍位于同一个包(package)内,因此通用 API 并未将可复用机制与 plan 策略隔离开来。 + +「mode」一词还横跨互不相关的领域。沙箱模式是由 `ctx.sandboxPolicy` 拥有、以 `sandbox/mode` 记录日志的强制执行策略;plan mode 则是一种协作方式,会贡献引导内容和经评审的退出路径。若把两者都视为同一个具名模式抽象的实例,就会掩盖二者各自独立的归属关系。ACP(Agent Client Protocol)协议恰好暴露了通用模式选择器,但这只是适配器词汇,并不能证明 harness 需要通用模式领域。 + +## 决策 + +Plan mode 拥有一个 plan 专用产品包:位于 `packages/plan/plan-mode/` 的 `@deepseek-ai/dsh-plan-mode`。持久化事实为 `plan/mode: { active: boolean }`,由 `foldPlanMode(events)` 折叠,空日志值为 `false`。`ctx.planMode.get(agent)` 返回 `{ active, pending? }`,`set(agent, active)` 则记录在边界生效的选择。现有的提示词提交、continuation、重试、追加失败和 dispose(资源释放)栅栏在语义上保持不变。 + +配置严格为 `{ section: string }`。该包自行注册固定的 `plan:policy` 段、`/plan [message]` 和 `exit_plan_mode`。不带参数的 `/plan` 选择该状态;非空参数则先选择该状态,再通过 `agent.steer()` 发送去除首尾空白后的文本,使该文本在受影响的步骤中成为一条记录到日志的普通用户消息。即使 plan mode 未激活,退出工具仍保持注册,以确保请求工具目录稳定。 + +ACP 保留协议层的 `default` 和 `plan` id。桥接层把这两个 id 映射到布尔服务,只公布这组固定选项,在适配器边界拒绝其他所有 id,并把已提交的 `plan/mode` 事件映射回 `current_mode_update`。协议仍保持通用性,但不会迫使产品领域也采用通用抽象。 + +沙箱模式与审批策略仍是彼此独立的强制约束轴。Plan mode 既不读取也不写入二者;此次简化也没有为这些概念引入共享基类型、注册表或预设抽象。 + +## 删除的接口 + +- 任意定义映射、模式名正则表达式、保留名称规则以及逐定义命令循环。 +- `ModeDefinition`、解析后的定义映射、`ctx.modes.list()`、字符串值的 get/set 状态,以及未知或已退役模式处理。 +- 仅用于测试的 `review` 模式用例,以及可通过配置添加其他模式的表述。 +- 通用的 `mode/set` 与 `mode:policy` 名称;plan 包拥有 `plan/mode` 与 `plan:policy`。 + +## 考虑过的替代方案 + +**保留私有的通用注册表,目前只暴露 plan。** 不予采纳,因为没有第二个生产消费方时,仍需维护和测试未使用的名称与配置机制。未来若出现另一种协作状态,可以从两个具体案例出发建立合适的共享 seam。 + +**将沙箱模式折叠进同一服务。** 不予采纳,因为协作引导与执行约束有不同的归属方、生命周期语义和消费方。二者的英文名称都含「mode」,不代表存在领域关系。 + +**让 ACP 拥有 plan 状态。** 不予采纳,因为 TUI、恢复、fork、提示词组装和退出工具都需要在 ACP 之外独立使用同一项已记录事实。ACP 只拥有协议投影。 + +## 验证 + +- 包测试通过布尔服务继续覆盖边界顺序、重试、追加失败、HMR(热模块替换)资源释放、提示词组装、稳定的原生 schema 与 Code Mode schema、评审结果和不变式。 +- 命令测试覆盖不带参数的 `/plan`、`/plan `、不存在 `/mode` 和 `/review`,以及随 effect 作用域移除。 +- ACP 测试覆盖固定模式列表公布、两个 id、未知 id 拒绝、乐观更新、已提交退出和加载回放。 +- 无密钥 TUI 场景通过 `/plan ` 进入,证明 `plan/mode` 先于首个请求头,且消息在 plan 引导下记录到日志。 + +## 后果 + +该实现只用一套词汇描述一项已交付功能。若要添加另一种协作方式,必须显式作出设计决策,而不能只增加配置项;ACP 客户端仍可看到标准模式选择器。根据仓库的预发布格式策略,本次迁移有意拒绝旧的 `mode/set` 日志与 `modes.plan.section` 配置。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de9a03288b..f52274cddd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,18 @@ name: CI on: push: - branches: [main, master] + branches: [master] pull_request: + workflow_dispatch: + inputs: + suite: + description: Manual CI suite to run + required: true + default: larger-runner-benchmark + type: choice + options: + - larger-runner-benchmark + - consolidated-runner-benchmark concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -16,106 +26,89 @@ env: PRIMARY_NODE_VERSION: '24' jobs: + + # One large runner pays hosted setup once, then the repository scheduler + # overlaps the complete unsharded primary Node inventory. Build starts eagerly; + # only consumers of emitted output wait for it. node-24: - runs-on: ubuntu-latest - name: node 24 / ${{ matrix.lane }} + if: github.event_name == 'pull_request' + runs-on: dsh-ubuntu-24-04-96core + name: node 24 / complete env: - DSH_GATE_CONCURRENCY: ${{ matrix.gate_concurrency }} - DSH_PUBLINT_CONCURRENCY: ${{ matrix.publint_concurrency }} - DSH_COVERAGE_MAX_WORKERS: ${{ matrix.coverage_max_workers }} - DSH_ESLINT_CACHE: ${{ matrix.eslint_cache }} - strategy: - fail-fast: false - matrix: - include: - - lane: static - command: pnpm run check:ci:static - gate_concurrency: '4' - publint_concurrency: '8' - coverage_max_workers: '' - eslint_cache: '' - - lane: lint - command: pnpm run check:ci:lint - gate_concurrency: '1' - publint_concurrency: '8' - coverage_max_workers: '' - eslint_cache: '1' - - lane: coverage - command: pnpm run check:ci:coverage - gate_concurrency: '1' - publint_concurrency: '8' - coverage_max_workers: '4' - eslint_cache: '' - - lane: snapshot - command: pnpm run check:ci:snapshot - gate_concurrency: '1' - publint_concurrency: '8' - coverage_max_workers: '' - eslint_cache: '' - - lane: artifacts - command: pnpm run check:ci:artifacts - gate_concurrency: '3' - publint_concurrency: '8' - coverage_max_workers: '' - eslint_cache: '' + DSH_COVERAGE_MAX_WORKERS: '16' + DSH_ESLINT_CACHE: '1' + DSH_ESLINT_CONCURRENCY: '16' + DSH_GATE_CONCURRENCY: '10' + DSH_PUBLINT_CONCURRENCY: '16' + DSH_SNAPSHOT_MAX_CONCURRENCY: '8' steps: - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 with: - node-version: ${{ env.PRIMARY_NODE_VERSION }} + persist-credentials: false - - name: Enable corepack (pnpm) - run: corepack enable - - - name: Resolve pnpm store path - id: pnpm-store - run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" - - - uses: actions/cache@v4 + # Pull requests consume the default-branch cache but do not put cache + # compression and upload on the paid latency-critical path. + - uses: actions/cache/restore@v4 with: - path: ${{ steps.pnpm-store.outputs.path }} + path: /home/runner/.local/share/pnpm/store/v11 key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- - - name: Install (immutable) - run: pnpm install --frozen-lockfile - - # The snapshot lane REPLAYS the sandbox example's recorded scenarios, - # re-executing their bash calls under a real runner. ubuntu-latest has - # no bubblewrap preinstalled and no built Landlock launcher, so without - # this the confined executions fail closed (SANDBOX_UNAVAILABLE). Same - # install as sandbox.yml's bwrap leg (incl. the Ubuntu 24.04 AppArmor - # userns knob). - - name: Install bubblewrap (unrestrict userns) - if: matrix.lane == 'snapshot' - run: | - sudo apt-get update -q - sudo apt-get install -yq bubblewrap - sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 \ - || echo "apparmor userns knob absent — the functional probe decides" - - - uses: actions/cache@v4 - if: matrix.lane == 'lint' + - uses: actions/cache/restore@v4 with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | - ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint- + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- + + - name: Select preinstalled Node, install dependencies, and prepare bubblewrap + run: | + node_root="$(printf '%s\n' "$RUNNER_TOOL_CACHE"/node/"${PRIMARY_NODE_VERSION}".*/x64 | sort -V | tail -n 1)" + if [[ ! -d "$node_root" ]]; then + echo "preinstalled Node ${PRIMARY_NODE_VERSION}.x not found in $RUNNER_TOOL_CACHE" >&2 + exit 1 + fi + echo "$node_root/bin" >> "$GITHUB_PATH" + export PATH="$node_root/bin:$PATH" + [[ "$(node --version)" == "v${PRIMARY_NODE_VERSION}."* ]] + corepack enable + pnpm install --frozen-lockfile & + install_pid=$! + bash scripts/prepare-ci-bubblewrap.sh & + sandbox_pid=$! + install_status=0 + wait "$install_pid" || install_status=$? + sandbox_status=0 + wait "$sandbox_pid" || sandbox_status=$? + if (( install_status != 0 )); then exit "$install_status"; fi + exit "$sandbox_status" + + - name: Run complete unsharded primary Node CI concurrently + run: pnpm run check:ci - - name: Run gates - run: ${{ matrix.command }} node-compat: - runs-on: ubuntu-latest - name: node ${{ matrix.node }} + if: github.event_name == 'pull_request' + # Distinct larger-runner pools avoid both standard-runner setup outliers and + # delayed allocation when independent environment contracts share one pool. + runs-on: ${{ matrix.runner }} + name: ${{ matrix.name }} env: - DSH_GATE_CONCURRENCY: '2' + DSH_GATE_CONCURRENCY: ${{ matrix.gate_concurrency }} + DSH_NODE_COMPAT_SKIP_TYPECHECK: '1' strategy: fail-fast: false matrix: - node: ['22.19', 24, 26] + include: + - node: '22.19' + name: node 22.19 + runner: dsh-ubuntu-24-04-4core + gate_concurrency: '2' + - node: 26 + name: node 26 + runner: dsh-ubuntu-24-04-32core + gate_concurrency: '2' steps: - uses: actions/checkout@v6 @@ -123,12 +116,11 @@ jobs: with: node-version: ${{ matrix.node }} - - name: Enable corepack (pnpm) - run: corepack enable - - - name: Resolve pnpm store path + - name: Enable corepack and resolve pnpm store path id: pnpm-store - run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + run: | + corepack enable + echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" - uses: actions/cache@v4 with: @@ -140,11 +132,12 @@ jobs: - name: Install (immutable) run: pnpm install --frozen-lockfile - - name: Run compatibility gates + - name: Run compatibility smokes run: pnpm run check:node-compat python-sdk: - runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + runs-on: dsh-ubuntu-24-04-8core name: python 3.10 / keyless SDK steps: - uses: actions/checkout@v6 @@ -160,12 +153,114 @@ jobs: - name: Run complete keyless Python suite run: uv run --python 3.10 --group test --project python/sdk pytest - # Blocking Windows build lane: keep the already-green native build protected - # while the broader observational gate matrix below exposes the remaining - # portability work without blocking mainline merges. - windows-build: - runs-on: windows-2025 - name: windows / build + # One Windows box shares setup across the required build/site checks and the + # complete observational portability inventory. run-gates reports failures + # from observational gates without allowing them to fail the required job. + windows: + if: github.event_name == 'pull_request' + runs-on: dsh-windows-2025-32core + name: windows node 24 / complete + env: + # Keep ESLint itself single-threaded: 16 ESLint workers took 174 seconds on + # this image. The outer scheduler still overlaps lint with the other gates. + DSH_COVERAGE_MAX_WORKERS: '12' + DSH_ESLINT_CACHE: '1' + DSH_GATE_CONCURRENCY: '16' + DSH_PUBLINT_CONCURRENCY: '16' + steps: + - uses: actions/checkout@v6 + + - uses: actions/cache/restore@v4 + with: + path: .cache/eslint + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- + + # Extracting the many-file pnpm store cache is slower on this image than + # a clean parallel install, and saving it adds more latency after gates. + - name: Select preinstalled Node and install (immutable) + shell: pwsh + run: | + $nodeRoot = Get-ChildItem -Path "$env:RUNNER_TOOL_CACHE\node" -Directory | + Where-Object { $_.Name -like "$env:PRIMARY_NODE_VERSION.*" } | + Sort-Object { [version]$_.Name } | + Select-Object -Last 1 + if ($null -eq $nodeRoot) { + throw "preinstalled Node $env:PRIMARY_NODE_VERSION.x not found in $env:RUNNER_TOOL_CACHE" + } + $nodeBin = Join-Path $nodeRoot.FullName 'x64' + if (-not (Test-Path $nodeBin -PathType Container)) { + throw "preinstalled Node x64 directory not found at $nodeBin" + } + Add-Content -Path $env:GITHUB_PATH -Value $nodeBin + $env:PATH = "$nodeBin;$env:PATH" + if ((node --version) -notlike "v$env:PRIMARY_NODE_VERSION.*") { + throw "selected unexpected Node version $(node --version)" + } + corepack enable + pnpm install --frozen-lockfile + + - name: Run blocking and observational Windows gates concurrently + shell: pwsh + run: pnpm run check:ci:windows-complete + + # Master pushes run only the serial reference jobs below. + # Each host executes the complete, unsharded primary Node aggregate with one + # gate worker, giving reviewers a simple cross-platform oracle for completeness + # and timing. + serial-linux: + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + name: serial / linux + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + + - name: Enable corepack and resolve pnpm store path + id: pnpm-store + run: | + corepack enable + echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + # Master refreshes the caches that pull requests restore without saving. + - uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-store.outputs.path }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- + + - uses: actions/cache@v4 + with: + path: .cache/eslint + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- + + - name: Install (immutable) + run: pnpm install --frozen-lockfile + + - name: Prepare bubblewrap (unrestrict userns) + run: bash scripts/prepare-ci-bubblewrap.sh + + - name: Run complete unsharded primary Node CI serially + env: + DSH_COVERAGE_MAX_WORKERS: '1' + DSH_E2E_MAX_WORKERS: '1' + DSH_ESLINT_CACHE: '1' + DSH_GATE_CONCURRENCY: '1' + DSH_PUBLINT_CONCURRENCY: '1' + DSH_SNAPSHOT_MAX_CONCURRENCY: '1' + run: pnpm run check:ci + + serial-macos: + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + name: serial / macos + runs-on: macos-latest steps: - uses: actions/checkout@v6 @@ -179,55 +274,19 @@ jobs: - name: Install (immutable) run: pnpm install --frozen-lockfile - - name: Build (tsc -b + tsdown) - run: pnpm run build + - name: Run complete unsharded primary Node CI serially + env: + DSH_COVERAGE_MAX_WORKERS: '1' + DSH_E2E_MAX_WORKERS: '1' + DSH_GATE_CONCURRENCY: '1' + DSH_PUBLINT_CONCURRENCY: '1' + DSH_SNAPSHOT_MAX_CONCURRENCY: '1' + run: pnpm run check:ci - # Observational, non-blocking Windows mirror of the Linux gate lanes. Run the - # gates from native PowerShell: an MSYS parent would change the environment - # being measured. This job intentionally stays out of all-checks-passed.needs. - windows-gates: - continue-on-error: true + serial-windows: + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + name: serial / windows runs-on: windows-2025 - name: windows node 24 / ${{ matrix.lane }} - env: - DSH_GATE_CONCURRENCY: ${{ matrix.gate_concurrency }} - DSH_PUBLINT_CONCURRENCY: ${{ matrix.publint_concurrency }} - DSH_COVERAGE_MAX_WORKERS: ${{ matrix.coverage_max_workers }} - DSH_ESLINT_CACHE: ${{ matrix.eslint_cache }} - strategy: - fail-fast: false - matrix: - include: - - lane: static - command: pnpm run check:ci:static - gate_concurrency: '4' - publint_concurrency: '8' - coverage_max_workers: '' - eslint_cache: '' - - lane: lint - command: pnpm run check:ci:lint - gate_concurrency: '1' - publint_concurrency: '8' - coverage_max_workers: '' - eslint_cache: '1' - - lane: coverage - command: pnpm run check:ci:coverage - gate_concurrency: '1' - publint_concurrency: '8' - coverage_max_workers: '4' - eslint_cache: '' - - lane: snapshot - command: pnpm run check:ci:snapshot - gate_concurrency: '1' - publint_concurrency: '8' - coverage_max_workers: '' - eslint_cache: '' - - lane: artifacts - command: pnpm run check:ci:artifacts - gate_concurrency: '3' - publint_concurrency: '8' - coverage_max_workers: '' - eslint_cache: '' steps: - uses: actions/checkout@v6 @@ -245,12 +304,112 @@ jobs: shell: pwsh run: corepack enable - - name: Resolve pnpm store path - id: pnpm-store + # Master refreshes the small cache that pull requests restore without + # putting package-store extraction back on the Windows critical path. + - uses: actions/cache@v4 + with: + path: .cache/eslint + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- + + - name: Install (immutable) shell: pwsh - run: '"path=$(pnpm store path --silent)" >> $env:GITHUB_OUTPUT' + run: pnpm install --frozen-lockfile + + - name: Run complete unsharded primary Node CI serially + shell: pwsh + env: + DSH_COVERAGE_MAX_WORKERS: '1' + DSH_E2E_MAX_WORKERS: '1' + DSH_ESLINT_CACHE: '1' + DSH_GATE_CONCURRENCY: '1' + DSH_PUBLINT_CONCURRENCY: '1' + DSH_SNAPSHOT_MAX_CONCURRENCY: '1' + run: pnpm run check:ci + + # Manual, bounded comparison of the actual critical Linux and Windows lanes. + # The named pools are restricted at the organization level to this repository. + larger-runner-benchmark: + if: github.event_name == 'workflow_dispatch' && inputs.suite == 'larger-runner-benchmark' + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + strategy: + fail-fast: false + max-parallel: 12 + matrix: + include: + - platform: linux + cores: '4' + runner: dsh-ubuntu-24-04-4core + workload: typecheck + - platform: linux + cores: '8' + runner: dsh-ubuntu-24-04-8core + workload: typecheck + - platform: linux + cores: '16' + runner: dsh-ubuntu-24-04-16core + workload: typecheck + - platform: linux + cores: '32' + runner: dsh-ubuntu-24-04-32core + workload: typecheck + - platform: linux + cores: '64' + runner: dsh-ubuntu-24-04-64core + workload: typecheck + - platform: linux + cores: '96' + runner: dsh-ubuntu-24-04-96core + workload: typecheck + - platform: windows + cores: '4' + runner: dsh-windows-2025-4core + workload: production-site + - platform: windows + cores: '8' + runner: dsh-windows-2025-8core + workload: production-site + - platform: windows + cores: '16' + runner: dsh-windows-2025-16core + workload: production-site + - platform: windows + cores: '32' + runner: dsh-windows-2025-32core + workload: production-site + - platform: windows + cores: '64' + runner: dsh-windows-2025-64core + workload: production-site + - platform: windows + cores: '96' + runner: dsh-windows-2025-96core + workload: production-site + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + + - name: Report runner capacity + run: >- + node -e "const os = require('node:os'); + console.log(JSON.stringify({ arch: process.arch, cpus: os.cpus().length, + memoryGiB: Math.round(os.totalmem() / 2 ** 30) }))" + + - name: Enable corepack (pnpm) + run: corepack enable + + - name: Resolve pnpm store path + if: matrix.platform == 'linux' + id: pnpm-store + run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" - uses: actions/cache@v4 + if: matrix.platform == 'linux' with: path: ${{ steps.pnpm-store.outputs.path }} key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} @@ -258,26 +417,161 @@ jobs: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- - name: Install (immutable) + run: pnpm install --frozen-lockfile + + - name: Run critical Linux typecheck lane + if: matrix.platform == 'linux' + run: pnpm run typecheck + + - name: Run critical Windows production-site lane + if: matrix.platform == 'windows' + run: pnpm run docs:build + + # Manual comparison of the intended low-fanout topology. Linux runs the + # complete unsharded primary aggregate with bounded in-runner parallelism; + # Windows runs both blocking build surfaces concurrently through run-gates. + consolidated-runner-benchmark: + if: github.event_name == 'workflow_dispatch' && inputs.suite == 'consolidated-runner-benchmark' + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + strategy: + fail-fast: false + max-parallel: 12 + matrix: + include: + - platform: linux + cores: '4' + runner: dsh-ubuntu-24-04-4core + workers: '4' + - platform: linux + cores: '8' + runner: dsh-ubuntu-24-04-8core + workers: '8' + - platform: linux + cores: '16' + runner: dsh-ubuntu-24-04-16core + workers: '16' + - platform: linux + cores: '32' + runner: dsh-ubuntu-24-04-32core + workers: '32' + - platform: linux + cores: '64' + runner: dsh-ubuntu-24-04-64core + workers: '32' + - platform: linux + cores: '96' + runner: dsh-ubuntu-24-04-96core + workers: '32' + - platform: windows + cores: '4' + runner: dsh-windows-2025-4core + workers: '2' + - platform: windows + cores: '8' + runner: dsh-windows-2025-8core + workers: '2' + - platform: windows + cores: '16' + runner: dsh-windows-2025-16core + workers: '2' + - platform: windows + cores: '32' + runner: dsh-windows-2025-32core + workers: '2' + - platform: windows + cores: '64' + runner: dsh-windows-2025-64core + workers: '2' + - platform: windows + cores: '96' + runner: dsh-windows-2025-96core + workers: '2' + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.PRIMARY_NODE_VERSION }} + + - name: Report runner capacity + run: >- + node -e "const os = require('node:os'); + console.log(JSON.stringify({ arch: process.arch, cpus: os.cpus().length, + memoryGiB: Math.round(os.totalmem() / 2 ** 30) }))" + + - name: Enable corepack (pnpm) + run: corepack enable + + - name: Resolve pnpm store path (Linux) + if: matrix.platform == 'linux' + id: pnpm-store-linux + run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - name: Resolve pnpm store path (Windows) + if: matrix.platform == 'windows' + id: pnpm-store-windows + shell: pwsh + run: '"path=$(pnpm store path --silent)" >> $env:GITHUB_OUTPUT' + + - uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-store-linux.outputs.path || steps.pnpm-store-windows.outputs.path }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- + + - uses: actions/cache@v4 + if: matrix.platform == 'linux' + with: + path: .cache/eslint + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + restore-keys: | + ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- + + - name: Install and prepare Linux + if: matrix.platform == 'linux' + run: | + pnpm install --frozen-lockfile & + install_pid=$! + bash scripts/prepare-ci-bubblewrap.sh & + sandbox_pid=$! + install_status=0 + wait "$install_pid" || install_status=$? + sandbox_status=0 + wait "$sandbox_pid" || sandbox_status=$? + if (( install_status != 0 )); then exit "$install_status"; fi + exit "$sandbox_status" + + - name: Install (immutable) + if: matrix.platform == 'windows' shell: pwsh run: pnpm install --frozen-lockfile - - uses: actions/cache@v4 - if: matrix.lane == 'lint' - with: - path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint- + - name: Run complete unsharded primary Node CI concurrently + if: matrix.platform == 'linux' + env: + DSH_COVERAGE_MAX_WORKERS: ${{ matrix.workers }} + DSH_ESLINT_CACHE: '1' + DSH_ESLINT_CONCURRENCY: ${{ matrix.workers }} + DSH_GATE_CONCURRENCY: ${{ matrix.workers }} + DSH_PUBLINT_CONCURRENCY: ${{ matrix.workers }} + DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ matrix.workers }} + run: pnpm run check:ci - - name: Run gates + - name: Run blocking Windows builds concurrently + if: matrix.platform == 'windows' shell: pwsh - run: ${{ matrix.command }} + env: + DSH_GATE_CONCURRENCY: ${{ matrix.workers }} + run: pnpm run check:ci:windows-blocking # Single stable required check for branch protection: require "all checks # passed" instead of enumerating matrix legs whose names change as lanes and # node versions evolve. Every blocking job in THIS workflow must be listed in - # `needs`; explicitly observational jobs such as windows-gates stay out - # (`needs` cannot reach across workflow files; e2e.yml stays its own check). + # `needs`; observational Windows gates share the required Windows job but are + # marked non-blocking inside run-gates. (`needs` cannot reach across workflow + # files; e2e.yml stays its own check.) # `if: always()` is load-bearing: without it a failed dependency # would SKIP this job, and GitHub counts a skipped required check as passing # — so this job always runs and fails on any non-success result, including @@ -285,8 +579,8 @@ jobs: all-checks-passed: name: all checks passed runs-on: ubuntu-latest - needs: [node-24, node-compat, python-sdk, windows-build] - if: always() + needs: [node-24, node-compat, python-sdk, windows] + if: always() && github.event_name == 'pull_request' steps: - name: Fail if any needed job did not succeed if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') diff --git a/AGENTS.md b/AGENTS.md index 8ac00e2ef2..739bdfafe2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,7 @@ packages/ @deepseek-ai/dsh- workspaces at packages/// subagent/ subagent seam + spawn/fork/ACP backends + delegation tool workflow/ workflow seam + worker-thread engine + the workflow tool todo/ the todo_write tool + plan/ plan mode as logged per-agent collaboration state guard/ loop-hygiene plugins cordis/ self-referential toolset: the agent inspects/mounts plugins in its own runtime hooks/ Claude Code / Codex hook bridges + shared wire-protocol library diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 9854d37019..ca3e7d5bdd 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: b3e2db14727c299562f9b061459547d147ec1d70 -architecture.zh.md: 6fd2a7e161a10ac5f2dcee6859b0d6251671676f +architecture.md: d7555368b583203cee664da3615c4fb4fb680c30 +architecture.zh.md: bafe59dee0559d5969c7400bdae85be0d012b3b5 diff --git a/docs/architecture.md b/docs/architecture.md index b3e2db1472..d7555368b5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -37,6 +37,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts whose packages contribute serv | `ctx.web` | [`web/`](../packages/web/README.md) | search/fetch provider registries | | `ctx.compact`, `ctx.toolResultPrune` | [`compact/`](../packages/compact/README.md)/[`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune/README.md) | summary compaction; optional model-free result pruning | | `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | named delegation providers | +| `ctx.planMode` | [`plan/`](../packages/plan/README.md) | logged plan collaboration state | | `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | background task registry + generic `task_*` control tools | | `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | script-driven multi-agent orchestration | | `ctx.goals` | [`goal/`](../packages/goal/README.md) | persisted same-session goals | diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 6fd2a7e161..bafe59dee0 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -37,6 +37,7 @@ | `ctx.web` | [`web/`](../packages/web/README.md) | 搜索与抓取提供方注册表 | | `ctx.compact`,`ctx.toolResultPrune` | [`compact/`](../packages/compact/README.md)/[`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune/README.md) | 摘要压缩(compaction);可选的无模型结果裁剪 | | `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | 具名委托提供方 | +| `ctx.planMode` | [`plan/`](../packages/plan/README.md) | 落日志的 plan 协作状态 | | `ctx.tasks` | [`tasks/`](../packages/tasks/README.md) | 后台任务注册表和通用 `task_*` 控制工具 | | `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | 脚本驱动的多 agent 编排 | | `ctx.goals` | [`goal/`](../packages/goal/README.md) | 持久化的同会话目标 | diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 0f696831cc..2832cec245 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -54,6 +54,8 @@ flowchart LR pkg_user_interaction["user-interaction"] svc_userInteraction["ctx.userInteraction
Human question/answer seam"] pkg_tui["tui"] + pkg_plan_mode["plan-mode"] + svc_planMode["ctx.planMode
Plan collaboration state"] pkg_commands["commands"] svc_commands["ctx.commands
Human command registry"] pkg_skill["skill"] @@ -136,6 +138,7 @@ flowchart LR pkg_llm_pi_ai --> svc_llm pkg_llm_replay --> svc_llm pkg_permission --> svc_permission + pkg_plan_mode --> svc_planMode pkg_sandbox --> svc_sandbox pkg_sandbox_local --> svc_sandbox pkg_sandbox_policy --> svc_sandboxPolicy @@ -192,6 +195,7 @@ flowchart LR svc_llm --> pkg_agent_loop svc_llm --> pkg_compact_basic svc_permission --> pkg_acp + svc_planMode --> pkg_acp svc_sandbox --> pkg_bash_sandbox svc_sandboxPolicy --> pkg_bash_sandbox svc_sandboxPolicy --> pkg_fs_sandbox @@ -252,6 +256,7 @@ flowchart LR | `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. | | `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/cordis/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web), [`acp`](../packages/ui/acp) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. | | `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | [`tui`](../packages/ui/tui), [`acp`](../packages/ui/acp) | [`tool-ask-user`](../packages/ui/tool-ask-user), [`tui`](../packages/ui/tui), [`acp`](../packages/ui/acp) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. | +| `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | [`acp`](../packages/ui/acp) | - | Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions. | | `ctx.commands` | `core` | [`commands`](../packages/ui/commands) | - | [`tui`](../packages/ui/tui), [`acp`](../packages/ui/acp) | - | Plugins register direct human commands; TUI and ACP consume the same effective per-agent catalog without sending invocations to the model. | | `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. | | `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tui-demo`](../packages/examples/tui-demo) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 924644dd42..f202990b82 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:254`](../packages/ui/acp/src/index.ts) +Source: [`packages/ui/acp/src/index.ts:275`](../packages/ui/acp/src/index.ts) ## `@deepseek-ai/dsh-acp-demo` @@ -786,6 +786,20 @@ Depends on: [`ApprovalPolicy`](core-data-structures/approval.md) · [`SandboxMod Source: [`packages/ui/permission/src/index.ts:83`](../packages/ui/permission/src/index.ts) +## `@deepseek-ai/dsh-plan-mode` + +Requires: `tools` · `systemPrompt` + +```ts config-catalog +/** Deployment-owned plan guidance. */ +export interface PlanModeConfig { + /** Guidance rendered as the `plan:policy` prompt section while plan mode is active. */ + section: string +} +``` + +Source: [`packages/plan/plan-mode/src/index.ts:57`](../packages/plan/plan-mode/src/index.ts) + ## `@deepseek-ai/dsh-repeat-tool-guard` ```ts config-catalog diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml index 17b2346249..f0dc01c523 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: a1f6d2f0d27b2258ae06236721bbd80cbd3af80e -extension-cookbook.zh.md: f7729492b68bfef50d5e289581028d0c0c4164cf +extension-cookbook.md: 8873cac21960e2e2efe0e8c6c5868c3a8e7ee75c +extension-cookbook.zh.md: f34e9f2fa707be69b13ac408cc1ede1a86310fae diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md index a1f6d2f0d2..8873cac219 100644 --- a/docs/cookbook/extension-cookbook.md +++ b/docs/cookbook/extension-cookbook.md @@ -87,7 +87,7 @@ export function apply(ctx: Context) { ## Runnable wirings -Four runnable leaves load their plugin trees from `cordis.yml`: [`examples/tui-agent`](../../examples/tui-agent) (DeepSeek coding tools through the full-screen TUI, `pnpm run demo:tui`), [`examples/headless-agent`](../../examples/headless-agent) (the coding capabilities behind a one-shot task and DSH-native output, `pnpm run demo:headless "task"`), [`examples/cordis-agent`](../../examples/cordis-agent) (self-inspection and dynamic plugin mounting through the TUI, `pnpm run demo:cordis`), and [`examples/acp-agent`](../../examples/acp-agent) (an ACP server over JSON-RPC stdio, `pnpm run demo:acp`). Interactive leaves load [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo), non-interactive leaves load [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo), the ACP leaf loads [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and all three app packages share [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo). +Runnable leaves load their plugin trees from `examples/*/cordis.yml`; the root `demo:*` scripts and those leaf directories are the authoritative inventory. Interactive leaves use [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo), non-interactive leaves use [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo), ACP leaves use [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and the app packages share [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo). ## The feature → mechanism map @@ -113,7 +113,7 @@ Every product feature maps to a listener on a documented extension seam — the | Monotonic terminal turn policy | return `{ action: 'stop' }` from serial `agent/turn-stop`, after continuation and steering have already been folded | | 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 | `tools/pre-execute` (deny writes) + a mode prompt section via `ctx.systemPrompt.section()` or `agent.inject()` (model-visible ⟺ logged: `agent/request` shapes call config only) | +| 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]`, and the user-reviewed `exit_plan_mode` exit; enforcement stays on the independent sandbox/approval axes | | Sub-agent delegation | the `ctx.subagents` provider registry (`dsh-subagent-spawn`/`-fork`/`-acp`) + `dsh-tool-subagent` exposing one configured provider to the model | | MCP | one plugin per server: discover tools → `ctx.tools.register()` | | Skills | section + tool registration; `inject()` skill content on invocation | diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md index f7729492b6..f34e9f2fa7 100644 --- a/docs/cookbook/extension-cookbook.zh.md +++ b/docs/cookbook/extension-cookbook.zh.md @@ -87,7 +87,7 @@ export function apply(ctx: Context) { ## 可运行的组装示例 -四个可运行叶子从 `cordis.yml` 加载各自的插件树:[`examples/tui-agent`](../../examples/tui-agent)(通过全屏 TUI 运行的 DeepSeek coding 工具,`pnpm run demo:tui`)、[`examples/headless-agent`](../../examples/headless-agent)(通过单次任务和 DSH 原生输出运行的 coding 能力,`pnpm run demo:headless "task"`)、[`examples/cordis-agent`](../../examples/cordis-agent)(通过 TUI 进行自我检查和动态插件挂载,`pnpm run demo:cordis`)与 [`examples/acp-agent`](../../examples/acp-agent)(通过 JSON-RPC stdio 暴露的 ACP 服务器,`pnpm run demo:acp`)。交互式叶子加载 [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo),非交互式叶子加载 [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo),ACP 叶子加载 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),三个 app 包都通过 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo) 共享主干。 +可运行叶子从 `examples/*/cordis.yml` 加载各自的插件树;根目录的 `demo:*` 脚本和这些叶子目录是权威清单。交互式叶子使用 [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo),非交互式叶子使用 [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo),ACP 叶子使用 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),应用包共享 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo)。 ## 功能→机制映射 @@ -113,7 +113,7 @@ export function apply(ctx: Context) { | 单调终端轮次策略 | 从串行 `agent/turn-stop` 返回 `{ action: 'stop' }`,此时 continuation 和 steering 已折叠完毕 | | 子进程沙箱(landlock / sandbox-exec) | 通过 `dsh-bash-sandbox` 使用 `ctx.sandbox` 后端;能力级别的拒绝使用 `tools/pre-execute` | | 权限系统 / AskUserQuestion | 从 `tools/pre-execute` 返回 `ask` 并通过 `ctx.approval` 应答;为普通用户提问注册一个独立的面向模型的 ask 工具 | -| Plan mode | `tools/pre-execute`(拒绝写操作)+ 通过 `ctx.systemPrompt.section()` 或 `agent.inject()` 注入模式提示词段(model-visible ⟺ logged:`agent/request` 仅塑形调用配置) | +| Plan mode | 已交付:[`@deepseek-ai/dsh-plan-mode`](../../packages/plan/plan-mode/README.md) — 落日志的 `plan/mode` 状态、`plan:policy` 引导段、`/plan [message]`,以及经用户评审的 `exit_plan_mode` 出口;强制约束留在独立的沙箱/审批轴上 | | 子 agent 委派 | `ctx.subagents` 提供方注册表(`dsh-subagent-spawn`/`-fork`/`-acp`)+ `dsh-tool-subagent` 向模型暴露一个已配置的提供方 | | MCP | 每个服务器一个插件:发现工具 → `ctx.tools.register()` | | Skill(技能) | section + 工具注册;调用时通过 `inject()` 注入 skill 内容 | diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 6aa44bcf63..e73fce4169 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -733,6 +733,33 @@ Types: [Session](../core-data-structures/session.md) · [SessionEvent](../core-d Source: [`packages/ui/permission/src/index.ts:97`](../../packages/ui/permission/src/index.ts) +## `ctx.planMode` — `PlanModeService` + +`ctx.planMode`: owns logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool. UIs observe committed flips through `session/event`; there is no live mirror. + +```ts cordis-catalog +/** + * Read the logged plan state and any selected state awaiting a boundary. + * + * @param agent The agent to read. + * @returns Current logged state plus a pending selection, when present. + */ +get(agent: Agent): { active: boolean; pending?: boolean } + +/** + * Select whether plan mode should be active from the next turn boundary. + * Repeated selection of the current or already-pending state is a no-op. + * + * @param agent The agent to switch. + * @param active Whether plan mode should be active. + */ +set(agent: Agent, active: boolean): void +``` + +Types: [Agent](../core-data-structures/core.md) + +Source: [`packages/plan/plan-mode/src/index.ts:141`](../../packages/plan/plan-mode/src/index.ts) + ## `ctx.sandbox` — `SandboxProvider` (abstract seam) Abstract process-sandbox service. confine must return enforcing argv or fail closed at wrap or runner-execution time; silent unconfined passthrough is forbidden. Functional probes arbitrate multi-runner chains and may be skipped for a sole candidate, whose own refusal remains the fail-closed end. diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 34df633982..c547c92761 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -430,7 +430,7 @@ interface Agent { } ``` -`AgentStatus` is `'idle' | 'running' | 'disposed'`, and `SessionId` is branded. `running` describes the driver-wide drain interval, which can span turn close, its durability checkpoint, and consecutive queued turns; it does not prove a turn is still open. `AgentOptions` is merge-extensible and currently includes `provider?` and `model?`; dispatch requires both after `agent/request`. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default. +`AgentStatus` is `'idle' | 'running' | 'disposed'`, and `SessionId` is branded. `running` describes the driver-wide drain interval, which can span turn close, its durability checkpoint, and consecutive queued turns; it does not prove a turn is still open. `AgentOptions` is merge-extensible: core declares `provider?` and `model?` (dispatch requires both after `agent/request`). Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default. The cause is a TypeScript-enforced same-process input. An active holder copies its discriminant into the runtime-only `AbortSignal.reason`; it is retired before `turn/end` publication. `agentInterruptReasonOf(signal)` recognizes `user`, `parent`, and lifecycle-only `disposed` without consulting ambient initiator state. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result. diff --git a/docs/core-data-structures/user-interaction.md b/docs/core-data-structures/user-interaction.md index 46e17f3f83..1ec83b7827 100644 --- a/docs/core-data-structures/user-interaction.md +++ b/docs/core-data-structures/user-interaction.md @@ -20,15 +20,17 @@ interface AskUserQuestionOption { ## Question item -`AskUserQuestionItem` is one question in a request. The model supplies a stable `id`, which is echoed back with the answer so batched questions remain routable. +`AskUserQuestionItem` is one question in a request. The caller supplies a stable `id`, which is echoed back with the answer so batched questions remain routable. Optional `detail` carries supporting text that providers render with the question but keep out of selectable option labels. ```ts type-equiv -/** One question in an ask_user_question request. */ +/** One question in a user-interaction request. */ interface AskUserQuestionItem { - /** Stable model-provided question id, echoed in the answer. */ + /** Stable caller-provided question id, echoed in the answer. */ id: string /** The question to display. */ question: string + /** Optional supporting detail rendered with the question but kept out of option labels. */ + detail?: string /** Optional short heading/group label. */ header?: string /** Optional choices the UI can render as a menu. */ diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 84a485f47c..d7f4123b91 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -14,15 +14,15 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent/error` | `emit` | [`packages/core/agent/src/types.ts:346`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) | | `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:296`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic) | | `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:230`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) | -| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:243`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | +| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:243`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`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/queued` | `emit` | [`packages/core/agent/src/types.ts:191`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) | | `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:257`](../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:311`](../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/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:311`](../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), [`plan-mode`](../packages/plan/plan-mode) | | `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:272`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`tool-skill`](../packages/skill/tool-skill), [`workspace-context`](../packages/context/workspace-context) | | `agent/session-start` | `emit` | [`packages/core/agent/src/types.ts:214`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`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:181`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) | | `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:284`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | -| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:322`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | +| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:322`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode) | | `agent/turn-stop` | `serial` | [`packages/core/agent/src/types.ts:333`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tool-goal`](../packages/goal/tool-goal) | | `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) | @@ -57,7 +57,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event string | Dispatchers | Listeners | | --- | --- | --- | -| `internal/dispatch` | - | [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`workflow`](../packages/workflow/workflow) | +| `internal/dispatch` | - | [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`workflow`](../packages/workflow/workflow) | | `internal/plugin` | - | `webserver` | | `internal/status` | - | [`agent`](../packages/core/agent) | | `slots/changed` | `runtime` (`emit`) | - | diff --git a/docs/module-graph.md b/docs/module-graph.md index 681d839072..ed5e11937f 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -86,6 +86,9 @@ flowchart TD subgraph group_todo["packages/todo"] pkg_tool_todo["tool-todo"] end + subgraph group_plan["packages/plan"] + pkg_plan_mode["plan-mode"] + end subgraph group_cordis["packages/cordis"] pkg_tool_cordis["tool-cordis"] end @@ -498,6 +501,13 @@ flowchart TD pkg_tool_todo --> pkg_invariants pkg_tool_todo --> pkg_session pkg_tool_todo --> pkg_tools + pkg_plan_mode --> pkg_agent + pkg_plan_mode --> pkg_commands + pkg_plan_mode --> pkg_invariants + pkg_plan_mode --> pkg_session + pkg_plan_mode --> pkg_system_prompt + pkg_plan_mode --> pkg_tools + pkg_plan_mode --> pkg_user_interaction pkg_tool_cordis --> pkg_invariants pkg_tool_cordis --> pkg_scope pkg_tool_cordis --> pkg_tools @@ -514,21 +524,6 @@ flowchart TD pkg_agent_loop_testkit --> pkg_session pkg_agent_loop_testkit --> pkg_system_prompt pkg_agent_loop_testkit --> pkg_tools - pkg_acp --> pkg_agent - pkg_acp --> pkg_bash - pkg_acp --> pkg_commands - pkg_acp --> pkg_invariants - pkg_acp --> pkg_llm - pkg_acp --> pkg_llm_retry - pkg_acp --> pkg_permission - pkg_acp --> pkg_sandbox - pkg_acp --> pkg_session - pkg_acp --> pkg_session_persistence - pkg_acp --> pkg_session_title - pkg_acp --> pkg_system_prompt - pkg_acp --> pkg_tools - pkg_acp --> pkg_user_approval - pkg_acp --> pkg_user_interaction pkg_tool_ask_user --> pkg_agent pkg_tool_ask_user --> pkg_invariants pkg_tool_ask_user --> pkg_tools @@ -590,6 +585,22 @@ flowchart TD pkg_hooks_claude --> pkg_session_persistence pkg_hooks_claude --> pkg_subagent pkg_hooks_claude --> pkg_tools + pkg_acp --> pkg_agent + pkg_acp --> pkg_bash + pkg_acp --> pkg_commands + pkg_acp --> pkg_invariants + pkg_acp --> pkg_llm + pkg_acp --> pkg_llm_retry + pkg_acp --> pkg_permission + pkg_acp --> pkg_plan_mode + pkg_acp --> pkg_sandbox + pkg_acp --> pkg_session + pkg_acp --> pkg_session_persistence + pkg_acp --> pkg_session_title + pkg_acp --> pkg_system_prompt + pkg_acp --> pkg_tools + pkg_acp --> pkg_user_approval + pkg_acp --> pkg_user_interaction pkg_jsonrpc --> pkg_agent pkg_jsonrpc --> pkg_invariants pkg_jsonrpc --> pkg_llm @@ -788,10 +799,10 @@ flowchart TD | [`spill-policy`](../packages/spill/spill-policy) | `spill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`tools`](../packages/core/tools) | | [`timeout-policy`](../packages/timeout/timeout-policy) | `timeout` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | +| [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/ui/commands), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | | [`tool-cordis`](../packages/cordis/tool-cordis) | `cordis` | [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) | | [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools) | | [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`acp`](../packages/ui/acp) | `ui` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`commands`](../packages/ui/commands), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-title`](../packages/session-title/session-title), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`user-interaction`](../packages/ui/user-interaction) | | [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | | [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | @@ -803,6 +814,7 @@ flowchart TD | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) | | [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | +| [`acp`](../packages/ui/acp) | `ui` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`commands`](../packages/ui/commands), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-title`](../packages/session-title/session-title), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`user-interaction`](../packages/ui/user-interaction) | | [`jsonrpc`](../packages/ui/jsonrpc) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tui`](../packages/ui/tui) | `ui` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`commands`](../packages/ui/commands), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-title`](../packages/session-title/session-title), [`skill`](../packages/skill/skill), [`system-prompt`](../packages/core/system-prompt), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 1948e01041..7720d92ed8 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -328,6 +328,21 @@ Source: [`packages/llm/llm-retry/src/index.ts:18`](../packages/llm/llm-retry/src Source: [`packages/ui/permission/src/index.ts:36`](../packages/ui/permission/src/index.ts) +### `plan/*` + +#### `plan/mode` — log-only + +```ts persistence-catalog +/** + * Whether plan mode is in force from this point on: log-only, non-surface, + * whole-value replace. The last `plan/mode` wins; a log with none folds to + * inactive through {@link foldPlanMode}. + */ +'plan/mode': { active: boolean } +``` + +Source: [`packages/plan/plan-mode/src/index.ts:40`](../packages/plan/plan-mode/src/index.ts) + ### `prompt/*` #### `prompt/blocked` — log-only diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 8273203616..817a67863a 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -17,6 +17,7 @@ This table connects model-visible tool names to the plugin package and service s | --- | --- | --- | --- | --- | --- | | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userInteraction` | `tool/call`, `tool/result after a UI/provider answers the question` | - | ask_user_question pauses the tool call until the active UI provider returns a human answer. | | `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/code-dispatch per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. | +| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.userInteraction (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-interaction seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. | | `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`, `ctx.bash`, `ctx.tasks at call time for run_in_background` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.tasks` runtime and is collected/stopped through the `task_*` tools from `@deepseek-ai/dsh-tool-tasks`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled. | | `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect`, `cordis_mount`, `cordis_unmount` | `ctx.tools` | `tool/call`, `tool/result`, `live plugin-tree mutations (mount/unmount)` | - | Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; a full changed request header logs those tool-set changes. | | `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after successful file operations`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin. | @@ -126,6 +127,31 @@ Source: [`packages/core/tools/src/code-mode.ts`](../packages/core/tools/src/code Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. +## `@deepseek-ai/dsh-plan-mode` + +### `exit_plan_mode` + +Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. + +```json +{ + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] +} +``` + +Source: [`packages/plan/plan-mode/src/index.ts`](../packages/plan/plan-mode/src/index.ts) + +exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-interaction seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. + ## `@deepseek-ai/dsh-tool-bash` ### `bash` diff --git a/examples/README.md b/examples/README.md index 8578c5c25a..b895259965 100644 --- a/examples/README.md +++ b/examples/README.md @@ -30,4 +30,4 @@ An agent demo exposed as an **Agent Client Protocol (ACP)** server over JSON-RPC Run with: `pnpm run demo:acp` (needs `DEEPSEEK_API_KEY`); `pnpm run demo:code-mode acp` boots the same server in Code Mode via the `code-mode.cordis.yml` overlay. See [acp-agent/README.md](acp-agent/README.md) for the Zed setup and the snapshot-test design. -The default `cordis.yml` composes [`@deepseek-ai/dsh-sandbox-local`](../packages/sandbox/sandbox-local), [`@deepseek-ai/dsh-bash-sandbox`](../packages/bash/bash-sandbox), [`@deepseek-ai/dsh-user-approval`](../packages/ui/user-approval), and [`@deepseek-ai/dsh-permission`](../packages/ui/permission). A capable client gets one `Permissions` select: `workspace-write` confines bash to the configured workspace and asks before a wider retry, while `danger-full-access` removes file confinement and disables approval prompts. A denied command can therefore surface a one-shot `session/request_permission` prompt in the editor; "Allow once" runs exactly that retry under the requested wider mode. +The default `cordis.yml` composes [`@deepseek-ai/dsh-plan-mode`](../packages/plan/plan-mode), [`@deepseek-ai/dsh-sandbox-local`](../packages/sandbox/sandbox-local), [`@deepseek-ai/dsh-bash-sandbox`](../packages/bash/bash-sandbox), [`@deepseek-ai/dsh-user-approval`](../packages/ui/user-approval), and [`@deepseek-ai/dsh-permission`](../packages/ui/permission). A capable client gets a `default` / `plan` mode picker plus one independent `Permissions` select: plan adds model guidance and the reviewed `exit_plan_mode` crossing without changing enforcement, while `workspace-write` confines bash to the configured workspace and asks before a wider retry. See [acp-agent/README.md](acp-agent/README.md#plan-mode) for the plan-review and elicitation flow. diff --git a/examples/acp-agent/README.md b/examples/acp-agent/README.md index 6e97204963..c96cf0109c 100644 --- a/examples/acp-agent/README.md +++ b/examples/acp-agent/README.md @@ -7,7 +7,7 @@ pnpm run demo:acp # needs DEEPSEEK_API_KEY (repo-root .env or env) pnpm run demo:code-mode acp # the same server in Code Mode: one wire tool, run_code ``` -The leaf config loads the ACP app, DeepSeek adapter, sandboxed bash, the sandboxed filesystem stack, approval and permission services, model-facing tools, and repeat guard. The app bundles the agent spine, JSONL persistence, and bridge, creates agents on `session/new`, and keeps stdout logger-free. [`fs.cordis.yml`](fs.cordis.yml) adds local tool-result spill storage for its dedicated scenarios; [`code-mode.cordis.yml`](code-mode.cordis.yml) adds `run_code` and its generated TypeScript SDK. See [Code Mode](../../packages/core/tools/README.md#code-mode). +The leaf config loads the ACP app, DeepSeek adapter, plan mode, sandboxed bash, the sandboxed filesystem stack, approval and permission services, model-facing tools, and repeat guard. The app bundles the agent spine, JSONL persistence, and bridge, creates agents on `session/new`, and keeps stdout logger-free. [`fs.cordis.yml`](fs.cordis.yml) adds local tool-result spill storage for its dedicated scenarios; [`code-mode.cordis.yml`](code-mode.cordis.yml) adds `run_code` and its generated TypeScript SDK. See [Code Mode](../../packages/core/tools/README.md#code-mode). ## stdout is the protocol @@ -29,11 +29,17 @@ Add to your Zed `settings.json` under `agent_servers`: } ``` -The editor sets each session's `cwd` to the project it opens, and bash uses that directory as its workdir. The current sandbox write boundary is nevertheless fixed when the server starts (`workspaceRoot: process.cwd()`), so launch the server from the workspace it should be allowed to modify; making that root session-scoped is deferred in the [sandbox Agent Note](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). The filesystem tools now ride the same sandbox policy through [`@deepseek-ai/dsh-fs-sandbox`](../../packages/fs/fs-sandbox/), so `read`/`write`/`edit` are available under every mode and confined to the same `workspaceRoot`. +The editor sets each session's `cwd` to the project it opens, and bash uses that directory as its workdir. The current sandbox write boundary is nevertheless fixed when the server starts (`workspaceRoot: process.cwd()`), so launch the server from the workspace it should be allowed to modify; making that root session-scoped is deferred in the [sandbox Agent Note](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). The filesystem tools ride the same sandbox policy through [`@deepseek-ai/dsh-fs-sandbox`](../../packages/fs/fs-sandbox/), so `read`/`write`/`edit` remain available regardless of plan state and confined to the same `workspaceRoot`. + +## Plan mode + +The same `demo:acp` server composes [`@deepseek-ai/dsh-plan-mode`](../../packages/plan/plan-mode/), so a capable client advertises `default` and `plan` in its mode picker. ACP owns those protocol ids and projects them onto the plugin's boolean plan state. This composition owns the complete plan instructions in [`cordis.yml`](cordis.yml): remain in plan mode, inspect before asking, avoid mutations, resolve discoverable repository facts, and submit a decision-complete plan through `exit_plan_mode`. Those are the instrumental behaviors shared by the local Codex and Claude Code references; product-specific plan files, phase machinery, and protocol tags stay out of the plugin contract. + +Plan mode adds only that configured guidance section. Every tool, including `exit_plan_mode`, keeps the same schema while plan mode is inactive or active; the exit tool describes itself as plan-only and rejects if called while inactive. Stable native schemas and Code Mode SDK bindings avoid tool-catalog churn at the transition. `ask_user_question` carries blocking user-owned choices through ACP elicitation, while `exit_plan_mode` renders the exact logged plan for approval and returns keep-planning feedback to the model. The mode picker and permission select remain independent: switching plan state never changes sandbox or approval state, and deployments that need a hard read-only planning floor configure that policy separately. The [plan-mode Agent Note](../../.agents/notes/implemented/feature/2026-07-07-plan-mode.md) owns the state and review contract. ## Snapshot tests (record-once / replay-deterministic) -This example hosts the ACP snapshot suite. It replays through `dsh-llm-replay`, which reconstructs model streams from `assistant/chunk` events in each scenario's session JSONL. Recording runs the real ACP agent and harvests its logs; refresh keeps the committed transcript as mock input and rewrites current replay outputs. `replay.override.json` covers throw and hang cases that chunks cannot express, and an optional `workspace/` seeds files. The [snapshot Agent Note](../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md) owns the ACP harness design. +This example hosts the ACP snapshot suite, including the picker advertisement and both plan-review branches. It replays through `dsh-llm-replay`, which reconstructs model streams from `assistant/chunk` events in each scenario's session JSONL. Recording runs the real ACP agent and harvests its logs; refresh keeps the committed transcript as mock input and rewrites current replay outputs. `replay.override.json` covers throw and hang cases that chunks cannot express, and an optional `workspace/` seeds files. The [snapshot Agent Note](../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md) owns the ACP harness design. ## Permissions and sandboxing diff --git a/examples/acp-agent/composition.md b/examples/acp-agent/composition.md index 7645e85265..841639fd90 100644 --- a/examples/acp-agent/composition.md +++ b/examples/acp-agent/composition.md @@ -29,6 +29,10 @@ flowchart LR bundle_agent_core --> spine_sessions["ctx.sessions"] bundle_agent_core --> spine_tools["ctx.tools + tool-bash"] bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"] + plugin_acp_plan_mode["plan-mode
@deepseek-ai/dsh-plan-mode"] + cfg --> plugin_acp_plan_mode + plugin_acp_tool_ask_user["tool-ask-user
@deepseek-ai/dsh-tool-ask-user"] + cfg --> plugin_acp_tool_ask_user plugin_acp_token_meter["token-meter
@deepseek-ai/dsh-token-meter"] cfg --> plugin_acp_token_meter plugin_acp_compact_basic["compact-basic
@deepseek-ai/dsh-compact-basic"] @@ -74,6 +78,8 @@ flowchart LR | `approval` | `@deepseek-ai/dsh-user-approval` | | `permission` | `@deepseek-ai/dsh-permission` | | `acp-agent` | `@deepseek-ai/dsh-acp-demo` | +| `plan-mode` | `@deepseek-ai/dsh-plan-mode` | +| `tool-ask-user` | `@deepseek-ai/dsh-tool-ask-user` | | `token-meter` | `@deepseek-ai/dsh-token-meter` | | `compact-basic` | `@deepseek-ai/dsh-compact-basic` | | `subagent` | `@deepseek-ai/dsh-subagent` | diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml index 4488b3ce23..4122867228 100644 --- a/examples/acp-agent/cordis.yml +++ b/examples/acp-agent/cordis.yml @@ -66,6 +66,29 @@ Verify your work by running the code or tests. Keep answers brief and factual. +# Plan mode is additive to the canonical ACP server. The ACP bridge projects +# it onto the protocol picker; sandbox and approval remain independent options. +- id: plan-mode + name: '@deepseek-ai/dsh-plan-mode' + config: + section: | + You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode. + + Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery. + + The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed only to keep the request shape stable. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode. + + Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out. + + Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions. + + When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation. + +# Blocking plan decisions and ordinary clarifications share ACP's elicitation +# provider through the model-facing question tool. +- id: tool-ask-user + name: '@deepseek-ai/dsh-tool-ask-user' + # Replay-aware request pressure; the routed adapter supplies model capacity. - id: token-meter name: '@deepseek-ai/dsh-token-meter' diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts index aab6aff2fa..b63017dac7 100644 --- a/examples/acp-agent/tests/acp.snapshot.ts +++ b/examples/acp-agent/tests/acp.snapshot.ts @@ -53,6 +53,20 @@ const SCENARIOS: Scenario[] = [ { name: 'reject-extra-dirs', hasModelTurn: false, recorded: false }, // Direct command dispatch reports goal state without spending a model turn. { name: 'goal-command-status', hasModelTurn: false, recorded: false }, + // Protocol-only (keyless, authored): session/new advertises the mode picker, + // session/set_mode acknowledges a valid selection, and an unknown mode id + // fails loudly. With no model turn, its membership in the plan header class + // is vacuous; the class still needs one explicit pin below. + { name: 'modes-advertise', hasModelTurn: false, recorded: false, headerClass: 'plan' }, + // The plan header pin covers the full arc: setMode(plan), a real read under + // the independently configured sandbox, plan review through exit_plan_mode, + // an approved boundary flip back to default, and a real edit in the next + // step. Leaving plan removes the policy section and exit tool, producing one + // changed request header. + { name: 'plan-mode', hasModelTurn: true, recorded: true, pinsHeader: true, headerClass: 'plan', expectedHeaderChanges: 1 }, + // Free-text review feedback returns as a corrective error and leaves the + // session in plan mode, so this scenario shares the pinned plan header. + { name: 'plan-mode-reject', hasModelTurn: true, recorded: true, headerClass: 'plan' }, // text-turn is the pinned-header scenario: the minimal single text turn. // Its prompt and tool-schema sidecars pin the composed header. { name: 'text-turn', hasModelTurn: true, recorded: true, pinsHeader: true }, diff --git a/examples/acp-agent/tests/goal-snapshots/goal-session/stdout.expected.jsonl b/examples/acp-agent/tests/goal-snapshots/goal-session/stdout.expected.jsonl index 809c9511a5..1b3c8688ed 100644 --- a/examples/acp-agent/tests/goal-snapshots/goal-session/stdout.expected.jsonl +++ b/examples/acp-agent/tests/goal-snapshots/goal-session/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Create a durable two-round goal","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_goal_create","title":"Create goal","kind":"other","status":"in_progress","rawInput":"Finish the ACP goal-session snapshot proof"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_goal_create","status":"completed","content":[{"type":"content","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\"}"}}]}}} diff --git a/examples/acp-agent/tests/plan-mode.e2e.ts b/examples/acp-agent/tests/plan-mode.e2e.ts new file mode 100644 index 0000000000..31cfc804f5 --- /dev/null +++ b/examples/acp-agent/tests/plan-mode.e2e.ts @@ -0,0 +1,84 @@ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { afterEach, describe, expect, it } from 'vitest' +import { + PROTOCOL_VERSION, + type CreateElicitationRequest, + type CreateElicitationResponse, +} from '@agentclientprotocol/sdk' +import { + launchAcpTestAgent, + type AgentUnderTest, + type LaunchedAcpTestAgent, +} from '@deepseek-ai/dsh-acp-snapshot' + +/** The shipped ACP leaf's plan mode exercised through its real subprocess entry. */ +const AGENT: AgentUnderTest = { + binScript: fileURLToPath(new URL('../../../packages/examples/acp-demo/src/bin.ts', import.meta.url)), + configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)), + tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)), +} + +let spawned: LaunchedAcpTestAgent | undefined +let workdir: string | undefined + +afterEach(async () => { + const ownedSpawned = spawned + const ownedWorkdir = workdir + spawned = undefined + workdir = undefined + try { + if (ownedSpawned !== undefined) { + await ownedSpawned.close('SIGKILL').catch((error: unknown) => { + throw new Error(`plan ACP cleanup failed; child stderr:\n${ownedSpawned.stderr()}`, { cause: error }) + }) + } + } finally { + if (ownedWorkdir !== undefined) await rm(ownedWorkdir, { recursive: true, force: true }) + } +}) + +describe.skipIf(!process.env.DEEPSEEK_API_KEY)('acp-agent plan mode e2e: approval gates implementation (real model)', () => { + it('keeps the file unchanged through review, then applies the approved plan', async () => { + workdir = await mkdtemp(join(tmpdir(), 'acp-plan-e2e-')) + const proofPath = join(workdir, 'proof.txt') + await writeFile(proofPath, 'BEFORE\n') + + const reviews: CreateElicitationRequest[] = [] + let contentAtReview: string | undefined + const createElicitation = async (request: CreateElicitationRequest): Promise => { + if (request.mode !== 'form' || request.requestedSchema.title !== 'Plan review') return { action: 'cancel' } + reviews.push(request) + contentAtReview = await readFile(proofPath, 'utf8') + return { action: 'accept', content: { choice: 'Approve' } } + } + + spawned = launchAcpTestAgent({ agent: AGENT, cwd: workdir, createElicitation }) + const { client, updates } = spawned + const rpc = async (stage: string, operation: Promise): Promise => operation.catch((error: unknown) => { + throw new Error(`plan ACP ${stage} failed; child stderr:\n${spawned?.stderr() ?? ''}`, { cause: error }) + }) + await rpc('initialize', client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })) + const created = await rpc('session/new', client.newSession({ cwd: workdir, mcpServers: [] })) + expect(created.modes?.availableModes.map(mode => mode.id)).toEqual(['default', 'plan']) + await rpc('session/set_mode', client.setSessionMode({ sessionId: created.sessionId, modeId: 'plan' })) + + const result = await rpc('prompt', client.prompt({ + sessionId: created.sessionId, + prompt: [{ + type: 'text', + text: 'Inspect proof.txt and plan the smallest change that replaces its contents with exactly AFTER followed by one newline. Present the complete plan through exit_plan_mode. After I approve it, implement the change with the filesystem tools, verify the exact file contents, and stop. Do not ask questions.', + }], + })) + + expect(['end_turn', 'max_tokens']).toContain(result.stopReason) + expect(reviews).toHaveLength(1) + expect(contentAtReview).toBe('BEFORE\n') + expect(await readFile(proofPath, 'utf8')).toBe('AFTER\n') + expect(updates + .filter(update => update.sessionUpdate === 'current_mode_update') + .map(update => update.currentModeId)).toEqual(['plan', 'default']) + }, 240_000) +}) diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/stdout.expected.jsonl index 52f0bd6fc8..c53a311f32 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Run this advanced flow exactly","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"advanced-mount","title":"Mount plugin into live cordis runtime","kind":"execute","status":"in_progress","rawInput":{"code":"return { name: 'snapshot-marker', apply() {} }"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"advanced-mount","status":"completed","content":[{"type":"content","content":{"type":"text","text":"mounted dyn-1 (plugin \"snapshot-marker\", state: active)"}}]}}} diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md index e5773c1319..2b46661722 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md @@ -37,6 +37,27 @@ The available tools: ```ts declare const tools: { + /** Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer. */ + ask_user_question(args: { + /** Questions to ask the user before continuing. */ + questions: { + /** Stable id for this question; echoed in the answer. */ + id: string; + /** The specific question to ask the user. */ + question: string; + /** Optional short heading for the question, such as "Confirm" or "Choose Mode". */ + header?: string; + /** Optional choices to show the user. If you recommend one, put it first and append "(Recommended)" to that label. */ + options?: { + /** Short user-facing option label. */ + label: string; + /** One sentence explaining the tradeoff or impact. */ + description?: string; + }[]; + /** Whether the user may select more than one option. Defaults to false. */ + multi_select?: boolean; + }[]; + }): Promise; /** 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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later. */ bash(args: { /** The bash command to execute. */ @@ -93,6 +114,11 @@ declare const tools: { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ justification?: string; }): Promise; + /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ + exit_plan_mode(args: { + /** The complete plan, as markdown, starting with a # heading that names it. */ + plan: string; + }): Promise; /** Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal. */ get_goal(args: Record): Promise; /** 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. */ diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json index 71597bc9c3..4b3868a31d 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json @@ -1,5 +1,66 @@ { "initial": [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, { "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", @@ -164,6 +225,22 @@ ] } }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, { "name": "get_goal", "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", diff --git a/examples/acp-agent/tests/snapshots/bash-spill/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/bash-spill/stdout.expected.jsonl index 888df14a4b..7323919b28 100644 --- a/examples/acp-agent/tests/snapshots/bash-spill/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/bash-spill/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the bash tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_spill","title":"node -e \"process.stdout.write('SPILL_START-' + 'x'.repeat(2000) + '-SPILL_END')\"","kind":"execute","status":"in_progress","rawInput":"node -e \"process.stdout.write('SPILL_START-' + 'x'.repeat(2000) + '-SPILL_END')\"","content":[{"type":"content","content":{"type":"text","text":"Print large deterministic output"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_spill","status":"completed","content":[{"type":"content","content":{"type":"text","text":"```console\nSPILL_START-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-SPILL_END\n\n(Omitted 1417 bytes. Full formatted result stored at: {{spillLocator:bash.txt}}. Use read with offset/limit, or grep this path to search within it.)\n```"}}]}}} diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/both-mode-turn/stdout.expected.jsonl index 5cbeaad1e9..7184499d06 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Call the run_code tool (NOT","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md index ac1114d0a3..8868412707 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md @@ -37,6 +37,27 @@ The available tools: ```ts declare const tools: { + /** Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer. */ + ask_user_question(args: { + /** Questions to ask the user before continuing. */ + questions: { + /** Stable id for this question; echoed in the answer. */ + id: string; + /** The specific question to ask the user. */ + question: string; + /** Optional short heading for the question, such as "Confirm" or "Choose Mode". */ + header?: string; + /** Optional choices to show the user. If you recommend one, put it first and append "(Recommended)" to that label. */ + options?: { + /** Short user-facing option label. */ + label: string; + /** One sentence explaining the tradeoff or impact. */ + description?: string; + }[]; + /** Whether the user may select more than one option. Defaults to false. */ + multi_select?: boolean; + }[]; + }): Promise; /** 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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later. */ bash(args: { /** The bash command to execute. */ @@ -76,6 +97,11 @@ declare const tools: { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ justification?: string; }): Promise; + /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ + exit_plan_mode(args: { + /** The complete plan, as markdown, starting with a # heading that names it. */ + plan: string; + }): Promise; /** Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal. */ get_goal(args: Record): Promise; /** 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. */ diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json index ab52ec415d..79cb046a40 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json @@ -1,5 +1,66 @@ { "initial": [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, { "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", @@ -107,6 +168,22 @@ ] } }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, { "name": "get_goal", "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", diff --git a/examples/acp-agent/tests/snapshots/cancel-tool-calls/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/cancel-tool-calls/stdout.expected.jsonl index 5c30d144c0..63d5bc0927 100644 --- a/examples/acp-agent/tests/snapshots/cancel-tool-calls/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/cancel-tool-calls/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Run two shell commands: wait","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_wait","title":"node -e \"setInterval(() => {}, 1000)\"","kind":"execute","status":"in_progress","rawInput":"node -e \"setInterval(() => {}, 1000)\"","content":[{"type":"content","content":{"type":"text","text":"Wait until cancellation"}}]}}} {"jsonrpc":"2.0","id":3,"result":{"stopReason":"cancelled"}} diff --git a/examples/acp-agent/tests/snapshots/cancel/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/cancel/stdout.expected.jsonl index bb775c6c90..adc8493618 100644 --- a/examples/acp-agent/tests/snapshots/cancel/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/cancel/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Start a long task; this","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"partial"}}}} {"jsonrpc":"2.0","id":3,"result":{"stopReason":"cancelled"}} diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/code-mode-turn/stdout.expected.jsonl index d30932e4f2..65c5350091 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-turn/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/code-mode-turn/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Using ONE run_code program: call","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md index ac1114d0a3..8868412707 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md @@ -37,6 +37,27 @@ The available tools: ```ts declare const tools: { + /** Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer. */ + ask_user_question(args: { + /** Questions to ask the user before continuing. */ + questions: { + /** Stable id for this question; echoed in the answer. */ + id: string; + /** The specific question to ask the user. */ + question: string; + /** Optional short heading for the question, such as "Confirm" or "Choose Mode". */ + header?: string; + /** Optional choices to show the user. If you recommend one, put it first and append "(Recommended)" to that label. */ + options?: { + /** Short user-facing option label. */ + label: string; + /** One sentence explaining the tradeoff or impact. */ + description?: string; + }[]; + /** Whether the user may select more than one option. Defaults to false. */ + multi_select?: boolean; + }[]; + }): Promise; /** 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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later. */ bash(args: { /** The bash command to execute. */ @@ -76,6 +97,11 @@ declare const tools: { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ justification?: string; }): Promise; + /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ + exit_plan_mode(args: { + /** The complete plan, as markdown, starting with a # heading that names it. */ + plan: string; + }): Promise; /** Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal. */ get_goal(args: Record): Promise; /** 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. */ diff --git a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/stdout.expected.jsonl index 36aee81d82..579f2a19cc 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Using ONE run_code program, call","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md index ac1114d0a3..8868412707 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/system-prompt.expected.md @@ -37,6 +37,27 @@ The available tools: ```ts declare const tools: { + /** Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer. */ + ask_user_question(args: { + /** Questions to ask the user before continuing. */ + questions: { + /** Stable id for this question; echoed in the answer. */ + id: string; + /** The specific question to ask the user. */ + question: string; + /** Optional short heading for the question, such as "Confirm" or "Choose Mode". */ + header?: string; + /** Optional choices to show the user. If you recommend one, put it first and append "(Recommended)" to that label. */ + options?: { + /** Short user-facing option label. */ + label: string; + /** One sentence explaining the tradeoff or impact. */ + description?: string; + }[]; + /** Whether the user may select more than one option. Defaults to false. */ + multi_select?: boolean; + }[]; + }): Promise; /** 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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later. */ bash(args: { /** The bash command to execute. */ @@ -76,6 +97,11 @@ declare const tools: { /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ justification?: string; }): Promise; + /** Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again. */ + exit_plan_mode(args: { + /** The complete plan, as markdown, starting with a # heading that names it. */ + plan: string; + }): Promise; /** Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal. */ get_goal(args: Record): Promise; /** 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. */ diff --git a/examples/acp-agent/tests/snapshots/config-options/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/config-options/stdout.expected.jsonl index eddfde0332..f8d8228f06 100644 --- a/examples/acp-agent/tests/snapshots/config-options/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/config-options/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","id":3,"result":{"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"workspace-write","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} {"jsonrpc":"2.0","id":4,"result":{"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} {"jsonrpc":"2.0","id":5,"error":{"code":-32602,"message":"Invalid params: unknown permission value \"plan\""}} 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 eb005ad42d..b58a707bce 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 @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[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 - the tool schema, execution, and optional presentation functions.\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, and final\n * notification. Tool and listener failures resolve as materialized error\n * results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is\n * the same lossless, frozen 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(content: ContentBlock[], options?: SendOptions): void;\n steer(content: ContentBlock[], options?: SendOptions): void;\n inject(content: ContentBlock[], options?: InjectOptions): void;\n cancel(cause?: AgentCancelCause): void;\n whenIdle(): Promise;\n }\n export type AgentCancelCause = {\n readonly kind: 'user';\n } | {\n readonly kind: 'parent';\n };\n export interface AgentOptions {\n provider?: string;\n model?: string;\n }\n export type AgentStatus = 'idle' | 'running' | 'disposed';\n export type Branded = string & {\n readonly [BRAND]: B;\n };\n export type CallId = Branded<'CallId'>;\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 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 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 HookContext {\n content: ContentBlock[];\n source: MessageSource;\n meta?: JsonValue;\n }\n export interface InjectOptions extends SendOptions {\n meta?: JsonValue;\n }\n export type JsonValue = null | boolean | number | string | JsonValue[] | {\n [key: string]: JsonValue;\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 ScopeKey = object;\n export interface SendOptions {\n source?: MessageSource;\n }\n export type SessionId = Branded<'SessionId'>;\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 type ToolCallKind = 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other';\n export type ToolCallView = GenericCallView | TerminalCallView | DiffCallView;\n export interface ToolDefinition extends ToolSchema {\n execute(args: unknown, exec: ToolRunContext): Promise;\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 type ToolExecuteReturn = ContentBlock[] | {\n content: ContentBlock[];\n meta?: unknown;\n };\n export interface ToolExecution extends ToolExecutionInput {\n readonly token: ToolExecutionToken;\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 interface ToolExecutionResult {\n content: ContentBlock[];\n isError: boolean;\n error?: ToolErrorInfo;\n additionalContexts?: HookContext[];\n meta?: unknown;\n }\n export type ToolExecutionToken = symbol & {\n readonly [toolExecutionTokenBrand]: true;\n };\n export type ToolGuard = (execution: Readonly) => string | undefined;\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?: unknown;\n }\n export type ToolResultView = GenericResultView | TerminalResultView | DiffResultView;\n export interface ToolRunContext extends ToolExecution {\n deferContext(context: HookContext): void;\n }\n export interface ToolSchema {\n name: string;\n description: string;\n parameters: Record;\n }"}}]}}} diff --git a/examples/acp-agent/tests/snapshots/error-finish/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/error-finish/stdout.expected.jsonl index fcc39c0637..1019c9057c 100644 --- a/examples/acp-agent/tests/snapshots/error-finish/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/error-finish/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"This prompt triggers a recorded","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"\n\n[Model attempt failed; any partial output above is discarded: simulated provider error (HTTP 401)]\n\n"}}}} {"jsonrpc":"2.0","id":3,"error":{"code":-32603,"message":"Internal error: turn failed: simulated provider error (HTTP 401)"}} diff --git a/examples/acp-agent/tests/snapshots/escalation-approved/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/escalation-approved/stdout.expected.jsonl index 09a23db100..a2ad0a6116 100644 --- a/examples/acp-agent/tests/snapshots/escalation-approved/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/escalation-approved/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","id":3,"result":{"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"workspace-write","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"The sandbox already denied writing","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} diff --git a/examples/acp-agent/tests/snapshots/escalation-rejected/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/escalation-rejected/stdout.expected.jsonl index 0446d46f75..3f7eff9b32 100644 --- a/examples/acp-agent/tests/snapshots/escalation-rejected/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/escalation-rejected/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","id":3,"result":{"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"workspace-write","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"The sandbox already denied writing","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} diff --git a/examples/acp-agent/tests/snapshots/fs-edit/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/fs-edit/stdout.expected.jsonl index c6ebeb6a8b..5eb3211ffb 100644 --- a/examples/acp-agent/tests/snapshots/fs-edit/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-edit/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"First use the read tool","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/fs-escalation-approved/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/fs-escalation-approved/stdout.expected.jsonl index 79c5214374..12b5b11c54 100644 --- a/examples/acp-agent/tests/snapshots/fs-escalation-approved/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-escalation-approved/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","id":3,"result":{"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"workspace-write","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the write tool (NOT","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} diff --git a/examples/acp-agent/tests/snapshots/fs-policy-reject/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/fs-policy-reject/stdout.expected.jsonl index 3d29cd8420..8263434a3e 100644 --- a/examples/acp-agent/tests/snapshots/fs-policy-reject/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-policy-reject/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Do NOT use the read","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/fs-read-window/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/fs-read-window/stdout.expected.jsonl index 5a457efde1..851252a7f3 100644 --- a/examples/acp-agent/tests/snapshots/fs-read-window/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-read-window/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the read tool (NOT","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/fs-read/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/fs-read/stdout.expected.jsonl index c35d6a3981..5a504a69d1 100644 --- a/examples/acp-agent/tests/snapshots/fs-read/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-read/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the read tool (NOT","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/fs-terminal-card/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/fs-terminal-card/stdout.expected.jsonl index 9e3690ca5f..fc1182e7d1 100644 --- a/examples/acp-agent/tests/snapshots/fs-terminal-card/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-terminal-card/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the bash tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/fs-write-overwrite/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/fs-write-overwrite/stdout.expected.jsonl index 1562dacc70..849ff51af1 100644 --- a/examples/acp-agent/tests/snapshots/fs-write-overwrite/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-write-overwrite/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"First use the read tool","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/fs-write/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/fs-write/stdout.expected.jsonl index a7a81496be..475f437a44 100644 --- a/examples/acp-agent/tests/snapshots/fs-write/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-write/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the write tool (NOT","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/goal-command-status/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/goal-command-status/stdout.expected.jsonl index 2ea5b1c29a..86ef26050b 100644 --- a/examples/acp-agent/tests/snapshots/goal-command-status/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/goal-command-status/stdout.expected.jsonl @@ -1,5 +1,5 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"No goal is currently set.\nUsage: /goal [|clear|edit |pause|resume]"}}}} {"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}} diff --git a/examples/acp-agent/tests/snapshots/handshake/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/handshake/stdout.expected.jsonl index e87bb6fec2..3320197421 100644 --- a/examples/acp-agent/tests/snapshots/handshake/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/handshake/stdout.expected.jsonl @@ -1,3 +1,3 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/stdout.expected.jsonl index aeabe98594..db89f8db97 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Call the bash tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/stdout.expected.jsonl index b4bbb1be13..827dc55637 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the bash tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/stdout.expected.jsonl index 30fba24fbd..2a3d8294b9 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the bash tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/stdout.expected.jsonl index b25efc21cd..bd569e474c 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the bash tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/stdout.expected.jsonl index aa2ff437c6..ad584a4ec3 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/stdout.expected.jsonl @@ -1,4 +1,4 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","id":3,"result":{"stopReason":"cancelled"}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/stdout.expected.jsonl index 2b5a3f74c8..8186d5bbb2 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"What is my favorite color?","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/stdout.expected.jsonl index b2d0d6e636..5d67d704e4 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Reply with the single word","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/stdout.expected.jsonl index 64922afe05..5806fbf699 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Call the bash tool exactly","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/stdout.expected.jsonl index 686b729e2c..f79581043a 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the bash tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/stdout.expected.jsonl index 602eee1837..6ab55160f8 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the bash tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/stdout.expected.jsonl index aa2ff437c6..ad584a4ec3 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/stdout.expected.jsonl @@ -1,4 +1,4 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","id":3,"result":{"stopReason":"cancelled"}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/stdout.expected.jsonl index 4249a4ba04..ee8725ddaa 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"What is my favorite color?","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/stdout.expected.jsonl index bcc1765b96..a7642218a4 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Reply with the single word","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/lsp-definition/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/lsp-definition/stdout.expected.jsonl index a993eff7c2..01f7b9d190 100644 --- a/examples/acp-agent/tests/snapshots/lsp-definition/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/lsp-definition/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the lsp tool exactly","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_lsp_definition","title":"LSP goToDefinition subject.ts:1:7","kind":"search","status":"in_progress","locations":[{"path":"subject.ts","line":1}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_lsp_definition","status":"completed","content":[{"type":"content","content":{"type":"text","text":"subject.ts:1:7\n… 1 more location omitted (limit 1)."}}]}}} diff --git a/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json index 4fa5010b72..9839668511 100644 --- a/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/lsp-definition/tool-schemas.expected.json @@ -1,5 +1,66 @@ { "initial": [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, { "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", @@ -107,6 +168,22 @@ ] } }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, { "name": "get_goal", "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", diff --git a/examples/acp-agent/tests/snapshots/model-switching/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/model-switching/stdout.expected.jsonl index e7d166b2c9..265df8545c 100644 --- a/examples/acp-agent/tests/snapshots/model-switching/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/model-switching/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Without using tools, reply with","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/model-switching/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/model-switching/tool-schemas.expected.json index 1bfe74b704..85acdd1ee1 100644 --- a/examples/acp-agent/tests/snapshots/model-switching/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/model-switching/tool-schemas.expected.json @@ -1,5 +1,66 @@ { "initial": [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, { "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", @@ -107,6 +168,22 @@ ] } }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, { "name": "get_goal", "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", @@ -467,6 +544,67 @@ ], "changes": [ [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, { "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", @@ -574,6 +712,22 @@ ] } }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, { "name": "get_goal", "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", diff --git a/examples/acp-agent/tests/snapshots/modes-advertise/input.json b/examples/acp-agent/tests/snapshots/modes-advertise/input.json new file mode 100644 index 0000000000..26c56b3425 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/modes-advertise/input.json @@ -0,0 +1,22 @@ +{ + "steps": [ + { + "op": "initialize" + }, + { + "op": "newSession" + }, + { + "op": "setMode", + "modeId": "plan" + }, + { + "op": "setMode", + "modeId": "default" + }, + { + "op": "setModeExpectError", + "modeId": "yolo" + } + ] +} diff --git a/examples/acp-agent/tests/snapshots/modes-advertise/session.jsonl b/examples/acp-agent/tests/snapshots/modes-advertise/session.jsonl new file mode 100644 index 0000000000..a6f73319bc --- /dev/null +++ b/examples/acp-agent/tests/snapshots/modes-advertise/session.jsonl @@ -0,0 +1 @@ +{"type":"session","version":0,"id":"00000000-0000-0000-0000-000000000000","createdAt":0} diff --git a/examples/acp-agent/tests/snapshots/modes-advertise/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/modes-advertise/stdout.expected.jsonl new file mode 100644 index 0000000000..c43a19bfd3 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/modes-advertise/stdout.expected.jsonl @@ -0,0 +1,8 @@ +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"current_mode_update","currentModeId":"plan"}}} +{"jsonrpc":"2.0","id":3,"result":{}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"current_mode_update","currentModeId":"default"}}} +{"jsonrpc":"2.0","id":4,"result":{}} +{"jsonrpc":"2.0","id":5,"error":{"code":-32602,"message":"Invalid params: unknown session mode \"yolo\" — available modes: default, plan"}} diff --git a/examples/acp-agent/tests/snapshots/multi-turn/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/multi-turn/stdout.expected.jsonl index cdc441a921..2151071ed4 100644 --- a/examples/acp-agent/tests/snapshots/multi-turn/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/multi-turn/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Reply with exactly the word:","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/parallel-tool-calls/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/parallel-tool-calls/stdout.expected.jsonl index 750f1726c8..600458b67a 100644 --- a/examples/acp-agent/tests/snapshots/parallel-tool-calls/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/parallel-tool-calls/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the read tool twice","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_read_a","title":"Read a.txt","kind":"read","status":"in_progress","locations":[{"path":"a.txt","line":1}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_read_b","title":"Read b.txt","kind":"read","status":"in_progress","locations":[{"path":"b.txt","line":1}]}}} diff --git a/examples/acp-agent/tests/snapshots/permission-switching/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/permission-switching/stdout.expected.jsonl index d132e3759e..fd78fe6dc2 100644 --- a/examples/acp-agent/tests/snapshots/permission-switching/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/permission-switching/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","id":3,"result":{"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"workspace-write","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the bash tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} diff --git a/examples/acp-agent/tests/snapshots/permission-switching/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/permission-switching/tool-schemas.expected.json index 1bfe74b704..85acdd1ee1 100644 --- a/examples/acp-agent/tests/snapshots/permission-switching/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/permission-switching/tool-schemas.expected.json @@ -1,5 +1,66 @@ { "initial": [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, { "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", @@ -107,6 +168,22 @@ ] } }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, { "name": "get_goal", "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", @@ -467,6 +544,67 @@ ], "changes": [ [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, { "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", @@ -574,6 +712,22 @@ ] } }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, { "name": "get_goal", "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", diff --git a/examples/acp-agent/tests/snapshots/plan-mode-reject/input.json b/examples/acp-agent/tests/snapshots/plan-mode-reject/input.json new file mode 100644 index 0000000000..07c135a87f --- /dev/null +++ b/examples/acp-agent/tests/snapshots/plan-mode-reject/input.json @@ -0,0 +1,24 @@ +{ + "steps": [ + { + "op": "initialize" + }, + { + "op": "newSession" + }, + { + "op": "setMode", + "modeId": "plan" + }, + { + "op": "prompt", + "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." + } + ], + "elicitationAnswers": [ + { + "action": "accept", + "custom": "Also add a verification step that re-reads the file after the fix." + } + ] +} diff --git a/examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl b/examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl new file mode 100644 index 0000000000..cad0f9cfb4 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/plan-mode-reject/session.jsonl @@ -0,0 +1,425 @@ +{"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":"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"}}} +{"type":"assistant/chunk","seq":7,"time":1784525385352,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":8,"time":1784525385471,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":9,"time":1784525385503,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":10,"time":1784525385503,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":11,"time":1784525385504,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":12,"time":1784525385504,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" read"}}} +{"type":"assistant/chunk","seq":13,"time":1784525385533,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":14,"time":1784525385534,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":15,"time":1784525385564,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" notes"}}} +{"type":"assistant/chunk","seq":16,"time":1784525385565,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".txt"}}} +{"type":"assistant/chunk","seq":17,"time":1784525385565,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" using"}}} +{"type":"assistant/chunk","seq":18,"time":1784525385595,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":19,"time":1784525385596,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" relative"}}} +{"type":"assistant/chunk","seq":20,"time":1784525385597,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" path"}}} +{"type":"assistant/chunk","seq":21,"time":1784525385597,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":22,"time":1784525385597,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} +{"type":"assistant/chunk","seq":23,"time":1784525385597,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" present"}}} +{"type":"assistant/chunk","seq":24,"time":1784525385626,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":25,"time":1784525385626,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" plan"}}} +{"type":"assistant/chunk","seq":26,"time":1784525385626,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" via"}}} +{"type":"assistant/chunk","seq":27,"time":1784525385657,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exit"}}} +{"type":"assistant/chunk","seq":28,"time":1784525385658,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_"}}} +{"type":"assistant/chunk","seq":29,"time":1784525385658,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"plan"}}} +{"type":"assistant/chunk","seq":30,"time":1784525385658,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_mode"}}} +{"type":"assistant/chunk","seq":31,"time":1784525385658,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":32,"time":1784525385658,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":33,"time":1784525385688,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":34,"time":1784525385689,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" start"}}} +{"type":"assistant/chunk","seq":35,"time":1784525385719,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" by"}}} +{"type":"assistant/chunk","seq":36,"time":1784525385719,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reading"}}} +{"type":"assistant/chunk","seq":37,"time":1784525385720,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":38,"time":1784525385720,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":39,"time":1784525385720,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":40,"time":1784525385812,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":41,"time":1784525385813,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":42,"time":1784525385843,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":43,"time":1784525385844,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":44,"time":1784525385844,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":"file"}}} +{"type":"assistant/chunk","seq":45,"time":1784525385844,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":"_path"}}} +{"type":"assistant/chunk","seq":46,"time":1784525385844,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":47,"time":1784525385844,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":48,"time":1784525385874,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":49,"time":1784525385875,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":"notes"}}} +{"type":"assistant/chunk","seq":50,"time":1784525385875,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":51,"time":1784525385906,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":52,"time":1784525385906,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":53,"time":1784525385972,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to read the file notes.txt using the relative path, then present a plan via exit_plan_mode. Let me start by reading the file."}}}} +{"type":"assistant/chunk","seq":54,"time":1784525385972,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","arguments":"{\"file_path\": \"notes.txt\"}"}}}} +{"type":"assistant/chunk","seq":55,"time":1784525385972,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3171,"outputTokens":78,"cacheReadTokens":0,"reasoningTokens":33}}}} +{"type":"assistant/chunk","seq":56,"time":1784525385972,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":57,"time":1784525385977,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to read the file notes.txt using the relative path, then present a plan via exit_plan_mode. Let me start by reading the file."},{"type":"tool-call","id":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","arguments":"{\"file_path\": \"notes.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3171,"outputTokens":78,"cacheReadTokens":0,"reasoningTokens":33}},"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],"surfaceOp":"append"} +{"type":"tool/call","seq":58,"time":1784525385977,"data":{"turn":1,"step":1,"callId":"call_00_Tez1Df1H9v8RqxqYIs5o6697","name":"read","arguments":"{\"file_path\": \"notes.txt\"}"}} +{"type":"tool/result","seq":59,"time":1784525385985,"data":{"turn":1,"step":1,"callId":"call_00_Tez1Df1H9v8RqxqYIs5o6697","content":[{"type":"text","text":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-DTr6Ra/notes.txt\nfile\n\n1: project notes\n2: - the greeting message still says \"helo wrld\"\n\n(End of file - total 2 lines)\n"}],"isError":false},"sourceEventSeqs":[58],"surfaceOp":"append"} +{"type":"step/end","seq":60,"time":1784525385986,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":61,"time":1784525385987,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":62,"time":1784525386578,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":63,"time":1784525386579,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":64,"time":1784525386701,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":65,"time":1784525386729,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" contains"}}} +{"type":"assistant/chunk","seq":66,"time":1784525386730,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":67,"time":1784525386730,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":68,"time":1784525386760,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" note"}}} +{"type":"assistant/chunk","seq":69,"time":1784525386787,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} +{"type":"assistant/chunk","seq":70,"time":1784525386787,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":71,"time":1784525386816,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"the"}}} +{"type":"assistant/chunk","seq":72,"time":1784525386817,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" greeting"}}} +{"type":"assistant/chunk","seq":73,"time":1784525386817,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" message"}}} +{"type":"assistant/chunk","seq":74,"time":1784525386817,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" still"}}} +{"type":"assistant/chunk","seq":75,"time":1784525386817,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" says"}}} +{"type":"assistant/chunk","seq":76,"time":1784525386818,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" '"}}} +{"type":"assistant/chunk","seq":77,"time":1784525386845,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"hel"}}} +{"type":"assistant/chunk","seq":78,"time":1784525386845,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"o"}}} +{"type":"assistant/chunk","seq":79,"time":1784525386846,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wr"}}} +{"type":"assistant/chunk","seq":80,"time":1784525386846,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ld"}}} +{"type":"assistant/chunk","seq":81,"time":1784525386846,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"'\"."}}} +{"type":"assistant/chunk","seq":82,"time":1784525386846,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} +{"type":"assistant/chunk","seq":83,"time":1784525386872,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":84,"time":1784525386873,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":85,"time":1784525386873,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":86,"time":1784525386902,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" plan"}}} +{"type":"assistant/chunk","seq":87,"time":1784525386902,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" titled"}}} +{"type":"assistant/chunk","seq":88,"time":1784525386902,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \"#"}}} +{"type":"assistant/chunk","seq":89,"time":1784525386930,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Fix"}}} +{"type":"assistant/chunk","seq":90,"time":1784525386930,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":91,"time":1784525386931,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" greeting"}}} +{"type":"assistant/chunk","seq":92,"time":1784525386959,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" typ"}}} +{"type":"assistant/chunk","seq":93,"time":1784525386959,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"o"}}} +{"type":"assistant/chunk","seq":94,"time":1784525386959,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":95,"time":1784525386959,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":96,"time":1784525386959,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":97,"time":1784525386959,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" present"}}} +{"type":"assistant/chunk","seq":98,"time":1784525386987,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":99,"time":1784525386988,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" simple"}}} +{"type":"assistant/chunk","seq":100,"time":1784525387016,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" plan"}}} +{"type":"assistant/chunk","seq":101,"time":1784525387017,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":102,"time":1784525387017,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" fix"}}} +{"type":"assistant/chunk","seq":103,"time":1784525387046,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" this"}}} +{"type":"assistant/chunk","seq":104,"time":1784525387047,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" typ"}}} +{"type":"assistant/chunk","seq":105,"time":1784525387047,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"o"}}} +{"type":"assistant/chunk","seq":106,"time":1784525387047,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":107,"time":1784525387160,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":108,"time":1784525387160,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":109,"time":1784525387188,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":110,"time":1784525387189,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":111,"time":1784525387189,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"plan"}}} +{"type":"assistant/chunk","seq":112,"time":1784525387189,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":113,"time":1784525387189,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":114,"time":1784525387217,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":115,"time":1784525387217,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"#"}}} +{"type":"assistant/chunk","seq":116,"time":1784525387217,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" Fix"}}} +{"type":"assistant/chunk","seq":117,"time":1784525387217,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":118,"time":1784525387245,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" greeting"}}} +{"type":"assistant/chunk","seq":119,"time":1784525387246,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" typ"}}} +{"type":"assistant/chunk","seq":120,"time":1784525387246,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"o"}}} +{"type":"assistant/chunk","seq":121,"time":1784525387246,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"\\n\\n"}}} +{"type":"assistant/chunk","seq":122,"time":1784525387246,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"**"}}} +{"type":"assistant/chunk","seq":123,"time":1784525387273,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"Issue"}}} +{"type":"assistant/chunk","seq":124,"time":1784525387302,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":":**"}}} +{"type":"assistant/chunk","seq":125,"time":1784525387303,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" The"}}} +{"type":"assistant/chunk","seq":126,"time":1784525387303,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" greeting"}}} +{"type":"assistant/chunk","seq":127,"time":1784525387332,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" message"}}} +{"type":"assistant/chunk","seq":128,"time":1784525387333,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" currently"}}} +{"type":"assistant/chunk","seq":129,"time":1784525387360,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" displays"}}} +{"type":"assistant/chunk","seq":130,"time":1784525387389,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":131,"time":1784525387389,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"hel"}}} +{"type":"assistant/chunk","seq":132,"time":1784525387389,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"o"}}} +{"type":"assistant/chunk","seq":133,"time":1784525387389,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" wr"}}} +{"type":"assistant/chunk","seq":134,"time":1784525387389,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"ld"}}} +{"type":"assistant/chunk","seq":135,"time":1784525387417,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":136,"time":1784525387417,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" instead"}}} +{"type":"assistant/chunk","seq":137,"time":1784525387418,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" of"}}} +{"type":"assistant/chunk","seq":138,"time":1784525387418,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":139,"time":1784525387446,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" correct"}}} +{"type":"assistant/chunk","seq":140,"time":1784525387447,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":141,"time":1784525387447,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"Hello"}}} +{"type":"assistant/chunk","seq":142,"time":1784525387447,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" World"}}} +{"type":"assistant/chunk","seq":143,"time":1784525387476,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"\\\".\\n\\n"}}} +{"type":"assistant/chunk","seq":144,"time":1784525387476,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"**"}}} +{"type":"assistant/chunk","seq":145,"time":1784525387476,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"Plan"}}} +{"type":"assistant/chunk","seq":146,"time":1784525387503,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":":**\\n\\n"}}} +{"type":"assistant/chunk","seq":147,"time":1784525387504,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"1"}}} +{"type":"assistant/chunk","seq":148,"time":1784525387504,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"."}}} +{"type":"assistant/chunk","seq":149,"time":1784525387504,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" **"}}} +{"type":"assistant/chunk","seq":150,"time":1784525387504,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"Find"}}} +{"type":"assistant/chunk","seq":151,"time":1784525387533,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":152,"time":1784525387534,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" source"}}} +{"type":"assistant/chunk","seq":153,"time":1784525387561,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" file"}}} +{"type":"assistant/chunk","seq":154,"time":1784525387562,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"**"}}} +{"type":"assistant/chunk","seq":155,"time":1784525387589,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" —"}}} +{"type":"assistant/chunk","seq":156,"time":1784525387590,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" Loc"}}} +{"type":"assistant/chunk","seq":157,"time":1784525387619,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"ate"}}} +{"type":"assistant/chunk","seq":158,"time":1784525387619,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":159,"time":1784525387647,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" file"}}} +{"type":"assistant/chunk","seq":160,"time":1784525387647,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" that"}}} +{"type":"assistant/chunk","seq":161,"time":1784525387675,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" contains"}}} +{"type":"assistant/chunk","seq":162,"time":1784525387676,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":163,"time":1784525387676,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" greeting"}}} +{"type":"assistant/chunk","seq":164,"time":1784525387706,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" string"}}} +{"type":"assistant/chunk","seq":165,"time":1784525387707,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":166,"time":1784525387736,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"hel"}}} +{"type":"assistant/chunk","seq":167,"time":1784525387737,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"o"}}} +{"type":"assistant/chunk","seq":168,"time":1784525387737,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" wr"}}} +{"type":"assistant/chunk","seq":169,"time":1784525387737,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"ld"}}} +{"type":"assistant/chunk","seq":170,"time":1784525387737,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"\\\".\\n"}}} +{"type":"assistant/chunk","seq":171,"time":1784525387764,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"2"}}} +{"type":"assistant/chunk","seq":172,"time":1784525387764,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"."}}} +{"type":"assistant/chunk","seq":173,"time":1784525387764,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" **"}}} +{"type":"assistant/chunk","seq":174,"time":1784525387765,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"Fix"}}} +{"type":"assistant/chunk","seq":175,"time":1784525387796,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":176,"time":1784525387796,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" typ"}}} +{"type":"assistant/chunk","seq":177,"time":1784525387796,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"o"}}} +{"type":"assistant/chunk","seq":178,"time":1784525387796,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"**"}}} +{"type":"assistant/chunk","seq":179,"time":1784525387796,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" —"}}} +{"type":"assistant/chunk","seq":180,"time":1784525387825,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" Replace"}}} +{"type":"assistant/chunk","seq":181,"time":1784525387826,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":182,"time":1784525387826,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"hel"}}} +{"type":"assistant/chunk","seq":183,"time":1784525387854,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"o"}}} +{"type":"assistant/chunk","seq":184,"time":1784525387854,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" wr"}}} +{"type":"assistant/chunk","seq":185,"time":1784525387854,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"ld"}}} +{"type":"assistant/chunk","seq":186,"time":1784525387855,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":187,"time":1784525387855,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" with"}}} +{"type":"assistant/chunk","seq":188,"time":1784525387855,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":189,"time":1784525387884,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"Hello"}}} +{"type":"assistant/chunk","seq":190,"time":1784525387884,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" World"}}} +{"type":"assistant/chunk","seq":191,"time":1784525387884,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":192,"time":1784525387884,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" in"}}} +{"type":"assistant/chunk","seq":193,"time":1784525387884,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" that"}}} +{"type":"assistant/chunk","seq":194,"time":1784525387911,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" source"}}} +{"type":"assistant/chunk","seq":195,"time":1784525387940,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" file"}}} +{"type":"assistant/chunk","seq":196,"time":1784525387940,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":".\\n"}}} +{"type":"assistant/chunk","seq":197,"time":1784525387940,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"3"}}} +{"type":"assistant/chunk","seq":198,"time":1784525387940,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"."}}} +{"type":"assistant/chunk","seq":199,"time":1784525387940,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" **"}}} +{"type":"assistant/chunk","seq":200,"time":1784525387940,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"Verify"}}} +{"type":"assistant/chunk","seq":201,"time":1784525387968,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"**"}}} +{"type":"assistant/chunk","seq":202,"time":1784525387968,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" —"}}} +{"type":"assistant/chunk","seq":203,"time":1784525387969,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" Confirm"}}} +{"type":"assistant/chunk","seq":204,"time":1784525388000,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":205,"time":1784525388000,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" fix"}}} +{"type":"assistant/chunk","seq":206,"time":1784525388028,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" is"}}} +{"type":"assistant/chunk","seq":207,"time":1784525388029,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" correct"}}} +{"type":"assistant/chunk","seq":208,"time":1784525388057,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" by"}}} +{"type":"assistant/chunk","seq":209,"time":1784525388058,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" reading"}}} +{"type":"assistant/chunk","seq":210,"time":1784525388105,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":211,"time":1784525388105,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" changed"}}} +{"type":"assistant/chunk","seq":212,"time":1784525388117,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":" file"}}} +{"type":"assistant/chunk","seq":213,"time":1784525388117,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"."}}} +{"type":"assistant/chunk","seq":214,"time":1784525388117,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":215,"time":1784525388146,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":216,"time":1784525388177,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The file contains a single note: \"the greeting message still says 'helo wrld'\". The user wants a plan titled \"# Fix the greeting typo\". Let me present a simple plan to fix this typo."}}}} +{"type":"assistant/chunk","seq":217,"time":1784525388177,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","arguments":"{\"plan\": \"# Fix the greeting typo\\n\\n**Issue:** The greeting message currently displays \\\"helo wrld\\\" instead of the correct \\\"Hello World\\\".\\n\\n**Plan:**\\n\\n1. **Find the source file** — Locate the file that contains the greeting string \\\"helo wrld\\\".\\n2. **Fix the typo** — Replace \\\"helo wrld\\\" with \\\"Hello World\\\" in that source file.\\n3. **Verify** — Confirm the fix is correct by reading the changed file.\"}"}}}} +{"type":"assistant/chunk","seq":218,"time":1784525388178,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":274,"outputTokens":187,"cacheReadTokens":3072,"reasoningTokens":44}}}} +{"type":"assistant/chunk","seq":219,"time":1784525388178,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":220,"time":1784525388178,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The file contains a single note: \"the greeting message still says 'helo wrld'\". The user wants a plan titled \"# Fix the greeting typo\". Let me present a simple plan to fix this typo."},{"type":"tool-call","id":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","arguments":"{\"plan\": \"# Fix the greeting typo\\n\\n**Issue:** The greeting message currently displays \\\"helo wrld\\\" instead of the correct \\\"Hello World\\\".\\n\\n**Plan:**\\n\\n1. **Find the source file** — Locate the file that contains the greeting string \\\"helo wrld\\\".\\n2. **Fix the typo** — Replace \\\"helo wrld\\\" with \\\"Hello World\\\" in that source file.\\n3. **Verify** — Confirm the fix is correct by reading the changed file.\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":274,"outputTokens":187,"cacheReadTokens":3072,"reasoningTokens":44}},"sourceEventSeqs":[62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,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],"surfaceOp":"append"} +{"type":"tool/call","seq":221,"time":1784525388178,"data":{"turn":1,"step":2,"callId":"call_00_ucsiIH3qJOCb1B6d3dmm4387","name":"exit_plan_mode","arguments":"{\"plan\": \"# Fix the greeting typo\\n\\n**Issue:** The greeting message currently displays \\\"helo wrld\\\" instead of the correct \\\"Hello World\\\".\\n\\n**Plan:**\\n\\n1. **Find the source file** — Locate the file that contains the greeting string \\\"helo wrld\\\".\\n2. **Fix the typo** — Replace \\\"helo wrld\\\" with \\\"Hello World\\\" in that source file.\\n3. **Verify** — Confirm the fix is correct by reading the changed file.\"}"}} +{"type":"tool/result","seq":222,"time":1784525388182,"data":{"turn":1,"step":2,"callId":"call_00_ucsiIH3qJOCb1B6d3dmm4387","content":[{"type":"text","text":"Error: The user chose to keep planning; their feedback: Also add a verification step that re-reads the file after the fix."}],"isError":true},"sourceEventSeqs":[221],"surfaceOp":"append"} +{"type":"step/end","seq":223,"time":1784525388182,"data":{"turn":1,"step":2}} +{"type":"step/start","seq":224,"time":1784525388183,"data":{"turn":1,"step":3}} +{"type":"assistant/chunk","seq":225,"time":1784525388812,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":226,"time":1784525388812,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":227,"time":1784525388919,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":228,"time":1784525388951,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":229,"time":1784525388951,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":230,"time":1784525388951,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":231,"time":1784525388978,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" add"}}} +{"type":"assistant/chunk","seq":232,"time":1784525389007,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":233,"time":1784525389007,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" verification"}}} +{"type":"assistant/chunk","seq":234,"time":1784525389008,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" step"}}} +{"type":"assistant/chunk","seq":235,"time":1784525389008,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":236,"time":1784525389038,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" re"}}} +{"type":"assistant/chunk","seq":237,"time":1784525389038,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"-read"}}} +{"type":"assistant/chunk","seq":238,"time":1784525389038,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"s"}}} +{"type":"assistant/chunk","seq":239,"time":1784525389038,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":240,"time":1784525389038,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":241,"time":1784525389038,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" after"}}} +{"type":"assistant/chunk","seq":242,"time":1784525389069,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":243,"time":1784525389070,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" fix"}}} +{"type":"assistant/chunk","seq":244,"time":1784525389070,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":245,"time":1784525389070,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" But"}}} +{"type":"assistant/chunk","seq":246,"time":1784525389101,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" they"}}} +{"type":"assistant/chunk","seq":247,"time":1784525389101,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" said"}}} +{"type":"assistant/chunk","seq":248,"time":1784525389133,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":249,"time":1784525389133,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" keep"}}} +{"type":"assistant/chunk","seq":250,"time":1784525389168,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" planning"}}} +{"type":"assistant/chunk","seq":251,"time":1784525389169,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" —"}}} +{"type":"assistant/chunk","seq":252,"time":1784525389193,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" they"}}} +{"type":"assistant/chunk","seq":253,"time":1784525389193,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" didn"}}} +{"type":"assistant/chunk","seq":254,"time":1784525389221,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"'t"}}} +{"type":"assistant/chunk","seq":255,"time":1784525389221,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" approve"}}} +{"type":"assistant/chunk","seq":256,"time":1784525389250,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":257,"time":1784525389278,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" They"}}} +{"type":"assistant/chunk","seq":258,"time":1784525389278,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" also"}}} +{"type":"assistant/chunk","seq":259,"time":1784525389309,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" said"}}} +{"type":"assistant/chunk","seq":260,"time":1784525389310,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":261,"time":1784525389310,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"do"}}} +{"type":"assistant/chunk","seq":262,"time":1784525389339,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} +{"type":"assistant/chunk","seq":263,"time":1784525389339,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" present"}}} +{"type":"assistant/chunk","seq":264,"time":1784525389339,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":265,"time":1784525389339,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" plan"}}} +{"type":"assistant/chunk","seq":266,"time":1784525389367,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" again"}}} +{"type":"assistant/chunk","seq":267,"time":1784525389367,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":268,"time":1784525389398,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" if"}}} +{"type":"assistant/chunk","seq":269,"time":1784525389431,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} +{"type":"assistant/chunk","seq":270,"time":1784525389462,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" approved"}}} +{"type":"assistant/chunk","seq":271,"time":1784525389463,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":272,"time":1784525389463,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":273,"time":1784525389494,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":274,"time":1784525389495,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" re"}}} +{"type":"assistant/chunk","seq":275,"time":1784525389495,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"-read"}}} +{"type":"assistant/chunk","seq":276,"time":1784525389495,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":277,"time":1784525389495,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" instructions"}}} +{"type":"assistant/chunk","seq":278,"time":1784525389525,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":":\n\n"}}} +{"type":"assistant/chunk","seq":279,"time":1784525389525,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"\"If"}}} +{"type":"assistant/chunk","seq":280,"time":1784525389557,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":281,"time":1784525389558,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" review"}}} +{"type":"assistant/chunk","seq":282,"time":1784525389558,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" does"}}} +{"type":"assistant/chunk","seq":283,"time":1784525389558,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} +{"type":"assistant/chunk","seq":284,"time":1784525389558,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" approve"}}} +{"type":"assistant/chunk","seq":285,"time":1784525389558,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":286,"time":1784525389590,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" summarize"}}} +{"type":"assistant/chunk","seq":287,"time":1784525389590,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":288,"time":1784525389590,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" reviewer"}}} +{"type":"assistant/chunk","seq":289,"time":1784525389623,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} +{"type":"assistant/chunk","seq":290,"time":1784525389623,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" feedback"}}} +{"type":"assistant/chunk","seq":291,"time":1784525389624,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} +{"type":"assistant/chunk","seq":292,"time":1784525389624,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" plain"}}} +{"type":"assistant/chunk","seq":293,"time":1784525389624,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" text"}}} +{"type":"assistant/chunk","seq":294,"time":1784525389654,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":295,"time":1784525389654,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" end"}}} +{"type":"assistant/chunk","seq":296,"time":1784525389655,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" your"}}} +{"type":"assistant/chunk","seq":297,"time":1784525389655,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":298,"time":1784525389655,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" -"}}} +{"type":"assistant/chunk","seq":299,"time":1784525389686,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" do"}}} +{"type":"assistant/chunk","seq":300,"time":1784525389686,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} +{"type":"assistant/chunk","seq":301,"time":1784525389686,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" present"}}} +{"type":"assistant/chunk","seq":302,"time":1784525389687,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":303,"time":1784525389687,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" plan"}}} +{"type":"assistant/chunk","seq":304,"time":1784525389687,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" again"}}} +{"type":"assistant/chunk","seq":305,"time":1784525389718,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":".\"\n\n"}}} +{"type":"assistant/chunk","seq":306,"time":1784525389718,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"So"}}} +{"type":"assistant/chunk","seq":307,"time":1784525389719,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":308,"time":1784525389719,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" should"}}} +{"type":"assistant/chunk","seq":309,"time":1784525389719,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" summarize"}}} +{"type":"assistant/chunk","seq":310,"time":1784525389765,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":311,"time":1784525389782,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" feedback"}}} +{"type":"assistant/chunk","seq":312,"time":1784525389783,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":313,"time":1784525389783,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" end"}}} +{"type":"assistant/chunk","seq":314,"time":1784525389783,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" my"}}} +{"type":"assistant/chunk","seq":315,"time":1784525389811,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":316,"time":1784525389811,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" without"}}} +{"type":"assistant/chunk","seq":317,"time":1784525389843,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" presenting"}}} +{"type":"assistant/chunk","seq":318,"time":1784525389844,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":319,"time":1784525389844,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" plan"}}} +{"type":"assistant/chunk","seq":320,"time":1784525389844,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" again"}}} +{"type":"assistant/chunk","seq":321,"time":1784525389844,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":322,"time":1784525389875,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":323,"time":1784525389875,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"The"}}} +{"type":"assistant/chunk","seq":324,"time":1784525389875,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" reviewer"}}} +{"type":"assistant/chunk","seq":325,"time":1784525389906,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"'s"}}} +{"type":"assistant/chunk","seq":326,"time":1784525389934,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" feedback"}}} +{"type":"assistant/chunk","seq":327,"time":1784525389934,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":":"}}} +{"type":"assistant/chunk","seq":328,"time":1784525389965,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":329,"time":1784525389994,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" plan"}}} +{"type":"assistant/chunk","seq":330,"time":1784525390022,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" already"}}} +{"type":"assistant/chunk","seq":331,"time":1784525390023,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" includes"}}} +{"type":"assistant/chunk","seq":332,"time":1784525390053,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" a"}}} +{"type":"assistant/chunk","seq":333,"time":1784525390055,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" \""}}} +{"type":"assistant/chunk","seq":334,"time":1784525390083,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"Verify"}}} +{"type":"assistant/chunk","seq":335,"time":1784525390084,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"\""}}} +{"type":"assistant/chunk","seq":336,"time":1784525390084,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" step"}}} +{"type":"assistant/chunk","seq":337,"time":1784525390084,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" ("}}} +{"type":"assistant/chunk","seq":338,"time":1784525390114,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"step"}}} +{"type":"assistant/chunk","seq":339,"time":1784525390115,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":340,"time":1784525390145,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"3"}}} +{"type":"assistant/chunk","seq":341,"time":1784525390146,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"),"}}} +{"type":"assistant/chunk","seq":342,"time":1784525390146,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" but"}}} +{"type":"assistant/chunk","seq":343,"time":1784525390146,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" they"}}} +{"type":"assistant/chunk","seq":344,"time":1784525390173,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" want"}}} +{"type":"assistant/chunk","seq":345,"time":1784525390173,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" it"}}} +{"type":"assistant/chunk","seq":346,"time":1784525390205,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" to"}}} +{"type":"assistant/chunk","seq":347,"time":1784525390205,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" be"}}} +{"type":"assistant/chunk","seq":348,"time":1784525390206,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" more"}}} +{"type":"assistant/chunk","seq":349,"time":1784525390234,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" explicit"}}} +{"type":"assistant/chunk","seq":350,"time":1784525390234,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" —"}}} +{"type":"assistant/chunk","seq":351,"time":1784525390235,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" specifically"}}} +{"type":"assistant/chunk","seq":352,"time":1784525390264,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" re"}}} +{"type":"assistant/chunk","seq":353,"time":1784525390296,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"-reading"}}} +{"type":"assistant/chunk","seq":354,"time":1784525390296,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":355,"time":1784525390297,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" file"}}} +{"type":"assistant/chunk","seq":356,"time":1784525390329,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" after"}}} +{"type":"assistant/chunk","seq":357,"time":1784525390329,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":358,"time":1784525390329,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" fix"}}} +{"type":"assistant/chunk","seq":359,"time":1784525390329,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" to"}}} +{"type":"assistant/chunk","seq":360,"time":1784525390367,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" confirm"}}} +{"type":"assistant/chunk","seq":361,"time":1784525390367,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":362,"time":1784525390367,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" change"}}} +{"type":"assistant/chunk","seq":363,"time":1784525390392,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"."}}} +{"type":"assistant/chunk","seq":364,"time":1784525390392,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" Since"}}} +{"type":"assistant/chunk","seq":365,"time":1784525390423,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":366,"time":1784525390454,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" plan"}}} +{"type":"assistant/chunk","seq":367,"time":1784525390454,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" was"}}} +{"type":"assistant/chunk","seq":368,"time":1784525390485,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" not"}}} +{"type":"assistant/chunk","seq":369,"time":1784525390485,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" approved"}}} +{"type":"assistant/chunk","seq":370,"time":1784525390513,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" and"}}} +{"type":"assistant/chunk","seq":371,"time":1784525390544,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" I"}}} +{"type":"assistant/chunk","seq":372,"time":1784525390572,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"'m"}}} +{"type":"assistant/chunk","seq":373,"time":1784525390602,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" instructed"}}} +{"type":"assistant/chunk","seq":374,"time":1784525390603,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" not"}}} +{"type":"assistant/chunk","seq":375,"time":1784525390603,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" to"}}} +{"type":"assistant/chunk","seq":376,"time":1784525390603,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" present"}}} +{"type":"assistant/chunk","seq":377,"time":1784525390629,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" it"}}} +{"type":"assistant/chunk","seq":378,"time":1784525390629,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" again"}}} +{"type":"assistant/chunk","seq":379,"time":1784525390629,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":380,"time":1784525390629,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" I"}}} +{"type":"assistant/chunk","seq":381,"time":1784525390629,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"'ll"}}} +{"type":"assistant/chunk","seq":382,"time":1784525390657,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" incorporate"}}} +{"type":"assistant/chunk","seq":383,"time":1784525390689,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" that"}}} +{"type":"assistant/chunk","seq":384,"time":1784525390725,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" feedback"}}} +{"type":"assistant/chunk","seq":385,"time":1784525390751,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":":"}}} +{"type":"assistant/chunk","seq":386,"time":1784525390751,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":387,"time":1784525390779,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" verification"}}} +{"type":"assistant/chunk","seq":388,"time":1784525390811,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" step"}}} +{"type":"assistant/chunk","seq":389,"time":1784525390811,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" should"}}} +{"type":"assistant/chunk","seq":390,"time":1784525390812,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" say"}}} +{"type":"assistant/chunk","seq":391,"time":1784525390838,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" **"}}} +{"type":"assistant/chunk","seq":392,"time":1784525390839,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"\""}}} +{"type":"assistant/chunk","seq":393,"time":1784525390870,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"Re"}}} +{"type":"assistant/chunk","seq":394,"time":1784525390901,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"-read"}}} +{"type":"assistant/chunk","seq":395,"time":1784525390901,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":396,"time":1784525390901,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" file"}}} +{"type":"assistant/chunk","seq":397,"time":1784525390902,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" to"}}} +{"type":"assistant/chunk","seq":398,"time":1784525390933,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" confirm"}}} +{"type":"assistant/chunk","seq":399,"time":1784525390933,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":400,"time":1784525390934,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" change"}}} +{"type":"assistant/chunk","seq":401,"time":1784525390965,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" took"}}} +{"type":"assistant/chunk","seq":402,"time":1784525390996,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" effect"}}} +{"type":"assistant/chunk","seq":403,"time":1784525390996,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"\""}}} +{"type":"assistant/chunk","seq":404,"time":1784525390996,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":405,"time":1784525391024,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" rather"}}} +{"type":"assistant/chunk","seq":406,"time":1784525391024,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" than"}}} +{"type":"assistant/chunk","seq":407,"time":1784525391024,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" just"}}} +{"type":"assistant/chunk","seq":408,"time":1784525391052,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":409,"time":1784525391053,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" generic"}}} +{"type":"assistant/chunk","seq":410,"time":1784525391081,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" \""}}} +{"type":"assistant/chunk","seq":411,"time":1784525391081,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":"Confirm"}}} +{"type":"assistant/chunk","seq":412,"time":1784525391081,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" the"}}} +{"type":"assistant/chunk","seq":413,"time":1784525391109,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" fix"}}} +{"type":"assistant/chunk","seq":414,"time":1784525391109,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" is"}}} +{"type":"assistant/chunk","seq":415,"time":1784525391111,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":" correct"}}} +{"type":"assistant/chunk","seq":416,"time":1784525391111,"data":{"turn":1,"step":3,"chunk":{"type":"text-delta","index":1,"text":".\""}}} +{"type":"assistant/chunk","seq":417,"time":1784525391112,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to add a verification step that re-reads the file after the fix. But they said to keep planning — they didn't approve. They also said \"do not present the plan again\" if not approved. Let me re-read the instructions:\n\n\"If the review does not approve, summarize the reviewer's feedback in plain text and end your reply - do not present the plan again.\"\n\nSo I should summarize the feedback and end my reply without presenting the plan again."}}}} +{"type":"assistant/chunk","seq":418,"time":1784525391112,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The reviewer's feedback: the plan already includes a \"Verify\" step (step 3), but they want it to be more explicit — specifically re-reading the file after the fix to confirm the change. Since the plan was not approved and I'm instructed not to present it again, I'll incorporate that feedback: the verification step should say **\"Re-read the file to confirm the change took effect\"** rather than just the generic \"Confirm the fix is correct.\""}}}} +{"type":"assistant/chunk","seq":419,"time":1784525391112,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":243,"outputTokens":191,"cacheReadTokens":3328,"reasoningTokens":96}}}} +{"type":"assistant/chunk","seq":420,"time":1784525391112,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":421,"time":1784525391113,"data":{"turn":1,"step":3,"content":[{"type":"reasoning","text":"The user wants me to add a verification step that re-reads the file after the fix. But they said to keep planning — they didn't approve. They also said \"do not present the plan again\" if not approved. Let me re-read the instructions:\n\n\"If the review does not approve, summarize the reviewer's feedback in plain text and end your reply - do not present the plan again.\"\n\nSo I should summarize the feedback and end my reply without presenting the plan again."},{"type":"text","text":"The reviewer's feedback: the plan already includes a \"Verify\" step (step 3), but they want it to be more explicit — specifically re-reading the file after the fix to confirm the change. Since the plan was not approved and I'm instructed not to present it again, I'll incorporate that feedback: the verification step should say **\"Re-read the file to confirm the change took effect\"** rather than just the generic \"Confirm the fix is correct.\""}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":243,"outputTokens":191,"cacheReadTokens":3328,"reasoningTokens":96}},"sourceEventSeqs":[225,226,227,228,229,230,231,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,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,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],"surfaceOp":"append"} +{"type":"step/end","seq":422,"time":1784525391113,"data":{"turn":1,"step":3}} +{"type":"turn/end","seq":423,"time":1784525391113,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/plan-mode-reject/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/plan-mode-reject/stdout.expected.jsonl new file mode 100644 index 0000000000..02878c0201 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/plan-mode-reject/stdout.expected.jsonl @@ -0,0 +1,279 @@ +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"current_mode_update","currentModeId":"plan"}}} +{"jsonrpc":"2.0","id":3,"result":{}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Read the file notes.txt (use","updatedAt":"{{updatedAt}}"}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" wants"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" read"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" file"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" notes"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":".txt"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" using"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" relative"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" path"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":","}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" then"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" present"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" a"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" via"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" exit"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_mode"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Let"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" start"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" by"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reading"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" file"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_Tez1Df1H9v8RqxqYIs5o6697","title":"Read notes.txt","kind":"read","status":"in_progress","locations":[{"path":"notes.txt","line":1}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_Tez1Df1H9v8RqxqYIs5o6697","status":"completed","content":[{"type":"content","content":{"type":"text","text":"{{cwd}}/notes.txt\nfile\n\n1: project notes\n2: - the greeting message still says \"helo wrld\"\n\n(End of file - total 2 lines)\n"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" file"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" contains"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" a"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" single"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" note"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":":"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" greeting"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" message"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" still"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" says"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" '"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"hel"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"o"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" wr"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ld"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"'\"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" wants"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" a"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" titled"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \"#"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Fix"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" greeting"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" typ"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"o"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Let"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" present"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" a"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" simple"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" fix"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" this"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" typ"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"o"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_ucsiIH3qJOCb1B6d3dmm4387","title":"Fix the greeting typo","kind":"other","status":"in_progress","content":[{"type":"content","content":{"type":"text","text":"# Fix the greeting typo\n\n**Issue:** The greeting message currently displays \"helo wrld\" instead of the correct \"Hello World\".\n\n**Plan:**\n\n1. **Find the source file** — Locate the file that contains the greeting string \"helo wrld\".\n2. **Fix the typo** — Replace \"helo wrld\" with \"Hello World\" in that source file.\n3. **Verify** — Confirm the fix is correct by reading the changed file."}}]}}} +{"jsonrpc":"2.0","id":1,"method":"elicitation/create","params":{"sessionId":"{{sessionId}}","mode":"form","message":"Approve this plan and leave plan mode?\n\n# Fix the greeting typo\n\n**Issue:** The greeting message currently displays \"helo wrld\" instead of the correct \"Hello World\".\n\n**Plan:**\n\n1. **Find the source file** — Locate the file that contains the greeting string \"helo wrld\".\n2. **Fix the typo** — Replace \"helo wrld\" with \"Hello World\" in that source file.\n3. **Verify** — Confirm the fix is correct by reading the changed file.","requestedSchema":{"type":"object","title":"Plan review","properties":{"choice":{"type":"string","title":"Approve this plan and leave plan mode?","description":"Choose one option, or fill a custom answer below.","oneOf":[{"const":"Approve","title":"Approve: Leave plan mode; the plan is carried out from the next step."},{"const":"Keep planning","title":"Keep planning: Stay in plan mode; feedback goes back to the model."}]},"custom":{"type":"string","title":"Custom answer","description":"Optional free-form answer. Leave empty to use the selected option."}},"required":[]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_ucsiIH3qJOCb1B6d3dmm4387","status":"failed","content":[{"type":"content","content":{"type":"text","text":"Error: The user chose to keep planning; their feedback: Also add a verification step that re-reads the file after the fix."}}],"title":"Plan review"}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" wants"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" add"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" a"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" verification"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" step"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" that"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" re"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"-read"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"s"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" file"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" after"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" fix"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" But"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" they"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" said"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" keep"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" planning"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" —"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" they"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" didn"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"'t"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" approve"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" They"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" also"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" said"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"do"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" not"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" present"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" again"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" if"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" not"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" approved"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Let"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" re"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"-read"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" instructions"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":":\n\n"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\"If"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" review"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" does"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" not"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" approve"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":","}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" summarize"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reviewer"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"'s"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" feedback"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" in"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" plain"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" text"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" end"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" your"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" -"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" do"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" not"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" present"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" again"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":".\"\n\n"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"So"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" should"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" summarize"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" feedback"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" end"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" my"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" without"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" presenting"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" again"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" reviewer"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"'s"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" feedback"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":":"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" already"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" includes"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" a"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"Verify"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" step"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" ("}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"step"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"3"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"),"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" but"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" they"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" want"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" it"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" be"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" more"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" explicit"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" —"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" specifically"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" re"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"-reading"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" file"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" after"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" fix"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" confirm"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" change"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" Since"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" was"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" not"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" approved"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" and"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"'m"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" instructed"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" not"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" present"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" it"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" again"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":","}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"'ll"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" incorporate"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" that"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" feedback"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":":"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" verification"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" step"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" should"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" say"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" **"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"Re"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"-read"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" file"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" confirm"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" change"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" took"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" effect"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"**"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" rather"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" than"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" just"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" generic"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"Confirm"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" fix"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" is"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" correct"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":".\""}}}} +{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}} diff --git a/examples/acp-agent/tests/snapshots/plan-mode-reject/workspace/notes.txt b/examples/acp-agent/tests/snapshots/plan-mode-reject/workspace/notes.txt new file mode 100644 index 0000000000..0230df4b3b --- /dev/null +++ b/examples/acp-agent/tests/snapshots/plan-mode-reject/workspace/notes.txt @@ -0,0 +1,2 @@ +project notes +- the greeting message still says "helo wrld" diff --git a/examples/acp-agent/tests/snapshots/plan-mode/input.json b/examples/acp-agent/tests/snapshots/plan-mode/input.json new file mode 100644 index 0000000000..afc80dc900 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/plan-mode/input.json @@ -0,0 +1,28 @@ +{ + "steps": [ + { + "op": "initialize" + }, + { + "op": "newSession" + }, + { + "op": "setMode", + "modeId": "plan" + }, + { + "op": "prompt", + "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." + }, + { + "op": "prompt", + "text": "Read notes.txt (relative path notes.txt) and reply with the single word DONE if line 2 now says hello world." + } + ], + "elicitationAnswers": [ + { + "action": "accept", + "choice": "Approve" + } + ] +} diff --git a/examples/acp-agent/tests/snapshots/plan-mode/session.jsonl b/examples/acp-agent/tests/snapshots/plan-mode/session.jsonl new file mode 100644 index 0000000000..71a1189702 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/plan-mode/session.jsonl @@ -0,0 +1,532 @@ +{"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":"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"}}} +{"type":"assistant/chunk","seq":7,"time":1784525375473,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":8,"time":1784525375620,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":9,"time":1784525375643,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":10,"time":1784525375644,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":11,"time":1784525375644,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":12,"time":1784525375644,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":":\n"}}} +{"type":"assistant/chunk","seq":13,"time":1784525375644,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"1"}}} +{"type":"assistant/chunk","seq":14,"time":1784525375644,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":15,"time":1784525375670,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Run"}}} +{"type":"assistant/chunk","seq":16,"time":1784525375670,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":17,"time":1784525375671,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"cat"}}} +{"type":"assistant/chunk","seq":18,"time":1784525375671,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" notes"}}} +{"type":"assistant/chunk","seq":19,"time":1784525375698,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".txt"}}} +{"type":"assistant/chunk","seq":20,"time":1784525375699,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":21,"time":1784525375699,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":22,"time":1784525375699,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" inspect"}}} +{"type":"assistant/chunk","seq":23,"time":1784525375727,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":24,"time":1784525375727,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":25,"time":1784525375755,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\n"}}} +{"type":"assistant/chunk","seq":26,"time":1784525375756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"2"}}} +{"type":"assistant/chunk","seq":27,"time":1784525375756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":28,"time":1784525375756,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Present"}}} +{"type":"assistant/chunk","seq":29,"time":1784525375784,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":30,"time":1784525375784,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" plan"}}} +{"type":"assistant/chunk","seq":31,"time":1784525375785,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" via"}}} +{"type":"assistant/chunk","seq":32,"time":1784525375813,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exit"}}} +{"type":"assistant/chunk","seq":33,"time":1784525375814,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_"}}} +{"type":"assistant/chunk","seq":34,"time":1784525375814,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"plan"}}} +{"type":"assistant/chunk","seq":35,"time":1784525375814,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_mode"}}} +{"type":"assistant/chunk","seq":36,"time":1784525375841,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\n"}}} +{"type":"assistant/chunk","seq":37,"time":1784525375841,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"3"}}} +{"type":"assistant/chunk","seq":38,"time":1784525375842,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":39,"time":1784525375842,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" After"}}} +{"type":"assistant/chunk","seq":40,"time":1784525375870,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" approval"}}} +{"type":"assistant/chunk","seq":41,"time":1784525375870,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":42,"time":1784525375870,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" make"}}} +{"type":"assistant/chunk","seq":43,"time":1784525375898,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":44,"time":1784525375926,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" edit"}}} +{"type":"assistant/chunk","seq":45,"time":1784525375927,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\n\n"}}} +{"type":"assistant/chunk","seq":46,"time":1784525375927,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Let"}}} +{"type":"assistant/chunk","seq":47,"time":1784525375927,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":48,"time":1784525375927,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" start"}}} +{"type":"assistant/chunk","seq":49,"time":1784525375954,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":50,"time":1784525375955,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" step"}}} +{"type":"assistant/chunk","seq":51,"time":1784525375983,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":52,"time":1784525375983,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"1"}}} +{"type":"assistant/chunk","seq":53,"time":1784525375983,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":54,"time":1784525376068,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":55,"time":1784525376068,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":56,"time":1784525376097,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":57,"time":1784525376097,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":58,"time":1784525376097,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":59,"time":1784525376098,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":60,"time":1784525376098,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":61,"time":1784525376125,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":62,"time":1784525376125,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"cat"}}} +{"type":"assistant/chunk","seq":63,"time":1784525376126,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":" notes"}}} +{"type":"assistant/chunk","seq":64,"time":1784525376126,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":65,"time":1784525376154,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":66,"time":1784525376182,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":67,"time":1784525376183,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":68,"time":1784525376183,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":69,"time":1784525376183,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":70,"time":1784525376183,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":71,"time":1784525376211,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":72,"time":1784525376211,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"In"}}} +{"type":"assistant/chunk","seq":73,"time":1784525376240,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"spect"}}} +{"type":"assistant/chunk","seq":74,"time":1784525376240,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":" notes"}}} +{"type":"assistant/chunk","seq":75,"time":1784525376240,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":76,"time":1784525376240,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":" contents"}}} +{"type":"assistant/chunk","seq":77,"time":1784525376268,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":78,"time":1784525376269,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":79,"time":1784525376333,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to:\n1. Run `cat notes.txt` to inspect the file\n2. Present a plan via exit_plan_mode\n3. After approval, make the edit\n\nLet me start with step 1."}}}} +{"type":"assistant/chunk","seq":80,"time":1784525376333,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","arguments":"{\"command\": \"cat notes.txt\", \"description\": \"Inspect notes.txt contents\"}"}}}} +{"type":"assistant/chunk","seq":81,"time":1784525376333,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3222,"outputTokens":112,"cacheReadTokens":0,"reasoningTokens":47}}}} +{"type":"assistant/chunk","seq":82,"time":1784525376333,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":83,"time":1784525376337,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to:\n1. Run `cat notes.txt` to inspect the file\n2. Present a plan via exit_plan_mode\n3. After approval, make the edit\n\nLet me start with step 1."},{"type":"tool-call","id":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","arguments":"{\"command\": \"cat notes.txt\", \"description\": \"Inspect notes.txt contents\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3222,"outputTokens":112,"cacheReadTokens":0,"reasoningTokens":47}},"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],"surfaceOp":"append"} +{"type":"tool/call","seq":84,"time":1784525376338,"data":{"turn":1,"step":1,"callId":"call_00_jRYWnAwUo7zSx2WVsRZ83655","name":"bash","arguments":"{\"command\": \"cat notes.txt\", \"description\": \"Inspect notes.txt contents\"}"}} +{"type":"tool/result","seq":85,"time":1784525376456,"data":{"turn":1,"step":1,"callId":"call_00_jRYWnAwUo7zSx2WVsRZ83655","content":[{"type":"text","text":"project notes\n- the greeting message still says \"helo wrld\"\n"}],"isError":false},"sourceEventSeqs":[84],"surfaceOp":"append"} +{"type":"step/end","seq":86,"time":1784525376457,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":87,"time":1784525376457,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":88,"time":1784525376956,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":89,"time":1784525376956,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":90,"time":1784525377129,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":91,"time":1784525377150,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" has"}}} +{"type":"assistant/chunk","seq":92,"time":1784525377179,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" two"}}} +{"type":"assistant/chunk","seq":93,"time":1784525377179,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" lines"}}} +{"type":"assistant/chunk","seq":94,"time":1784525377207,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":":\n"}}} +{"type":"assistant/chunk","seq":95,"time":1784525377207,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-"}}} +{"type":"assistant/chunk","seq":96,"time":1784525377207,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Line"}}} +{"type":"assistant/chunk","seq":97,"time":1784525377207,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":98,"time":1784525377208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"1"}}} +{"type":"assistant/chunk","seq":99,"time":1784525377208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} +{"type":"assistant/chunk","seq":100,"time":1784525377208,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":101,"time":1784525377235,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"project"}}} +{"type":"assistant/chunk","seq":102,"time":1784525377235,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" notes"}}} +{"type":"assistant/chunk","seq":103,"time":1784525377235,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"`\n"}}} +{"type":"assistant/chunk","seq":104,"time":1784525377235,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-"}}} +{"type":"assistant/chunk","seq":105,"time":1784525377235,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Line"}}} +{"type":"assistant/chunk","seq":106,"time":1784525377235,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":107,"time":1784525377264,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"2"}}} +{"type":"assistant/chunk","seq":108,"time":1784525377264,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} +{"type":"assistant/chunk","seq":109,"time":1784525377265,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":110,"time":1784525377265,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-"}}} +{"type":"assistant/chunk","seq":111,"time":1784525377265,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":112,"time":1784525377292,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" greeting"}}} +{"type":"assistant/chunk","seq":113,"time":1784525377292,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" message"}}} +{"type":"assistant/chunk","seq":114,"time":1784525377292,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" still"}}} +{"type":"assistant/chunk","seq":115,"time":1784525377293,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" says"}}} +{"type":"assistant/chunk","seq":116,"time":1784525377293,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":117,"time":1784525377293,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"hel"}}} +{"type":"assistant/chunk","seq":118,"time":1784525377323,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"o"}}} +{"type":"assistant/chunk","seq":119,"time":1784525377323,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wr"}}} +{"type":"assistant/chunk","seq":120,"time":1784525377323,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ld"}}} +{"type":"assistant/chunk","seq":121,"time":1784525377323,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":122,"time":1784525377323,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"`\n\n"}}} +{"type":"assistant/chunk","seq":123,"time":1784525377324,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":124,"time":1784525377349,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":125,"time":1784525377349,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" says"}}} +{"type":"assistant/chunk","seq":126,"time":1784525377378,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" line"}}} +{"type":"assistant/chunk","seq":127,"time":1784525377379,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":128,"time":1784525377379,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"2"}}} +{"type":"assistant/chunk","seq":129,"time":1784525377405,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" has"}}} +{"type":"assistant/chunk","seq":130,"time":1784525377405,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":131,"time":1784525377434,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" typ"}}} +{"type":"assistant/chunk","seq":132,"time":1784525377435,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"o"}}} +{"type":"assistant/chunk","seq":133,"time":1784525377435,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":134,"time":1784525377435,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" should"}}} +{"type":"assistant/chunk","seq":135,"time":1784525377462,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" say"}}} +{"type":"assistant/chunk","seq":136,"time":1784525377491,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":137,"time":1784525377491,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"hello"}}} +{"type":"assistant/chunk","seq":138,"time":1784525377491,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" world"}}} +{"type":"assistant/chunk","seq":139,"time":1784525377492,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":140,"time":1784525377492,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" So"}}} +{"type":"assistant/chunk","seq":141,"time":1784525377520,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":142,"time":1784525377549,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} +{"type":"assistant/chunk","seq":143,"time":1784525377549,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":144,"time":1784525377549,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" replace"}}} +{"type":"assistant/chunk","seq":145,"time":1784525377575,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" line"}}} +{"type":"assistant/chunk","seq":146,"time":1784525377576,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":147,"time":1784525377576,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"2"}}} +{"type":"assistant/chunk","seq":148,"time":1784525377576,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":149,"time":1784525377604,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":150,"time":1784525377632,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"hello"}}} +{"type":"assistant/chunk","seq":151,"time":1784525377632,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" world"}}} +{"type":"assistant/chunk","seq":152,"time":1784525377684,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\".\n\n"}}} +{"type":"assistant/chunk","seq":153,"time":1784525377685,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"Let"}}} +{"type":"assistant/chunk","seq":154,"time":1784525377685,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":155,"time":1784525377685,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" present"}}} +{"type":"assistant/chunk","seq":156,"time":1784525377685,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":157,"time":1784525377689,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" plan"}}} +{"type":"assistant/chunk","seq":158,"time":1784525377689,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":159,"time":1784525377774,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":160,"time":1784525377774,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":161,"time":1784525377803,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":162,"time":1784525377803,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":163,"time":1784525377803,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"plan"}}} +{"type":"assistant/chunk","seq":164,"time":1784525377803,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":165,"time":1784525377834,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":166,"time":1784525377834,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":167,"time":1784525377834,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"#"}}} +{"type":"assistant/chunk","seq":168,"time":1784525377834,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" Fix"}}} +{"type":"assistant/chunk","seq":169,"time":1784525377859,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":170,"time":1784525377860,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" greeting"}}} +{"type":"assistant/chunk","seq":171,"time":1784525377860,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" typ"}}} +{"type":"assistant/chunk","seq":172,"time":1784525377860,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"o"}}} +{"type":"assistant/chunk","seq":173,"time":1784525377860,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"\\n\\n"}}} +{"type":"assistant/chunk","seq":174,"time":1784525377860,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"##"}}} +{"type":"assistant/chunk","seq":175,"time":1784525377887,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" Single"}}} +{"type":"assistant/chunk","seq":176,"time":1784525377887,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" step"}}} +{"type":"assistant/chunk","seq":177,"time":1784525377916,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"\\n"}}} +{"type":"assistant/chunk","seq":178,"time":1784525377916,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"1"}}} +{"type":"assistant/chunk","seq":179,"time":1784525377944,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"."}}} +{"type":"assistant/chunk","seq":180,"time":1784525377944,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" **"}}} +{"type":"assistant/chunk","seq":181,"time":1784525377944,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"Edit"}}} +{"type":"assistant/chunk","seq":182,"time":1784525377973,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" line"}}} +{"type":"assistant/chunk","seq":183,"time":1784525378010,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":184,"time":1784525378010,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"2"}}} +{"type":"assistant/chunk","seq":185,"time":1784525378010,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" of"}}} +{"type":"assistant/chunk","seq":186,"time":1784525378010,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" `"}}} +{"type":"assistant/chunk","seq":187,"time":1784525378030,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"notes"}}} +{"type":"assistant/chunk","seq":188,"time":1784525378030,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":189,"time":1784525378030,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"`"}}} +{"type":"assistant/chunk","seq":190,"time":1784525378030,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"**"}}} +{"type":"assistant/chunk","seq":191,"time":1784525378030,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" —"}}} +{"type":"assistant/chunk","seq":192,"time":1784525378030,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" Replace"}}} +{"type":"assistant/chunk","seq":193,"time":1784525378057,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":194,"time":1784525378057,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" content"}}} +{"type":"assistant/chunk","seq":195,"time":1784525378087,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" of"}}} +{"type":"assistant/chunk","seq":196,"time":1784525378087,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" line"}}} +{"type":"assistant/chunk","seq":197,"time":1784525378115,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":198,"time":1784525378115,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"2"}}} +{"type":"assistant/chunk","seq":199,"time":1784525378115,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" (`"}}} +{"type":"assistant/chunk","seq":200,"time":1784525378144,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"-"}}} +{"type":"assistant/chunk","seq":201,"time":1784525378145,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":202,"time":1784525378145,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" greeting"}}} +{"type":"assistant/chunk","seq":203,"time":1784525378145,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" message"}}} +{"type":"assistant/chunk","seq":204,"time":1784525378145,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" still"}}} +{"type":"assistant/chunk","seq":205,"time":1784525378145,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" says"}}} +{"type":"assistant/chunk","seq":206,"time":1784525378200,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":207,"time":1784525378200,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"hel"}}} +{"type":"assistant/chunk","seq":208,"time":1784525378200,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"o"}}} +{"type":"assistant/chunk","seq":209,"time":1784525378201,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" wr"}}} +{"type":"assistant/chunk","seq":210,"time":1784525378201,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"ld"}}} +{"type":"assistant/chunk","seq":211,"time":1784525378201,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":212,"time":1784525378201,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"`)"}}} +{"type":"assistant/chunk","seq":213,"time":1784525378201,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" with"}}} +{"type":"assistant/chunk","seq":214,"time":1784525378201,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" `"}}} +{"type":"assistant/chunk","seq":215,"time":1784525378201,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"hello"}}} +{"type":"assistant/chunk","seq":216,"time":1784525378229,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":" world"}}} +{"type":"assistant/chunk","seq":217,"time":1784525378230,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"`"}}} +{"type":"assistant/chunk","seq":218,"time":1784525378257,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"."}}} +{"type":"assistant/chunk","seq":219,"time":1784525378257,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":220,"time":1784525378257,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":221,"time":1784525378320,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The file has two lines:\n- Line 1: `project notes`\n- Line 2: `- the greeting message still says \"helo wrld\"`\n\nThe user says line 2 has a typo and should say \"hello world\". So I need to replace line 2 with \"hello world\".\n\nLet me present the plan."}}}} +{"type":"assistant/chunk","seq":222,"time":1784525378320,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","arguments":"{\"plan\": \"# Fix the greeting typo\\n\\n## Single step\\n1. **Edit line 2 of `notes.txt`** — Replace the content of line 2 (`- the greeting message still says \\\"helo wrld\\\"`) with `hello world`.\"}"}}}} +{"type":"assistant/chunk","seq":223,"time":1784525378320,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":33,"outputTokens":166,"cacheReadTokens":3328,"reasoningTokens":70}}}} +{"type":"assistant/chunk","seq":224,"time":1784525378321,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":225,"time":1784525378321,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The file has two lines:\n- Line 1: `project notes`\n- Line 2: `- the greeting message still says \"helo wrld\"`\n\nThe user says line 2 has a typo and should say \"hello world\". So I need to replace line 2 with \"hello world\".\n\nLet me present the plan."},{"type":"tool-call","id":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","arguments":"{\"plan\": \"# Fix the greeting typo\\n\\n## Single step\\n1. **Edit line 2 of `notes.txt`** — Replace the content of line 2 (`- the greeting message still says \\\"helo wrld\\\"`) with `hello world`.\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":33,"outputTokens":166,"cacheReadTokens":3328,"reasoningTokens":70}},"sourceEventSeqs":[88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,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":1784525378322,"data":{"turn":1,"step":2,"callId":"call_00_6RLxuiGtAFswvfLnWdt63143","name":"exit_plan_mode","arguments":"{\"plan\": \"# Fix the greeting typo\\n\\n## Single step\\n1. **Edit line 2 of `notes.txt`** — Replace the content of line 2 (`- the greeting message still says \\\"helo wrld\\\"`) with `hello world`.\"}"}} +{"type":"tool/result","seq":227,"time":1784525378329,"data":{"turn":1,"step":2,"callId":"call_00_6RLxuiGtAFswvfLnWdt63143","content":[{"type":"text","text":"Plan approved — plan mode exited; carry out the plan starting with your next step."}],"isError":false},"sourceEventSeqs":[226],"surfaceOp":"append"} +{"type":"step/end","seq":228,"time":1784525378329,"data":{"turn":1,"step":2}} +{"type":"plan/mode","seq":229,"time":1784525378329,"data":{"active":false}} +{"type":"step/start","seq":230,"time":1784525378330,"data":{"turn":1,"step":3}} +{"type":"request/header","seq":231,"time":1784553020470,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"change"}} +{"type":"assistant/chunk","seq":232,"time":1784525378723,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":233,"time":1784525378723,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":234,"time":1784525378807,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" plan"}}} +{"type":"assistant/chunk","seq":235,"time":1784525378835,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} +{"type":"assistant/chunk","seq":236,"time":1784525378836,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" approved"}}} +{"type":"assistant/chunk","seq":237,"time":1784525378836,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":238,"time":1784525378836,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" Now"}}} +{"type":"assistant/chunk","seq":239,"time":1784525378836,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":240,"time":1784525378837,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} +{"type":"assistant/chunk","seq":241,"time":1784525378866,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":242,"time":1784525378867,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" apply"}}} +{"type":"assistant/chunk","seq":243,"time":1784525378867,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":244,"time":1784525378867,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":245,"time":1784525378899,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" one"}}} +{"type":"assistant/chunk","seq":246,"time":1784525378899,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" edit"}}} +{"type":"assistant/chunk","seq":247,"time":1784525378899,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} +{"type":"assistant/chunk","seq":248,"time":1784525378900,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" change"}}} +{"type":"assistant/chunk","seq":249,"time":1784525378927,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" line"}}} +{"type":"assistant/chunk","seq":250,"time":1784525378928,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":251,"time":1784525378928,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"2"}}} +{"type":"assistant/chunk","seq":252,"time":1784525378928,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" of"}}} +{"type":"assistant/chunk","seq":253,"time":1784525378928,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" notes"}}} +{"type":"assistant/chunk","seq":254,"time":1784525378959,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":".txt"}}} +{"type":"assistant/chunk","seq":255,"time":1784525378960,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":256,"time":1784525378961,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" say"}}} +{"type":"assistant/chunk","seq":257,"time":1784525378961,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":258,"time":1784525378961,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"hello"}}} +{"type":"assistant/chunk","seq":259,"time":1784525378961,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" world"}}} +{"type":"assistant/chunk","seq":260,"time":1784525378989,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":261,"time":1784525378990,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":262,"time":1784525379021,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"'ll"}}} +{"type":"assistant/chunk","seq":263,"time":1784525379021,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" use"}}} +{"type":"assistant/chunk","seq":264,"time":1784525379022,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":265,"time":1784525379022,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" edit"}}} +{"type":"assistant/chunk","seq":266,"time":1784525379052,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" tool"}}} +{"type":"assistant/chunk","seq":267,"time":1784525379052,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":268,"time":1784525379083,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" replace"}}} +{"type":"assistant/chunk","seq":269,"time":1784525379084,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":270,"time":1784525379084,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" current"}}} +{"type":"assistant/chunk","seq":271,"time":1784525379115,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" line"}}} +{"type":"assistant/chunk","seq":272,"time":1784525379116,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":273,"time":1784525379116,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"2"}}} +{"type":"assistant/chunk","seq":274,"time":1784525379116,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" content"}}} +{"type":"assistant/chunk","seq":275,"time":1784525379145,"data":{"turn":1,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":276,"time":1784525379211,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":277,"time":1784525379211,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":278,"time":1784525379240,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":279,"time":1784525379241,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":280,"time":1784525379241,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"file"}}} +{"type":"assistant/chunk","seq":281,"time":1784525379241,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"_path"}}} +{"type":"assistant/chunk","seq":282,"time":1784525379271,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":283,"time":1784525379272,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":284,"time":1784525379272,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":285,"time":1784525379272,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"notes"}}} +{"type":"assistant/chunk","seq":286,"time":1784525379300,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":287,"time":1784525379300,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":288,"time":1784525379330,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":289,"time":1784525379330,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":290,"time":1784525379330,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"old"}}} +{"type":"assistant/chunk","seq":291,"time":1784525379330,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"_string"}}} +{"type":"assistant/chunk","seq":292,"time":1784525379362,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":293,"time":1784525379362,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":294,"time":1784525379362,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":295,"time":1784525379362,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"-"}}} +{"type":"assistant/chunk","seq":296,"time":1784525379393,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":297,"time":1784525379394,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":" greeting"}}} +{"type":"assistant/chunk","seq":298,"time":1784525379394,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":" message"}}} +{"type":"assistant/chunk","seq":299,"time":1784525379394,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":" still"}}} +{"type":"assistant/chunk","seq":300,"time":1784525379424,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":" says"}}} +{"type":"assistant/chunk","seq":301,"time":1784525379425,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":302,"time":1784525379425,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"hel"}}} +{"type":"assistant/chunk","seq":303,"time":1784525379425,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"o"}}} +{"type":"assistant/chunk","seq":304,"time":1784525379455,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":" wr"}}} +{"type":"assistant/chunk","seq":305,"time":1784525379456,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"ld"}}} +{"type":"assistant/chunk","seq":306,"time":1784525379456,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":307,"time":1784525379456,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":308,"time":1784525379486,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":309,"time":1784525379487,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":310,"time":1784525379519,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"new"}}} +{"type":"assistant/chunk","seq":311,"time":1784525379520,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"_string"}}} +{"type":"assistant/chunk","seq":312,"time":1784525379520,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":313,"time":1784525379520,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":314,"time":1784525379548,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":315,"time":1784525379548,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"hello"}}} +{"type":"assistant/chunk","seq":316,"time":1784525379548,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":" world"}}} +{"type":"assistant/chunk","seq":317,"time":1784525379548,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":318,"time":1784525379579,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","name":"edit","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":319,"time":1784525379646,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"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":"assistant/chunk","seq":320,"time":1784525379646,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":1,"block":{"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\"}"}}}} +{"type":"assistant/chunk","seq":321,"time":1784525379646,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3269,"outputTokens":134,"cacheReadTokens":0,"reasoningTokens":43}}}} +{"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":"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"}}} +{"type":"assistant/chunk","seq":329,"time":1784525380205,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"I"}}} +{"type":"assistant/chunk","seq":330,"time":1784525380298,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} +{"type":"assistant/chunk","seq":331,"time":1784525380329,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":332,"time":1784525380330,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" read"}}} +{"type":"assistant/chunk","seq":333,"time":1784525380330,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":334,"time":1784525380360,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":335,"time":1784525380361,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" first"}}} +{"type":"assistant/chunk","seq":336,"time":1784525380361,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" before"}}} +{"type":"assistant/chunk","seq":337,"time":1784525380361,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" editing"}}} +{"type":"assistant/chunk","seq":338,"time":1784525380361,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":339,"time":1784525380391,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":340,"time":1784525380392,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":341,"time":1784525380392,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":342,"time":1784525380392,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" read"}}} +{"type":"assistant/chunk","seq":343,"time":1784525380392,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" it"}}} +{"type":"assistant/chunk","seq":344,"time":1784525380422,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":345,"time":1784525380422,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} +{"type":"assistant/chunk","seq":346,"time":1784525380423,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" apply"}}} +{"type":"assistant/chunk","seq":347,"time":1784525380423,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":348,"time":1784525380423,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":" edit"}}} +{"type":"assistant/chunk","seq":349,"time":1784525380423,"data":{"turn":1,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":350,"time":1784525380516,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":351,"time":1784525380516,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":352,"time":1784525380547,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":353,"time":1784525380547,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":354,"time":1784525380547,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":"file"}}} +{"type":"assistant/chunk","seq":355,"time":1784525380547,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":"_path"}}} +{"type":"assistant/chunk","seq":356,"time":1784525380547,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":357,"time":1784525380579,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":358,"time":1784525380580,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":359,"time":1784525380580,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":"notes"}}} +{"type":"assistant/chunk","seq":360,"time":1784525380580,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":361,"time":1784525380610,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":362,"time":1784525380610,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":363,"time":1784525380678,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"I need to read the file first before editing it. Let me read it and then apply the edit."}}}} +{"type":"assistant/chunk","seq":364,"time":1784525380678,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","arguments":"{\"file_path\": \"notes.txt\"}"}}}} +{"type":"assistant/chunk","seq":365,"time":1784525380678,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":262,"outputTokens":66,"cacheReadTokens":3200,"reasoningTokens":21}}}} +{"type":"assistant/chunk","seq":366,"time":1784525380678,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":367,"time":1784525380679,"data":{"turn":1,"step":4,"content":[{"type":"reasoning","text":"I need to read the file first before editing it. Let me read it and then apply the edit."},{"type":"tool-call","id":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","arguments":"{\"file_path\": \"notes.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":262,"outputTokens":66,"cacheReadTokens":3200,"reasoningTokens":21}},"sourceEventSeqs":[328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366],"surfaceOp":"append"} +{"type":"tool/call","seq":368,"time":1784525380679,"data":{"turn":1,"step":4,"callId":"call_00_XG9RoTfylfw20IP0dmrV7315","name":"read","arguments":"{\"file_path\": \"notes.txt\"}"}} +{"type":"tool/result","seq":369,"time":1784525380686,"data":{"turn":1,"step":4,"callId":"call_00_XG9RoTfylfw20IP0dmrV7315","content":[{"type":"text","text":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-tia05W/notes.txt\nfile\n\n1: project notes\n2: - the greeting message still says \"helo wrld\"\n\n(End of file - total 2 lines)\n"}],"isError":false},"sourceEventSeqs":[368],"surfaceOp":"append"} +{"type":"step/end","seq":370,"time":1784525380686,"data":{"turn":1,"step":4}} +{"type":"step/start","seq":371,"time":1784525380687,"data":{"turn":1,"step":5}} +{"type":"assistant/chunk","seq":372,"time":1784525381105,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":373,"time":1784525381105,"data":{"turn":1,"step":5,"chunk":{"type":"reasoning-delta","index":0,"text":"Now"}}} +{"type":"assistant/chunk","seq":374,"time":1784525381207,"data":{"turn":1,"step":5,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":375,"time":1784525381237,"data":{"turn":1,"step":5,"chunk":{"type":"reasoning-delta","index":0,"text":" can"}}} +{"type":"assistant/chunk","seq":376,"time":1784525381237,"data":{"turn":1,"step":5,"chunk":{"type":"reasoning-delta","index":0,"text":" apply"}}} +{"type":"assistant/chunk","seq":377,"time":1784525381237,"data":{"turn":1,"step":5,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":378,"time":1784525381238,"data":{"turn":1,"step":5,"chunk":{"type":"reasoning-delta","index":0,"text":" edit"}}} +{"type":"assistant/chunk","seq":379,"time":1784525381274,"data":{"turn":1,"step":5,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":380,"time":1784525381363,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":381,"time":1784525381363,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":382,"time":1784525381364,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":383,"time":1784525381364,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":384,"time":1784525381394,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"file"}}} +{"type":"assistant/chunk","seq":385,"time":1784525381395,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"_path"}}} +{"type":"assistant/chunk","seq":386,"time":1784525381395,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":387,"time":1784525381395,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":388,"time":1784525381427,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":389,"time":1784525381427,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"notes"}}} +{"type":"assistant/chunk","seq":390,"time":1784525381428,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":391,"time":1784525381428,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":392,"time":1784525381457,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":393,"time":1784525381458,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":394,"time":1784525381489,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"old"}}} +{"type":"assistant/chunk","seq":395,"time":1784525381490,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"_string"}}} +{"type":"assistant/chunk","seq":396,"time":1784525381490,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":397,"time":1784525381490,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":398,"time":1784525381520,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":399,"time":1784525381520,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"-"}}} +{"type":"assistant/chunk","seq":400,"time":1784525381521,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":" the"}}} +{"type":"assistant/chunk","seq":401,"time":1784525381521,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":" greeting"}}} +{"type":"assistant/chunk","seq":402,"time":1784525381521,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":" message"}}} +{"type":"assistant/chunk","seq":403,"time":1784525381521,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":" still"}}} +{"type":"assistant/chunk","seq":404,"time":1784525381552,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":" says"}}} +{"type":"assistant/chunk","seq":405,"time":1784525381552,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":" \\\""}}} +{"type":"assistant/chunk","seq":406,"time":1784525381552,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"hel"}}} +{"type":"assistant/chunk","seq":407,"time":1784525381552,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"o"}}} +{"type":"assistant/chunk","seq":408,"time":1784525381552,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":" wr"}}} +{"type":"assistant/chunk","seq":409,"time":1784525381552,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"ld"}}} +{"type":"assistant/chunk","seq":410,"time":1784525381583,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\\\""}}} +{"type":"assistant/chunk","seq":411,"time":1784525381583,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":412,"time":1784525381614,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":413,"time":1784525381615,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":414,"time":1784525381615,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"new"}}} +{"type":"assistant/chunk","seq":415,"time":1784525381615,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"_string"}}} +{"type":"assistant/chunk","seq":416,"time":1784525381647,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":417,"time":1784525381647,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":418,"time":1784525381647,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":419,"time":1784525381647,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"hello"}}} +{"type":"assistant/chunk","seq":420,"time":1784525381683,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":" world"}}} +{"type":"assistant/chunk","seq":421,"time":1784525381684,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":422,"time":1784525381709,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","name":"edit","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":423,"time":1784525381744,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Now I can apply the edit."}}}} +{"type":"assistant/chunk","seq":424,"time":1784525381744,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":1,"block":{"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\"}"}}}} +{"type":"assistant/chunk","seq":425,"time":1784525381745,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":169,"outputTokens":98,"cacheReadTokens":3456,"reasoningTokens":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":"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"}}} +{"type":"assistant/chunk","seq":433,"time":1784525382334,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":434,"time":1784525382411,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" edit"}}} +{"type":"assistant/chunk","seq":435,"time":1784525382439,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" was"}}} +{"type":"assistant/chunk","seq":436,"time":1784525382440,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" applied"}}} +{"type":"assistant/chunk","seq":437,"time":1784525382440,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" successfully"}}} +{"type":"assistant/chunk","seq":438,"time":1784525382467,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":439,"time":1784525382468,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" Line"}}} +{"type":"assistant/chunk","seq":440,"time":1784525382468,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":441,"time":1784525382468,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":"2"}}} +{"type":"assistant/chunk","seq":442,"time":1784525382496,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" of"}}} +{"type":"assistant/chunk","seq":443,"time":1784525382497,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" notes"}}} +{"type":"assistant/chunk","seq":444,"time":1784525382497,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":".txt"}}} +{"type":"assistant/chunk","seq":445,"time":1784525382497,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" now"}}} +{"type":"assistant/chunk","seq":446,"time":1784525382524,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" reads"}}} +{"type":"assistant/chunk","seq":447,"time":1784525382524,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":448,"time":1784525382524,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":"hello"}}} +{"type":"assistant/chunk","seq":449,"time":1784525382524,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" world"}}} +{"type":"assistant/chunk","seq":450,"time":1784525382525,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":451,"time":1784525382553,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":" Done"}}} +{"type":"assistant/chunk","seq":452,"time":1784525382554,"data":{"turn":1,"step":6,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":453,"time":1784525382580,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":454,"time":1784525382580,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":"Done"}}} +{"type":"assistant/chunk","seq":455,"time":1784525382580,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":"."}}} +{"type":"assistant/chunk","seq":456,"time":1784525382608,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":" Line"}}} +{"type":"assistant/chunk","seq":457,"time":1784525382608,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":458,"time":1784525382609,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":"2"}}} +{"type":"assistant/chunk","seq":459,"time":1784525382609,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":" of"}}} +{"type":"assistant/chunk","seq":460,"time":1784525382636,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":461,"time":1784525382636,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":"notes"}}} +{"type":"assistant/chunk","seq":462,"time":1784525382636,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":".txt"}}} +{"type":"assistant/chunk","seq":463,"time":1784525382637,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":464,"time":1784525382637,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":" now"}}} +{"type":"assistant/chunk","seq":465,"time":1784525382637,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":" reads"}}} +{"type":"assistant/chunk","seq":466,"time":1784525382664,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":467,"time":1784525382664,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":"hello"}}} +{"type":"assistant/chunk","seq":468,"time":1784525382697,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":" world"}}} +{"type":"assistant/chunk","seq":469,"time":1784525382697,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":1,"text":"`."}}} +{"type":"assistant/chunk","seq":470,"time":1784525382697,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The edit was applied successfully. Line 2 of notes.txt now reads \"hello world\". Done."}}}} +{"type":"assistant/chunk","seq":471,"time":1784525382697,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"Done. Line 2 of `notes.txt` now reads `hello world`."}}}} +{"type":"assistant/chunk","seq":472,"time":1784525382697,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":196,"outputTokens":37,"cacheReadTokens":3584,"reasoningTokens":20}}}} +{"type":"assistant/chunk","seq":473,"time":1784525382697,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":474,"time":1784525382698,"data":{"turn":1,"step":6,"content":[{"type":"reasoning","text":"The edit was applied successfully. Line 2 of notes.txt now reads \"hello world\". Done."},{"type":"text","text":"Done. Line 2 of `notes.txt` now reads `hello world`."}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":196,"outputTokens":37,"cacheReadTokens":3584,"reasoningTokens":20}},"sourceEventSeqs":[432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473],"surfaceOp":"append"} +{"type":"step/end","seq":475,"time":1784525382698,"data":{"turn":1,"step":6}} +{"type":"turn/end","seq":476,"time":1784525382698,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":477,"time":1784525382723,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":478,"time":1784525382723,"data":{"content":[{"type":"text","text":"Read notes.txt (relative path notes.txt) and reply with the single word DONE if line 2 now says hello world."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":479,"time":1784525382724,"data":{"turn":2,"step":1}} +{"type":"assistant/chunk","seq":480,"time":1784525383445,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":481,"time":1784525383446,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":482,"time":1784525383458,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":483,"time":1784525383458,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":484,"time":1784525383458,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":"file"}}} +{"type":"assistant/chunk","seq":485,"time":1784525383459,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":"_path"}}} +{"type":"assistant/chunk","seq":486,"time":1784525383459,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":487,"time":1784525383459,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":488,"time":1784525383486,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":489,"time":1784525383486,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":"notes"}}} +{"type":"assistant/chunk","seq":490,"time":1784525383486,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":".txt"}}} +{"type":"assistant/chunk","seq":491,"time":1784525383486,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":492,"time":1784525383520,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":493,"time":1784525383584,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","arguments":"{\"file_path\": \"notes.txt\"}"}}}} +{"type":"assistant/chunk","seq":494,"time":1784525383584,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":115,"outputTokens":45,"cacheReadTokens":3712,"reasoningTokens":0}}}} +{"type":"assistant/chunk","seq":495,"time":1784525383585,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":496,"time":1784525383585,"data":{"turn":2,"step":1,"content":[{"type":"tool-call","id":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","arguments":"{\"file_path\": \"notes.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":115,"outputTokens":45,"cacheReadTokens":3712,"reasoningTokens":0}},"sourceEventSeqs":[480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495],"surfaceOp":"append"} +{"type":"tool/call","seq":497,"time":1784525383585,"data":{"turn":2,"step":1,"callId":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","name":"read","arguments":"{\"file_path\": \"notes.txt\"}"}} +{"type":"tool/result","seq":498,"time":1784525383589,"data":{"turn":2,"step":1,"callId":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","content":[{"type":"text","text":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-tia05W/notes.txt\nfile\n\n1: project notes\n2: hello world\n\n(End of file - total 2 lines)\n"}],"isError":false},"sourceEventSeqs":[497],"surfaceOp":"append"} +{"type":"step/end","seq":499,"time":1784525383590,"data":{"turn":2,"step":1}} +{"type":"step/start","seq":500,"time":1784525383590,"data":{"turn":2,"step":2}} +{"type":"assistant/chunk","seq":501,"time":1784525384009,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":502,"time":1784525384010,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"Line"}}} +{"type":"assistant/chunk","seq":503,"time":1784525384158,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":504,"time":1784525384189,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"2"}}} +{"type":"assistant/chunk","seq":505,"time":1784525384190,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" says"}}} +{"type":"assistant/chunk","seq":506,"time":1784525384190,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":507,"time":1784525384190,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"hello"}}} +{"type":"assistant/chunk","seq":508,"time":1784525384190,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" world"}}} +{"type":"assistant/chunk","seq":509,"time":1784525384191,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} +{"type":"assistant/chunk","seq":510,"time":1784525384221,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} +{"type":"assistant/chunk","seq":511,"time":1784525384222,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":512,"time":1784525384222,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":513,"time":1784525384255,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":514,"time":1784525384256,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":515,"time":1784525384256,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":516,"time":1784525384257,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":517,"time":1784525384284,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":518,"time":1784525384313,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":519,"time":1784525384313,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":520,"time":1784525384314,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":521,"time":1784525384348,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":522,"time":1784525384348,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} +{"type":"assistant/chunk","seq":523,"time":1784525384349,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":524,"time":1784525384349,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"Line 2 says \"hello world\". The user wants me to reply with a single word."}}}} +{"type":"assistant/chunk","seq":525,"time":1784525384349,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":526,"time":1784525384349,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":247,"outputTokens":22,"cacheReadTokens":3712,"reasoningTokens":19}}}} +{"type":"assistant/chunk","seq":527,"time":1784525384349,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":528,"time":1784525384350,"data":{"turn":2,"step":2,"content":[{"type":"reasoning","text":"Line 2 says \"hello world\". The user wants me to reply with a single word."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":247,"outputTokens":22,"cacheReadTokens":3712,"reasoningTokens":19}},"sourceEventSeqs":[501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527],"surfaceOp":"append"} +{"type":"step/end","seq":529,"time":1784525384350,"data":{"turn":2,"step":2}} +{"type":"turn/end","seq":530,"time":1784525384350,"data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/plan-mode/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/plan-mode/stdout.expected.jsonl new file mode 100644 index 0000000000..2fa56587ef --- /dev/null +++ b/examples/acp-agent/tests/snapshots/plan-mode/stdout.expected.jsonl @@ -0,0 +1,267 @@ +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"current_mode_update","currentModeId":"plan"}}} +{"jsonrpc":"2.0","id":3,"result":{}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"The typo is on line","updatedAt":"{{updatedAt}}"}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" wants"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":":\n"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"1"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Run"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" `"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"cat"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" notes"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":".txt"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"`"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" inspect"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" file"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\n"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"2"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Present"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" a"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" via"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" exit"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_mode"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\n"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"3"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" After"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" approval"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":","}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" make"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" edit"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\n\n"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"Let"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" start"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" step"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"1"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_jRYWnAwUo7zSx2WVsRZ83655","title":"cat notes.txt","kind":"execute","status":"in_progress","rawInput":"cat notes.txt","content":[{"type":"content","content":{"type":"text","text":"Inspect notes.txt contents"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_jRYWnAwUo7zSx2WVsRZ83655","status":"completed","content":[{"type":"content","content":{"type":"text","text":"```console\nproject notes\n- the greeting message still says \"helo wrld\"\n```"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" file"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" has"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" two"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" lines"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":":\n"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"-"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Line"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"1"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":":"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" `"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"project"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" notes"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"`\n"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"-"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Line"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"2"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":":"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" `"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"-"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" greeting"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" message"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" still"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" says"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"hel"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"o"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" wr"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ld"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"`\n\n"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" says"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" line"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"2"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" has"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" a"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" typ"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"o"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" should"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" say"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"hello"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" world"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" So"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" need"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" replace"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" line"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"2"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"hello"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" world"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\".\n\n"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"Let"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" present"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_6RLxuiGtAFswvfLnWdt63143","title":"Fix the greeting typo","kind":"other","status":"in_progress","content":[{"type":"content","content":{"type":"text","text":"# Fix the greeting typo\n\n## Single step\n1. **Edit line 2 of `notes.txt`** — Replace the content of line 2 (`- the greeting message still says \"helo wrld\"`) with `hello world`."}}]}}} +{"jsonrpc":"2.0","id":1,"method":"elicitation/create","params":{"sessionId":"{{sessionId}}","mode":"form","message":"Approve this plan and leave plan mode?\n\n# Fix the greeting typo\n\n## Single step\n1. **Edit line 2 of `notes.txt`** — Replace the content of line 2 (`- the greeting message still says \"helo wrld\"`) with `hello world`.","requestedSchema":{"type":"object","title":"Plan review","properties":{"choice":{"type":"string","title":"Approve this plan and leave plan mode?","description":"Choose one option, or fill a custom answer below.","oneOf":[{"const":"Approve","title":"Approve: Leave plan mode; the plan is carried out from the next step."},{"const":"Keep planning","title":"Keep planning: Stay in plan mode; feedback goes back to the model."}]},"custom":{"type":"string","title":"Custom answer","description":"Optional free-form answer. Leave empty to use the selected option."}},"required":[]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_6RLxuiGtAFswvfLnWdt63143","status":"completed","content":[{"type":"content","content":{"type":"text","text":"Plan approved — plan mode exited; carry out the plan starting with your next step."}}],"title":"Plan review"}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"current_mode_update","currentModeId":"default"}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" plan"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" is"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" approved"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Now"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" need"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" apply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" exactly"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" that"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" one"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" edit"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":":"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" change"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" line"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"2"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" of"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" notes"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":".txt"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" say"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"hello"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" world"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"'ll"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" use"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" edit"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" tool"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" replace"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" current"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" line"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"2"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" content"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","title":"Edit notes.txt","kind":"edit","status":"in_progress","locations":[{"path":"notes.txt"}],"content":[{"type":"diff","path":"notes.txt","oldText":"- the greeting message still says \"helo wrld\"","newText":"hello world"}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_eYuZ6fAuzqr8vtFd3oRg2452","status":"failed","content":[{"type":"content","content":{"type":"text","text":"Error: edit requires reading \"{{cwd}}/notes.txt\" first"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" need"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" read"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" file"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" first"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" before"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" editing"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" it"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Let"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" read"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" it"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" then"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" apply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" edit"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_XG9RoTfylfw20IP0dmrV7315","title":"Read notes.txt","kind":"read","status":"in_progress","locations":[{"path":"notes.txt","line":1}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_XG9RoTfylfw20IP0dmrV7315","status":"completed","content":[{"type":"content","content":{"type":"text","text":"{{cwd}}/notes.txt\nfile\n\n1: project notes\n2: - the greeting message still says \"helo wrld\"\n\n(End of file - total 2 lines)\n"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"Now"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" can"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" apply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" edit"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","title":"Edit notes.txt","kind":"edit","status":"in_progress","locations":[{"path":"notes.txt"}],"content":[{"type":"diff","path":"notes.txt","oldText":"- the greeting message still says \"helo wrld\"","newText":"hello world"}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_7H6VFdLjBqzzW9M5Z4OR8016","status":"completed","content":[{"type":"diff","path":"notes.txt","oldText":"project notes\n- the greeting message still says \"helo wrld\"","newText":"project notes\nhello world"}],"title":"Edit notes.txt"}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" edit"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" was"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" applied"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" successfully"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Line"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"2"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" of"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" notes"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":".txt"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" now"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reads"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"hello"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" world"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Done"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"Done"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" Line"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"2"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" of"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" `"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"notes"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":".txt"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"`"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" now"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" reads"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" `"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"hello"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":" world"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"`."}}}} +{"jsonrpc":"2.0","id":4,"result":{"stopReason":"end_turn"}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","title":"Read notes.txt","kind":"read","status":"in_progress","locations":[{"path":"notes.txt","line":1}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_ET_5aWcvKPYl6cZ73MsPcV29873","status":"completed","content":[{"type":"content","content":{"type":"text","text":"{{cwd}}/notes.txt\nfile\n\n1: project notes\n2: hello world\n\n(End of file - total 2 lines)\n"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"Line"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" "}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"2"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" says"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"hello"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" world"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" The"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" wants"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" me"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" a"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" single"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" word"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"D"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ONE"}}}} +{"jsonrpc":"2.0","id":5,"result":{"stopReason":"end_turn"}} diff --git a/examples/acp-agent/tests/snapshots/plan-mode/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/plan-mode/system-prompt.expected.md new file mode 100644 index 0000000000..df467239bd --- /dev/null +++ b/examples/acp-agent/tests/snapshots/plan-mode/system-prompt.expected.md @@ -0,0 +1,66 @@ +You are an AI agent powered by the DeepSeek Harness SDK. + +You are a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug. + +Verify your work by running the code or tests. Keep answers brief and factual. + + +You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode. + +Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery. + +The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed only to keep the request shape stable. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode. + +Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out. + +Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions. + +When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation. + + +Use 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. + +Use 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. + +Use 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. + +Check the [exit code: N] marker on every bash result; investigate failures before moving on. + +Track 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. + +Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked. + +Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`). + + +Use 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. + +Use 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. + + + +You are an AI agent powered by the DeepSeek Harness SDK. + +You are a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}. Your bash tool runs under a file sandbox — a `[sandbox: file access denied …]` result is policy, not a command bug. + +Verify your work by running the code or tests. Keep answers brief and factual. + + +Use 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. + +Use 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. + +Use 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. + +Check the [exit code: N] marker on every bash result; investigate failures before moving on. + +Track 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. + +Use goal tools for one long-running completion objective in the current session. create_goal may infer goal intent from a direct human request in any language; do not create a goal for routine single-turn work. Call get_goal before update_goal and copy its exact goal_id and revision. After session resume or fork, an active goal is disarmed: when a human asks to continue or resume in any wording or language, use update_goal action resume to rearm it. Mark complete only when the objective is actually achieved. Mark blocked only after the same blocking condition persists for at least 3 consecutive goal rounds, and report that concrete condition in blocked_reason; difficulty, uncertainty, or useful remaining work is not blocked. + +Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`). + + +Use 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. + +Use 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. diff --git a/examples/acp-agent/tests/snapshots/plan-mode/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/plan-mode/tool-schemas.expected.json new file mode 100644 index 0000000000..85acdd1ee1 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/plan-mode/tool-schemas.expected.json @@ -0,0 +1,1090 @@ +{ + "initial": [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, + { + "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", + "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." + }, + "sandbox_permissions": { + "type": "string", + "description": "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval.", + "enum": [ + "workspace-write", + "danger-full-access" + ] + }, + "justification": { + "type": "string", + "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access." + } + }, + "required": [ + "command", + "description" + ] + } + }, + { + "name": "create_goal", + "description": "Create one persisted same-session completion goal when the current direct human request is a long-running objective that should continue across autonomous goal rounds. You may infer that intent without requiring the user to say \"create a goal\". Do not use this for trivial single-turn work. Execution rejects non-human and subagent authority.", + "parameters": { + "type": "object", + "properties": { + "objective": { + "type": "string", + "description": "The concrete completion objective inferred from the direct human request." + }, + "max_goal_rounds": { + "type": "number", + "description": "Optional positive safe-integer limit on automatic continuation rounds." + } + }, + "required": [ + "objective" + ] + } + }, + { + "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." + }, + "sandbox_permissions": { + "type": "string", + "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.", + "enum": [ + "workspace-write", + "danger-full-access" + ] + }, + "justification": { + "type": "string", + "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access." + } + }, + "required": [ + "file_path", + "old_string", + "new_string" + ] + } + }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, + { + "name": "get_goal", + "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", + "parameters": { + "type": "object", + "properties": {} + } + }, + { + "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": "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": "update_goal", + "description": "Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason.", + "parameters": { + "type": "object", + "properties": { + "goal_id": { + "type": "string", + "description": "Exact id returned by get_goal." + }, + "revision": { + "type": "number", + "description": "Exact positive revision returned by get_goal." + }, + "action": { + "type": "string", + "description": "edit | pause | resume | complete | blocked", + "enum": [ + "edit", + "pause", + "resume", + "complete", + "blocked" + ] + }, + "objective": { + "type": "string", + "description": "Replacement objective; valid only with action edit." + }, + "max_goal_rounds": { + "type": "number", + "description": "Replacement cap; valid only with action edit." + }, + "blocked_reason": { + "type": "string", + "description": "Concrete blocking condition; required only with action blocked." + } + }, + "required": [ + "goal_id", + "revision", + "action" + ] + } + }, + { + "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." + }, + "sandbox_permissions": { + "type": "string", + "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.", + "enum": [ + "workspace-write", + "danger-full-access" + ] + }, + "justification": { + "type": "string", + "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access." + } + }, + "required": [ + "file_path", + "content" + ] + } + } + ], + "changes": [ + [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, + { + "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", + "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." + }, + "sandbox_permissions": { + "type": "string", + "description": "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval.", + "enum": [ + "workspace-write", + "danger-full-access" + ] + }, + "justification": { + "type": "string", + "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access." + } + }, + "required": [ + "command", + "description" + ] + } + }, + { + "name": "create_goal", + "description": "Create one persisted same-session completion goal when the current direct human request is a long-running objective that should continue across autonomous goal rounds. You may infer that intent without requiring the user to say \"create a goal\". Do not use this for trivial single-turn work. Execution rejects non-human and subagent authority.", + "parameters": { + "type": "object", + "properties": { + "objective": { + "type": "string", + "description": "The concrete completion objective inferred from the direct human request." + }, + "max_goal_rounds": { + "type": "number", + "description": "Optional positive safe-integer limit on automatic continuation rounds." + } + }, + "required": [ + "objective" + ] + } + }, + { + "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." + }, + "sandbox_permissions": { + "type": "string", + "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.", + "enum": [ + "workspace-write", + "danger-full-access" + ] + }, + "justification": { + "type": "string", + "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access." + } + }, + "required": [ + "file_path", + "old_string", + "new_string" + ] + } + }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, + { + "name": "get_goal", + "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", + "parameters": { + "type": "object", + "properties": {} + } + }, + { + "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": "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": "update_goal", + "description": "Update the exact current goal revision. edit, pause, and resume require a direct top-level human request. During an automatic continuation of the current goal, complete and blocked are also allowed. blocked is rejected before the configured minimum round count; the model remains responsible for judging that the same condition persisted across those rounds and must explain it in blocked_reason.", + "parameters": { + "type": "object", + "properties": { + "goal_id": { + "type": "string", + "description": "Exact id returned by get_goal." + }, + "revision": { + "type": "number", + "description": "Exact positive revision returned by get_goal." + }, + "action": { + "type": "string", + "description": "edit | pause | resume | complete | blocked", + "enum": [ + "edit", + "pause", + "resume", + "complete", + "blocked" + ] + }, + "objective": { + "type": "string", + "description": "Replacement objective; valid only with action edit." + }, + "max_goal_rounds": { + "type": "number", + "description": "Replacement cap; valid only with action edit." + }, + "blocked_reason": { + "type": "string", + "description": "Concrete blocking condition; required only with action blocked." + } + }, + "required": [ + "goal_id", + "revision", + "action" + ] + } + }, + { + "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." + }, + "sandbox_permissions": { + "type": "string", + "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval.", + "enum": [ + "workspace-write", + "danger-full-access" + ] + }, + "justification": { + "type": "string", + "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access." + } + }, + "required": [ + "file_path", + "content" + ] + } + } + ] + ] +} diff --git a/examples/acp-agent/tests/snapshots/plan-mode/workspace/notes.txt b/examples/acp-agent/tests/snapshots/plan-mode/workspace/notes.txt new file mode 100644 index 0000000000..0230df4b3b --- /dev/null +++ b/examples/acp-agent/tests/snapshots/plan-mode/workspace/notes.txt @@ -0,0 +1,2 @@ +project notes +- the greeting message still says "helo wrld" diff --git a/examples/acp-agent/tests/snapshots/repeat-tool-guard/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/repeat-tool-guard/stdout.expected.jsonl index 247cbecb8b..1b1155bfb8 100644 --- a/examples/acp-agent/tests/snapshots/repeat-tool-guard/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/repeat-tool-guard/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Write the todo list 'watch","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_1","title":"Update todo list","kind":"other","status":"in_progress","rawInput":[{"content":"watch the kettle boil","status":"in_progress"}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"plan","entries":[{"content":"watch the kettle boil","priority":"medium","status":"in_progress"}]}}} diff --git a/examples/acp-agent/tests/snapshots/skill-load/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/skill-load/stdout.expected.jsonl index d972b1a032..1c05855181 100644 --- a/examples/acp-agent/tests/snapshots/skill-load/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/skill-load/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Load the snapshot-skill skill with","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"Load the requested skill."}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_skill_load","title":"Load skill snapshot-skill","kind":"read","status":"in_progress","rawInput":"snapshot-skill"}}} diff --git a/examples/acp-agent/tests/snapshots/skill-load/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/skill-load/tool-schemas.expected.json index 52b3c1812e..61d317e2f8 100644 --- a/examples/acp-agent/tests/snapshots/skill-load/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/skill-load/tool-schemas.expected.json @@ -1,5 +1,66 @@ { "initial": [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, { "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", @@ -107,6 +168,22 @@ ] } }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, { "name": "get_goal", "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/stdout.expected.jsonl index 7b69668a59..44d7c5d000 100644 --- a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Delegate through two child generations.","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_root_child","title":"subagent","kind":"other","status":"in_progress","rawInput":{"description":"Start depth one","prompt":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_root_child","status":"completed","content":[{"type":"content","content":{"type":"text","text":"DEPTH_ONE_DONE"}}]}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-fork/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-fork/stdout.expected.jsonl index cfff8b76e5..01050bbb21 100644 --- a/examples/acp-agent/tests/snapshots/subagent-fork/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-fork/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Remember this fact for later:","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/stdout.expected.jsonl index 13008b8a8e..e413c72b36 100644 --- a/examples/acp-agent/tests/snapshots/subagent-mixed/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-mixed/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Remember this fact for later:","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/stdout.expected.jsonl index 20cabfa5ef..1636ccc9f4 100644 --- a/examples/acp-agent/tests/snapshots/subagent-multi/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-multi/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the subagent tool TWICE,","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn/stdout.expected.jsonl index 5127a672f0..70633ca4b5 100644 --- a/examples/acp-agent/tests/snapshots/subagent-spawn/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-spawn/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the subagent tool exactly","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/text-turn/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/text-turn/stdout.expected.jsonl index bba9f955f3..3ba70963d2 100644 --- a/examples/acp-agent/tests/snapshots/text-turn/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/text-turn/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Reply with exactly the word:","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json index 52b3c1812e..61d317e2f8 100644 --- a/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/text-turn/tool-schemas.expected.json @@ -1,5 +1,66 @@ { "initial": [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, { "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", @@ -107,6 +168,22 @@ ] } }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, { "name": "get_goal", "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", diff --git a/examples/acp-agent/tests/snapshots/todo-plan/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/todo-plan/stdout.expected.jsonl index c4911152ee..87c9de38fc 100644 --- a/examples/acp-agent/tests/snapshots/todo-plan/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/todo-plan/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the todo_write tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/tool-call-turn/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/tool-call-turn/stdout.expected.jsonl index b4f15657d6..7a76747755 100644 --- a/examples/acp-agent/tests/snapshots/tool-call-turn/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/tool-call-turn/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the bash tool to","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/workflow-run/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/workflow-run/stdout.expected.jsonl index e3a2ebb673..6769815989 100644 --- a/examples/acp-agent/tests/snapshots/workflow-run/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/workflow-run/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Use the workflow tool exactly","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/workspace-context/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/workspace-context/stdout.expected.jsonl index 4167839f4c..5ac3496843 100644 --- a/examples/acp-agent/tests/snapshots/workspace-context/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/workspace-context/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"Read nested/task.txt with the read","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_workspace_read","title":"Read nested/task.txt","kind":"read","status":"in_progress","locations":[{"path":"nested/task.txt","line":1}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_workspace_read","status":"completed","content":[{"type":"content","content":{"type":"text","text":"{{cwd}}/nested/task.txt\nfile\n\n1: snapshot task\n\n(End of file - total 1 lines)\n"}}]}}} diff --git a/examples/acp-agent/tests/snapshots/workspace-context/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/workspace-context/tool-schemas.expected.json index 52b3c1812e..61d317e2f8 100644 --- a/examples/acp-agent/tests/snapshots/workspace-context/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/workspace-context/tool-schemas.expected.json @@ -1,5 +1,66 @@ { "initial": [ + { + "name": "ask_user_question", + "description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.", + "parameters": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "description": "Questions to ask the user before continuing.", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Stable id for this question; echoed in the answer." + }, + "question": { + "type": "string", + "description": "The specific question to ask the user." + }, + "header": { + "type": "string", + "description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"." + }, + "options": { + "type": "array", + "description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Short user-facing option label." + }, + "description": { + "type": "string", + "description": "One sentence explaining the tradeoff or impact." + } + }, + "required": [ + "label" + ] + } + }, + "multi_select": { + "type": "boolean", + "description": "Whether the user may select more than one option. Defaults to false." + } + }, + "required": [ + "id", + "question" + ] + } + } + }, + "required": [ + "questions" + ] + } + }, { "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`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.", @@ -107,6 +168,22 @@ ] } }, + { + "name": "exit_plan_mode", + "description": "Use only in plan mode. Present your plan for the user's review and, on approval, leave plan mode. Send the COMPLETE plan as markdown, starting with a # heading that names it. The user may approve (carry out the plan from your next step) or keep planning — their feedback comes back in the tool result; revise and present again.", + "parameters": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "description": "The complete plan, as markdown, starting with a # heading that names it." + } + }, + "required": [ + "plan" + ] + } + }, { "name": "get_goal", "description": "Read the current same-session goal, including its exact id/revision, objective, phase, completed continuation rounds, round limit, blocker reason when present, and whether another continuation is armed. Call this before updating a goal.", diff --git a/examples/acp-agent/tests/snapshots/workspace-edit/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/workspace-edit/stdout.expected.jsonl index 16aba07874..2d9b6c418c 100644 --- a/examples/acp-agent/tests/snapshots/workspace-edit/stdout.expected.jsonl +++ b/examples/acp-agent/tests/snapshots/workspace-edit/stdout.expected.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"A file named greeting.txt in","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/acp-agent/tests/snapshots/workspace-edit/stdout.expected.windows.jsonl b/examples/acp-agent/tests/snapshots/workspace-edit/stdout.expected.windows.jsonl index f39ff91716..50c4c4209e 100644 --- a/examples/acp-agent/tests/snapshots/workspace-edit/stdout.expected.windows.jsonl +++ b/examples/acp-agent/tests/snapshots/workspace-edit/stdout.expected.windows.jsonl @@ -1,6 +1,6 @@ {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":1,"agentInfo":{"name":"deepseek-harness-acp","version":"0.0.1"},"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":false}},"authMethods":[]}} -{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} -{"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]"}}]}}} +{"jsonrpc":"2.0","id":2,"result":{"sessionId":"{{sessionId}}","modes":{"availableModes":[{"id":"default","name":"default"},{"id":"plan","name":"plan"}],"currentModeId":"default"},"configOptions":[{"id":"model","name":"Model","description":"Sets this session's provider and model.","category":"model","type":"select","currentValue":"[\"deepseek\",\"deepseek-v4-flash\"]","options":[{"value":"[\"deepseek\",\"deepseek-v4-flash\"]","name":"deepseek-v4-flash"},{"value":"[\"deepseek\",\"deepseek-v4-pro\"]","name":"deepseek-v4-pro"}]},{"id":"permission","name":"Permissions","description":"The session permission preset: each choice bundles a sandbox mode and an approval policy.","category":"mode","type":"select","currentValue":"danger-full-access","options":[{"value":"workspace-write","name":"workspace-write","description":"Write inside the workspace and permitted temporary directories; wider retries require approval."},{"value":"danger-full-access","name":"danger-full-access","description":"Full file access without approval prompts."}]}]}} +{"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 plan mode","input":{"hint":"[message]"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"session_info_update","title":"A file named greeting.txt in","updatedAt":"{{updatedAt}}"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" user"}}}} diff --git a/examples/package.json b/examples/package.json index ae22d0513d..1ceeb0930a 100644 --- a/examples/package.json +++ b/examples/package.json @@ -29,6 +29,7 @@ "@deepseek-ai/dsh-llm-replay": "workspace:*", "@deepseek-ai/dsh-lsp": "workspace:*", "@deepseek-ai/dsh-lsp-local": "workspace:*", + "@deepseek-ai/dsh-plan-mode": "workspace:*", "@deepseek-ai/dsh-permission": "workspace:*", "@deepseek-ai/dsh-repeat-tool-guard": "workspace:*", "@deepseek-ai/dsh-sandbox-local": "workspace:*", @@ -45,6 +46,7 @@ "@deepseek-ai/dsh-time-context": "workspace:*", "@deepseek-ai/dsh-timeout-policy": "workspace:*", "@deepseek-ai/dsh-token-meter": "workspace:*", + "@deepseek-ai/dsh-tool-ask-user": "workspace:*", "@deepseek-ai/dsh-tool-cordis": "workspace:*", "@deepseek-ai/dsh-tool-fs": "workspace:*", "@deepseek-ai/dsh-tool-fs-search": "workspace:*", diff --git a/examples/tui-agent/README.md b/examples/tui-agent/README.md index ecae86750b..0a26bb5a6b 100644 --- a/examples/tui-agent/README.md +++ b/examples/tui-agent/README.md @@ -1,6 +1,6 @@ # tui-agent -The interactive coding agent: DeepSeek V4 + the `read`/`write`/`edit` filesystem tools + the bash tool suite + subagent delegation + workflows + compaction + JSONL persistence, on the full-screen `dsh-tui` front door, loaded from `cordis.yml`. The sibling [`headless-agent`](../headless-agent/README.md) runs the same capability class as a one-shot pipe-friendly task, and [`acp-agent`](../acp-agent/README.md) serves it over JSON-RPC. +The full-screen interactive coding agent: DeepSeek V4, local bash and filesystem tools, compaction, subagents, workflows and fresh-agent Ralph iteration, plan mode (`/plan` enters and `exit_plan_mode` reviews the exit), timeout/spill policy, and JSONL persistence through [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo), loaded from `cordis.yml`. The sibling [`headless-agent`](../headless-agent/README.md) runs the same capability class as a one-shot pipe-friendly task, and [`acp-agent`](../acp-agent/README.md) serves it over JSON-RPC. ## Run it @@ -17,7 +17,7 @@ Type a coding task. The agent works through the `read`/`write`/`edit` filesystem The `todo_write` task tracker is opt-in and not in the shipped config: add `@deepseek-ai/dsh-tool-todo` to `cordis.yml` (or a personal-config overlay under `~/.dsh`) to expose it. Once loaded, the model records a whole-list plan to the session log and the TUI renders it. -The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and — when `todo_write` is loaded — the latest plan. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/status` expands the current session's identity, activity counts, exact token/cache buckets, context use, and timestamps without interrupting a running turn. `/model` opens a keyboard selector for the current provider catalog; use Up/Down and Enter, or `/model ` and `/model /` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options. +The TUI renders Markdown history, reasoning, tool-owned terminal/diff/generic cards, token totals, and — when `todo_write` is loaded — the latest plan. Long tool bodies keep a head/tail preview; Ctrl+O expands or collapses every card. Enter submits or steers while the agent runs, Ctrl+R toggles reasoning, Escape cancels, and `/help` lists commands. `/plan` selects plan mode for the next step; `/plan ` also submits the message into that step. `/status` expands the current session's identity, activity counts, exact token/cache buckets, context use, and timestamps without interrupting a running turn. `/model` opens a keyboard selector for the current provider catalog; use Up/Down and Enter, or `/model ` and `/model /` for direct selection. `ask_user_question` opens a wide bottom-left keyboard panel with batch progress and numbered options. ### Resuming a prior session @@ -57,6 +57,7 @@ This example is a thin leaf `cordis.yml`: it picks the swappable backends, loads | `subagent`, `subagent-spawn`, `subagent-fork` | the subagent provider registry plus the two in-process backends: a fresh child and a child seeded with the parent's completed-turn prefix | | `tool-subagent`, `tool-subagent-fork` | two model-facing `dsh-tool-subagent` loads, each bound to a different provider and exposed under a distinct tool name (`subagent`, `subagent_fork`) | | `workflow-workerthread`, `tool-workflow` | the worker-thread workflow engine and its model-facing `workflow` tool, with child calls routed through the spawn backend | +| `plan-mode` | the plugin-owned `/plan [message]` command, plan-mode prompt policy, tool restrictions, and reviewed `exit_plan_mode` transition | | `fs-local`, `fs-policy`, `tool-fs` | the filesystem stack: the local `ctx.fs` provider, the read-before-write/edit policy gate (on the `fs/*` event gate), and the model-facing `read`/`write`/`edit` tools. Relative paths resolve against the session workspace | ## End-to-end tests (`pnpm run test:e2e`) @@ -70,7 +71,7 @@ The UI-independent with-key suites assemble the full stack programmatically thro - `tests/todo-write.e2e.ts` — loads the opt-in `todo_write` tool, then a real model drives it and the test verifies the resulting `todo/write` session event. - `tests/code-mode.e2e.ts` — the with-key Code Mode proof: a real model, a two-tool task, asserting the wire tool list was exactly `[run_code]`, the `tool/code-dispatch` events landed under the parent call, and the curated answer came back. -These self-skip without `DEEPSEEK_API_KEY`. The keyless `tests/tui-keyless-smoke.e2e.ts` boots the real Loader tree in a PTY (the one sanctioned PTY surface): the base boot + `/exit`, a scripted-LLM conversation with a question dialog and tool round-trip, the Code Mode overlay welcome line, and the resume-failure exit path. +These self-skip without `DEEPSEEK_API_KEY`. The keyless `tests/tui-keyless-smoke.e2e.ts` boots the real Loader tree in a PTY (the one sanctioned PTY surface): the base boot + `/plan` + `/exit`, a scripted-LLM conversation with a question dialog and tool round-trip, the Code Mode overlay welcome line, and the resume-failure exit path. ## Snapshot tests diff --git a/examples/tui-agent/composition.md b/examples/tui-agent/composition.md index b380e7610c..fd6d163952 100644 --- a/examples/tui-agent/composition.md +++ b/examples/tui-agent/composition.md @@ -47,6 +47,8 @@ flowchart LR cfg --> plugin_tui_tool_workflow plugin_tui_tool_ralph["tool-ralph
@deepseek-ai/dsh-tool-ralph"] cfg --> plugin_tui_tool_ralph + plugin_tui_plan_mode["plan-mode
@deepseek-ai/dsh-plan-mode"] + cfg --> plugin_tui_plan_mode plugin_tui_fs_local["fs-local
@deepseek-ai/dsh-fs-local"] cfg --> plugin_tui_fs_local plugin_tui_fs_policy["fs-policy
@deepseek-ai/dsh-fs-policy"] @@ -81,6 +83,7 @@ flowchart LR | `workflow-workerthread` | `@deepseek-ai/dsh-workflow-workerthread` | | `tool-workflow` | `@deepseek-ai/dsh-tool-workflow` | | `tool-ralph` | `@deepseek-ai/dsh-tool-ralph` | +| `plan-mode` | `@deepseek-ai/dsh-plan-mode` | | `fs-local` | `@deepseek-ai/dsh-fs-local` | | `fs-policy` | `@deepseek-ai/dsh-fs-policy` | | `tool-fs` | `@deepseek-ai/dsh-tool-fs` | diff --git a/examples/tui-agent/cordis.yml b/examples/tui-agent/cordis.yml index e7a1c8877a..eeb44dc7fb 100644 --- a/examples/tui-agent/cordis.yml +++ b/examples/tui-agent/cordis.yml @@ -122,6 +122,24 @@ - id: tool-ralph name: '@deepseek-ai/dsh-tool-ralph' +# Plan mode gives the TUI a plugin-owned /plan [message] command; the exit +# review rides the TUI's user-interaction provider. +- id: plan-mode + name: '@deepseek-ai/dsh-plan-mode' + config: + section: | + You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode. + + Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery. + + The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed only to keep the request shape stable. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode. + + Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out. + + Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions. + + When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation. + # Policy loads before the model-facing filesystem tools so writes and edits require # an observed file. This single-session app resolves relative paths from the process cwd. - id: fs-local diff --git a/examples/tui-agent/tests/fixtures/tui-scripted.cordis.yml b/examples/tui-agent/tests/fixtures/tui-scripted.cordis.yml index cfc7a8e344..e4548da79e 100644 --- a/examples/tui-agent/tests/fixtures/tui-scripted.cordis.yml +++ b/examples/tui-agent/tests/fixtures/tui-scripted.cordis.yml @@ -15,6 +15,11 @@ - id: token-meter name: '@deepseek-ai/dsh-token-meter' +- id: plan-mode + name: '@deepseek-ai/dsh-plan-mode' + config: + section: 'Stay in plan mode for this scripted TUI test.' + - id: tui-agent name: '@deepseek-ai/dsh-tui-demo' config: 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 43d589e9ac..de5389c059 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 @@ -1,7 +1,7 @@ terminal 100x36 buffer=normal length=36 base=0 viewport=0 lifecycle started=1 stopped=0 progress=inactive title "Reply with exactly the word: — DSH TUI snapshot" -cursor hidden column=1 viewportRow=26 bufferRow=26 +cursor hidden column=1 viewportRow=28 bufferRow=28 buffer 0| " DEEPSEEK HARNESS" style 1-8 fg=bright-blue bold @@ -11,50 +11,53 @@ buffer 2| " deepseek-v4-flash • main-session" style 1-34 dim 3| -4| "▌ " +4| " Entering plan mode (applies from the next step). " + style 1-48 fg=bright-black +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| "▌ Reply with exactly the word: ONE. No tools. " +8| "▌ Reply with exactly the word: ONE. No tools. " style 0-0 fg=bright-blue -7| "▌ " +9| "▌ " style 0-0 fg=bright-blue -8| -9| " Reasoning " +10| +11| " Reasoning " style 1-9 fg=bright-black italic -10| " The user wants me to reply with exactly the word \"ONE\" and use no tools. " +12| " The user wants me to reply with exactly the word \"ONE\" and use no tools. " style 1-72 fg=bright-black italic -11| -12| " Assistant " +13| +14| " Assistant " style 1-9 fg=bright-magenta bold -13| " ONE " -14| -15| "▌ " +15| " ONE " +16| +17| "▌ " style 0-0 fg=bright-blue -16| "▌ You " +18| "▌ You " style 0-0 fg=bright-blue style 2-4 fg=bright-blue bold -17| "▌ Reply with exactly the word: TWO. No tools. " +19| "▌ Reply with exactly the word: TWO. No tools. " style 0-0 fg=bright-blue -18| "▌ " +20| "▌ " style 0-0 fg=bright-blue -19| -20| " Reasoning " +21| +22| " Reasoning " style 1-9 fg=bright-black italic -21| " The user wants me to reply with exactly the word \"TWO\" and no tools. " +23| " The user wants me to reply with exactly the word \"TWO\" and no tools. " style 1-68 fg=bright-black italic -22| -23| " Assistant " +24| +25| " Assistant " style 1-9 fg=bright-magenta bold -24| " TWO " -25| "────────────────────────────────────────────────────────────────────────────────────────────────────" - style 0-99 dim -26| " " - style 1-1 inverse +26| " TWO " 27| "────────────────────────────────────────────────────────────────────────────────────────────────────" style 0-99 dim -28| "deepseek-v4-flash /workspace/project ↑2.9k ↓41 cache 49% 3% co" +28| " " + style 1-1 inverse +29| "────────────────────────────────────────────────────────────────────────────────────────────────────" + style 0-99 dim +30| "deepseek-v4-flash /workspace/project ↑2.9k ↓41 cache 49% 3% co" style 0-92 dim style 95-99 dim -29-35| +31-35| diff --git a/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts b/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts index e9991b1404..c5d6fa36f3 100644 --- a/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts +++ b/examples/tui-agent/tests/tui-keyless-smoke.e2e.ts @@ -70,17 +70,21 @@ const SELECT_PRO_MODEL = [ ] as const describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => { - it('boots pi-tui, sweeps the borderless banner in, accepts /exit, and restores the terminal', async () => { + it('boots pi-tui, sweeps the borderless banner in, enters plan mode, and restores the terminal', async () => { // With no configured welcome the borderless banner sweeps in left-to-right; // the detail line's session id (`main-session-`) renders only once // the sweep reaches it, so it marks a settled banner. const output = await smoke({ label: 'tui-agent boot', - actions: [{ waitFor: 'main-session-', send: '/exit\r' }], + actions: [ + { waitFor: 'main-session-', send: '/plan\r' }, + { waitFor: 'Entering plan mode (applies from the next step).', send: '/exit\r' }, + ], }) expect(output).toContain('DEEPSEEK') expect(output).toContain('HARNESS') expect(output).toContain('main-session-') + expect(output).toContain('Entering plan mode (applies from the next step).') // Borderless: no box-drawing frame around the banner. expect(output).not.toContain('╭') expect(output).not.toContain('╮') @@ -94,7 +98,7 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => { configPath: scriptedConfigPath, actions: [ ...SELECT_PRO_MODEL, - { waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: 'exercise the TUI\r' }, + { waitFor: 'Model selected: tui-scripted/tui-scripted-model-pro.', send: '/plan exercise the TUI\r' }, { waitFor: 'How should the scripted run proceed?', send: '\r' }, { waitFor: 'Decision received. Scripted TUI run complete.', send: '' }, // Session title: the first user message drives the first-message-llm @@ -108,6 +112,7 @@ describe('tui-agent keyless smoke (real Loader tree in a PTY)', () => { ], }) expect(output).toContain('I need one decision before I continue.') + expect(output).toContain('Entering plan mode (applies from the next step).') expect(output).toContain(String.raw`\x1b]2;MODEL_CONTROLLED\x07`) expect(output).toContain(String.raw`\x1b[999CMODEL_CURSOR`) expect(output).toContain(String.raw`\x9b31mMODEL_C1`) diff --git a/examples/tui-agent/tests/tui.snapshot.ts b/examples/tui-agent/tests/tui.snapshot.ts index e0272d8a71..a36b004def 100644 --- a/examples/tui-agent/tests/tui.snapshot.ts +++ b/examples/tui-agent/tests/tui.snapshot.ts @@ -15,6 +15,7 @@ import * as FsPolicy from '@deepseek-ai/dsh-fs-policy' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import { installLlmReplay, parseSessionLog } from '@deepseek-ai/dsh-llm-replay' +import PlanModeService from '@deepseek-ai/dsh-plan-mode' import TokenMeterService from '@deepseek-ai/dsh-token-meter' import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' import { SessionId } from '@deepseek-ai/dsh-session' @@ -45,6 +46,7 @@ interface Scenario { expectedTools: string[] expectedEventCounts?: Record childSessions?: number + enterPlanMode?: boolean recorded: boolean seedWorkspace?: boolean /** @@ -60,6 +62,8 @@ const SCENARIOS: Scenario[] = [ name: 'multi-turn-conversation', composition: 'native', expectedTools: [], + expectedEventCounts: { 'plan/mode': 1 }, + enterPlanMode: true, recorded: true, }, { @@ -213,6 +217,9 @@ async function mountScenarioContext( await ctx.plugin(ToolWorkflow) await ctx.plugin(ToolRalph) await ctx.plugin(CommandService) + if (scenario.enterPlanMode === true) { + await ctx.plugin(PlanModeService, { section: 'Snapshot plan mode instructions.' }) + } if (scenario.composition === 'code' || scenario.composition === 'advanced') { await ctx.plugin(WorkerCodeRuntime, {}) } @@ -277,7 +284,17 @@ async function runScenario(scenario: Scenario): Promise { }) await settleTerminal(terminal) - for (const prompt of prompts) { + let remainingPrompts = prompts + if (scenario.enterPlanMode === true) { + const firstPrompt = prompts[0]! + terminal.send(`/plan ${firstPrompt}`) + terminal.send('\r') + await agent.whenIdle() + await settleTerminal(terminal) + remainingPrompts = prompts.slice(1) + } + + for (const prompt of remainingPrompts) { terminal.send(prompt) terminal.send('\r') await agent.whenIdle() @@ -289,6 +306,18 @@ async function runScenario(scenario: Scenario): Promise { for (const [type, count] of Object.entries(scenario.expectedEventCounts ?? {})) { expect(events.filter(event => event.type === type), `${scenario.name} must emit ${type}`).toHaveLength(count) } + if (scenario.enterPlanMode === true) { + expect(ctx.planMode.get(agent)).toEqual({ active: true }) + const planMode = events.find(event => event.type === 'plan/mode') + const firstHeader = events.find(event => event.type === 'request/header') + if (planMode === undefined || firstHeader === undefined) { + throw new Error('plan-mode command snapshot needs plan/mode before its first request/header') + } + expect(planMode.seq).toBeLessThan(firstHeader.seq) + expect(firstHeader.data.header.system).toContain('Snapshot plan mode instructions.') + const firstMessage = events.find(event => event.type === 'user/message') + expect(firstMessage?.data.content).toEqual([{ type: 'text', text: prompts[0] }]) + } expect(events.filter(event => event.type === 'tool/result').every(event => !event.data.isError)).toBe(true) expect(events.filter(event => event.type === 'turn/end').every(event => event.data.reason.kind !== 'error')).toBe(true) if (scenario.name === 'dynamic-workflow' || scenario.name === 'cordis-dynamic-toolchain') { diff --git a/package.json b/package.json index 711c6cb03a..cab2592fea 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "check:ci:coverage": "tsx scripts/run-gates.ts ci-coverage", "check:ci:snapshot": "tsx scripts/run-gates.ts ci-snapshot", "check:ci:artifacts": "tsx scripts/run-gates.ts ci-artifacts", + "check:ci:windows-blocking": "tsx scripts/run-gates.ts ci-windows-blocking", + "check:ci:windows-complete": "tsx scripts/run-gates.ts ci-windows-complete", + "check:ci:windows-observational": "tsx scripts/run-gates.ts ci-windows-observational", "check:node-compat": "tsx scripts/run-gates.ts node-compat", "knip": "knip --treat-config-hints-as-errors", "publint": "tsx scripts/publint-all.ts", @@ -55,6 +58,7 @@ "verify-doc-budgets": "tsx scripts/verify-doc-budgets.ts", "docs:dev": "pnpm --filter @deepseek-ai/website run dev", "docs:build": "pnpm --filter @deepseek-ai/website run build", + "docs:build:mpa": "pnpm --filter @deepseek-ai/website exec vitepress build . --mpa", "docs:preview": "pnpm --filter @deepseek-ai/website run preview", "docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts && pnpm run docs:build", "website:dev": "pnpm run docs:dev", diff --git a/packages/README.md b/packages/README.md index fdba51405e..932d4c1562 100644 --- a/packages/README.md +++ b/packages/README.md @@ -11,10 +11,10 @@ Packages live at `packages///`; groups are containers, while names r | [`core/`](core/README.md) | Product API spine: sessions, prompts, tools, agent services, and the concrete loop | Product — stable surface | | [`goal/`](goal/README.md) | Persisted same-session goal state and lifecycle | Product — stable surface | | [`llm/`](llm/README.md) | LLM capability family: the abstract service + provider adapters | Product — stable surface | -| [`bash/`](bash/README.md) | Bash capability family: the executor seam, a local impl, and the model-facing tool | Product — stable surface | -| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: the abstract runtime seam for model-written programs + a worker-thread backend | Product — stable surface | +| [`bash/`](bash/README.md) | Bash capability family: executor seam, local impl, model-facing tool | Product — stable surface | +| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: the runtime seam for model-written programs + a worker-thread backend | Product — stable surface | | [`sandbox/`](sandbox/README.md) | Process-confinement seam; bwrap/Landlock/Seatbelt backends | Product — stable surface | -| [`fs/`](fs/README.md) | Filesystem capability family: the abstract seam, a local impl, the model-facing file tools, and the bash-backed discovery tools | Product — stable surface | +| [`fs/`](fs/README.md) | Filesystem capability family: seam, local impl, model-facing file tools, bash-backed discovery tools | Product — stable surface | | [`lsp/`](lsp/README.md) | LSP capability family: seam, generic stdio provider, and the `lsp` tool | Product — stable surface | | [`skill/`](skill/README.md) | Skill capability family: the provider registry, local provider, and model-facing catalog/loader | Product — stable surface | | [`compact/`](compact/README.md) | Compaction capability family: the abstract seam + a basic backend (tool deferred) | Product — stable surface | @@ -22,9 +22,10 @@ Packages live at `packages///`; groups are containers, while names r | [`subagent/`](subagent/README.md) | Subagent capability family: the provider-registry seam and the model-facing delegation tool | Product — stable surface | | [`tasks/`](tasks/README.md) | Generic background-task runtime and model-facing `task_*` control tools | Product — stable surface | | [`workflow/`](workflow/README.md) | Workflow capability family: the script-engine seam, worker-thread engine, and model-facing `workflow` and fresh-agent `ralph` tools | Product — stable surface | -| [`web/`](web/README.md) | Web capability family: the abstract seam, search/fetch provider impls, and the model-facing web tools | Product — stable surface | -| [`spill/`](spill/README.md) | Spill capability family: the storage seam, a local impl, and the tool-result spill policy | Product — stable surface | +| [`web/`](web/README.md) | Web capability family: seam, search/fetch provider impls, and the model-facing web tools | Product — stable surface | +| [`spill/`](spill/README.md) | Spill capability family: storage seam, local impl, tool-result spill policy | Product — stable surface | | [`todo/`](todo/README.md) | Todo/planning family: the model-facing `todo_write` tool | Product — stable surface | +| [`plan/`](plan/README.md) | Plan collaboration state with a direct entry command and reviewed exit | Product — stable surface | | [`timeout/`](timeout/README.md) | Tool-call timeout policy: the `tools/execute` deadline enforcer | Product — stable surface | | [`guard/`](guard/README.md) | Loop-hygiene guards: advisory repeat-call reminders | Product — stable surface | | [`cordis/`](cordis/README.md) | Self-referential runtime toolset: inspect the live runtime's plugins and services, mount/unmount model-written plugins ([design](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)) | Product — stable surface | diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 1929fa7c81..d747d97506 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -372,6 +372,20 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, ], }, + { + key: 'planMode', + summary: '`ctx.planMode`: owns logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool.', + methods: [ + { + signature: 'get(agent: Agent): { active: boolean; pending?: boolean }', + jsDoc: '/**\n * Read the logged plan state and any selected state awaiting a boundary.\n *\n * @param agent The agent to read.\n * @returns Current logged state plus a pending selection, when present.\n */', + }, + { + signature: 'set(agent: Agent, active: boolean): void', + jsDoc: '/**\n * Select whether plan mode should be active from the next turn boundary.\n * Repeated selection of the current or already-pending state is a no-op.\n *\n * @param agent The agent to switch.\n * @param active Whether plan mode should be active.\n */', + }, + ], + }, { key: 'sandbox', summary: 'Abstract process-sandbox service.', @@ -1098,7 +1112,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'AskUserQuestionItem', - declaration: 'export interface AskUserQuestionItem {\n id: string;\n question: string;\n header?: string;\n options?: AskUserQuestionOption[];\n multiSelect?: boolean;\n}', + declaration: 'export interface AskUserQuestionItem {\n id: string;\n question: string;\n detail?: string;\n header?: string;\n options?: AskUserQuestionOption[];\n multiSelect?: boolean;\n}', }, { name: 'AskUserQuestionOption', diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index 54a9d8e794..8aa5c9b09e 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -23,7 +23,7 @@ describe('gen-tool-catalog collectToolCatalog', () => { it('boots every shipped tool package and harvests its model-facing schemas', async () => { const catalog = await collectToolCatalog() const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort() - expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'get_goal', 'glob', 'grep', 'lsp', 'ralph', 'read', 'run_code', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write']) + expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'lsp', 'ralph', 'read', 'run_code', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write']) // Every tool carries a JSON-Schema `parameters` object (what the model sees). for (const entry of catalog) { for (const schema of entry.schemas) { diff --git a/packages/llm/llm-deepseek/src/serialize.ts b/packages/llm/llm-deepseek/src/serialize.ts index 6c43772dbc..f463a4e30e 100644 --- a/packages/llm/llm-deepseek/src/serialize.ts +++ b/packages/llm/llm-deepseek/src/serialize.ts @@ -40,10 +40,15 @@ function serializeAssistant(message: Message): WireMessage { return { role: 'assistant', - // Tool-call turns send "" rather than null: the live API answers both, - // but the official samples replay message.content verbatim (which is "" - // for pure tool-call responses) and some gateways reject null outright. - content: text.length > 0 ? text : toolCalls.length > 0 ? '' : null, + // Text-less turns send "" — NEVER null. Pure tool-call turns: the + // official samples replay message.content verbatim (which is "") and + // some gateways reject null outright. Reasoning-ONLY turns (the model + // can answer entirely in the reasoning channel, e.g. a v4-flash + // greeting): the live API rejects null-content/no-tool_calls assistant + // messages with a 400 ("content or tool_calls must be set"), and since + // the message sits durably in the session log, a null here bricks every + // later turn of that session. + content: text, // Official passback rule (guides/thinking_mode.mdx): reasoning_content // must return on tool-call turns; it is ignored on plain turns, so we // drop it there to save tokens. diff --git a/packages/llm/llm-deepseek/tests/serialize.spec.ts b/packages/llm/llm-deepseek/tests/serialize.spec.ts index 8d3b190ed1..e84909fc20 100644 --- a/packages/llm/llm-deepseek/tests/serialize.spec.ts +++ b/packages/llm/llm-deepseek/tests/serialize.spec.ts @@ -187,12 +187,22 @@ describe('serializeRequest', () => { }) }) -describe('assistant empty and tool-call content shapes', () => { - it('serializes a content-less, tool-call-less assistant message as null content', () => { - // Aborted/empty assistant turns: no text, no calls → null (the wire - // accepts it; "" is reserved for tool-call turns per the samples). +describe('review fixes: assistant content shapes', () => { + it('serializes a content-less, tool-call-less assistant message as "" content, never null', () => { + // Aborted/empty assistant turns: no text, no calls → "". The earlier + // null shape was live-falsified: the API 400s a null-content assistant + // message without tool_calls ("content or tool_calls must be set"). const wire = serializeMessages([{ role: 'assistant', content: [] }]) - expect(wire).toEqual([{ role: 'assistant', content: null }]) + expect(wire).toEqual([{ role: 'assistant', content: '' }]) + }) + + it('serializes a reasoning-ONLY assistant message as "" content with the reasoning dropped', () => { + // The model can answer entirely in the reasoning channel (a v4-flash + // greeting did, live). The passback rule keeps reasoning_content off + // plain turns, and content must still be SET — a null here poisoned the + // session log and bricked every later turn of that session. + const wire = serializeMessages([{ role: 'assistant', content: [{ type: 'reasoning', text: '你好!有什么我可以帮你的吗?' }] }]) + expect(wire).toEqual([{ role: 'assistant', content: '' }]) }) it('serializes tool-call turns with empty string content, not null', () => { diff --git a/packages/lsp/lsp-local/tests/lifecycle.spec.ts b/packages/lsp/lsp-local/tests/lifecycle.spec.ts index 47b8d78bb4..8a556a01e9 100644 --- a/packages/lsp/lsp-local/tests/lifecycle.spec.ts +++ b/packages/lsp/lsp-local/tests/lifecycle.spec.ts @@ -1,11 +1,11 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { realpath } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { pathToFileURL, fileURLToPath } from 'node:url' import { Context } from 'cordis' -import Lsp, { type LspQueryRequest, type LspQueryResult } from '@deepseek-ai/dsh-lsp' +import Lsp, { type LspProvider, type LspQueryRequest, type LspQueryResult } from '@deepseek-ai/dsh-lsp' import { deadline } from '@deepseek-ai/dsh-timeout' import * as LspLocal from '@deepseek-ai/dsh-lsp-local' import type { LspLocalServerConfig } from '@deepseek-ai/dsh-lsp-local' @@ -38,12 +38,27 @@ function fakeServer(fakeEnv: Record = {}, overrides: Partial = {}, overrides: Partial = {}): Promise { +async function mount( + fakeEnv: Record = {}, + overrides: Partial = {}, + captureProvider?: (provider: LspProvider) => void, +): Promise { const ctx = new Context() await ctx.plugin(Lsp) - await ctx.plugin(LspLocal, { - servers: { fake: fakeServer(fakeEnv, overrides) }, - }) + const register = ctx.lsp.registerProvider.bind(ctx.lsp) + const registrationSpy = captureProvider === undefined + ? undefined + : vi.spyOn(ctx.lsp, 'registerProvider').mockImplementation((provider) => { + captureProvider(provider) + return register(provider) + }) + try { + await ctx.plugin(LspLocal, { + servers: { fake: fakeServer(fakeEnv, overrides) }, + }) + } finally { + registrationSpy?.mockRestore() + } return ctx } @@ -247,10 +262,22 @@ describe('lsp-local end to end over a fake server', () => { // The first query succeeds, then the server exits before the second arrives, leaving a dead // instance in the pool. The next query must evict-and-replace it and still succeed, rather than // failing once on the closed connection first. - const ctx = await mount({ LSP_FAKE_EXIT_AFTER_REPLY: '1', LSP_FAKE_DEF: JSON.stringify(locationJson(0)) }) + let provider: LspProvider | undefined + const ctx = await mount( + { LSP_FAKE_EXIT_AFTER_REPLY: '1', LSP_FAKE_DEF: JSON.stringify(locationJson(0)) }, + {}, + (registered) => { provider = registered }, + ) expect(await ctx.lsp.query(query('goToDefinition'))).toMatchObject({ kind: 'locations' }) - // Wait past the fixture's post-reply exit so the pooled instance is observably dead. - await new Promise(resolve => setTimeout(resolve, 60)) + if (provider === undefined) throw new Error('expected lsp-local to register a provider') + // This implementation-local test reaches the private pool only to synchronize with its actual + // close state. A fixed wall-clock sleep can expire before a CPU-starved child runs its exit timer. + const instances = (provider as unknown as { + readonly instances: ReadonlyMap + }).instances + const instance = [...instances.values()][0] + if (instance === undefined) throw new Error('expected one pooled LSP instance') + await waitFor(async () => instance.dead) expect(await ctx.lsp.query(query('goToDefinition'))).toMatchObject({ kind: 'locations' }) await ctx.fiber.dispose() }) diff --git a/packages/plan/README.md b/packages/plan/README.md new file mode 100644 index 0000000000..a1f73d191f --- /dev/null +++ b/packages/plan/README.md @@ -0,0 +1,9 @@ +# plan/ — plan collaboration state + +Plan mode is one logged, per-agent collaboration state. It is a single **product** package, not a generic mode registry or a capability-seam trio. + +| Package | Role | ctx key | +|---|---|---| +| `plan-mode/` | `plan/mode` vocabulary + fold, boundary-applied state, the `plan:policy` guidance section, `/plan [message]`, and the model-facing `exit_plan_mode` review tool | `ctx.planMode` | + +The active state is a pure function of the session log, so resume and fork restore it without extra machinery. The deployment supplies plan instructions through Cordis config, while `exit_plan_mode` stays registered when planning is inactive to keep the request tool catalog stable. ACP maps this capability onto its generic `default` / `plan` picker; sandbox mode and approval policy remain independent enforcement settings. Design: [plan-mode Agent Note](../../.agents/notes/implemented/feature/2026-07-07-plan-mode.md) and [plan-specific state simplification](../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md). diff --git a/packages/plan/plan-mode/README.md b/packages/plan/plan-mode/README.md new file mode 100644 index 0000000000..1c43c37b7f --- /dev/null +++ b/packages/plan/plan-mode/README.md @@ -0,0 +1,88 @@ +# @deepseek-ai/dsh-plan-mode + +Logged, per-agent plan collaboration state with deployment-owned guidance, a direct `/plan [message]` entry command, and the reviewed `exit_plan_mode` exit. Plan mode is soft guidance; sandbox mode and approval policy remain independent enforcement axes. + +## Durable state + +`plan/mode` (`{ active: boolean }`) is a log-only, whole-value-replace `SessionEventMap` member. `foldPlanMode(events)` returns the last logged value or `false`, so resume, fork, and compaction recover plan state directly from the session log. UIs observe committed flips through `session/event`. + +`ctx.planMode.set(agent, active)` records a pending selection and flushes it inside the next turn boundary. `get(agent)` returns `{ active, pending? }`, separating the logged state shaping the current step from a user's optimistic selection. Prompt submission, ordinary continuation, and request-recovery retry are all covered; a changed user selection contributes one `context/message` notice when the last logged request header described the other state. + +## Model and human surfaces + +While active, `plan:policy` renders the configured `section`. The plugin always registers `exit_plan_mode`, keeping tool schemas stable across the transition; its execute path accepts only active plan mode and leaves it only after an exact user approval through `ctx.userInteraction`. + +When `ctx.commands` is composed, the package registers `/plan [message]`. The command selects plan mode first. A non-empty argument is then submitted through `agent.steer()`, so it becomes the next step's ordinary logged user message under plan guidance; bare `/plan` only changes state. + +ACP is an adapter, not the owner of this vocabulary: it advertises the fixed wire ids `default` and `plan`, maps `session/set_mode` to the boolean service, and translates committed `plan/mode` events back to `current_mode_update`. + +## Configuration + +```yaml +- id: plan-mode + name: '@deepseek-ai/dsh-plan-mode' + config: + section: | + You are in plan mode. Explore and design before presenting the complete + plan through exit_plan_mode. +``` + +`section` is required and non-empty. Unknown keys fail at load. The package does not accept arbitrary named modes, tool filters, sandbox settings, or approval policy. + +Design: [plan-mode Agent Note](../../../.agents/notes/implemented/feature/2026-07-07-plan-mode.md) and [plan-specific state simplification](../../../.agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md). + +## Model Experience + +### Plan policy system prompt + +#### What the model sees + +While plan mode is active, the model sees the deployment's exact `section` text at prompt order 50; inactive mode contributes no text. + +##### Configuration example + +```markdown +You are in plan mode. Explore and design before presenting the complete plan through exit_plan_mode. +``` + +#### Token effect + +Inactive mode adds no tokens; active mode adds the configured section to every request. + +#### KV Cache effect + +The section is stable within plan mode, but entering or leaving changes the system prompt from order 50 onward. + +### Optional command message + +#### What the model sees + +`/plan` and its terminal result stay outside model history; a non-empty suffix becomes one trimmed user text block through `agent.steer()` after plan mode is selected. + +#### Token effect + +The suffix costs the same history tokens as submitting that text separately; a bare command adds none. + +#### KV Cache effect + +The user block is append-only conversation growth, while entering plan mode also changes the earlier policy section. + +### Exit tool schema and review exchange + +#### What the model sees + +The [`exit_plan_mode` schema](../../../docs/tool-catalog.md#deepseek-aidsh-plan-mode) remains available in both states; execution outside plan mode fails, while an approved in-mode review returns the exit result and rejection returns feedback. + +#### Token effect + +The stable schema is paid according to ToolRegistry mode, and each plan argument and review result remains in conversation history. + +#### KV Cache effect + +Mode transitions do not change the tool catalog; plan arguments and review results extend the conversation normally. + +## Known Limitations and Deferred Work + +- Plan mode guides rather than enforces; deployments needing a hard boundary must combine independent sandbox and approval controls. +- A pending selection made while idle is lost if the process exits before the next boundary, so the UI must reapply it. +- Forked agents inherit logged plan state, while newly spawned agents begin inactive; there is no creation-time plan option. diff --git a/packages/plan/plan-mode/package.json b/packages/plan/plan-mode/package.json new file mode 100644 index 0000000000..53e4cb11a1 --- /dev/null +++ b/packages/plan/plan-mode/package.json @@ -0,0 +1,57 @@ +{ + "name": "@deepseek-ai/dsh-plan-mode", + "description": "Logged per-agent plan mode with deployment guidance, a direct slash command, and a user-reviewed exit", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/invariant.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", + "src" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "@deepseek-ai/dsh-agent": "^0.0.1", + "@deepseek-ai/dsh-commands": "^0.0.1", + "@deepseek-ai/dsh-invariants": "^0.0.1", + "@deepseek-ai/dsh-session": "^0.0.1", + "@deepseek-ai/dsh-system-prompt": "^0.0.1", + "@deepseek-ai/dsh-tools": "^0.0.1", + "@deepseek-ai/dsh-user-interaction": "^0.0.1", + "cordis": "^4.0.0-rc.7" + }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-commands": { + "optional": true + } + }, + "devDependencies": { + "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-loop": "workspace:^", + "@deepseek-ai/dsh-code-runtime": "workspace:^", + "@deepseek-ai/dsh-commands": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-user-interaction": "workspace:^", + "cordis": "^4.0.0-rc.7" + } +} diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts new file mode 100644 index 0000000000..27d54c5a69 --- /dev/null +++ b/packages/plan/plan-mode/src/index.ts @@ -0,0 +1,342 @@ +/** + * Plan mode is logged per-agent collaboration state: while active, a + * deployment-owned guidance section shapes each model request, and + * `exit_plan_mode` presents the completed plan for user review. It is + * independent of sandbox mode and approval policy; those enforcement axes do + * not read or write plan state. + * + * The state in force is folded from the session log (`plan/mode`, last one + * wins), so resume and fork restore it without a live mirror. User selections + * are held as pending intent until a turn boundary because every session event + * is turn-enclosed. The service flushes before the affected request assembly + * on prompt submission, ordinary continuation, and request-recovery retry. + * + * The exit tool remains registered while plan mode is inactive so crossing a + * boundary changes only the prompt section, not the request tool catalog. + * + * Agent Notes: + * - .agents/notes/implemented/feature/2026-07-07-plan-mode.md + * - .agents/notes/implemented/simplification/2026-07-22-plan-specific-collaboration-state.md + * + * @module @deepseek-ai/dsh-plan-mode + */ + +import { Context, Service } from 'cordis' +import type { Agent } from '@deepseek-ai/dsh-agent' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import { defineTool } from '@deepseek-ai/dsh-tools' +import type {} from '@deepseek-ai/dsh-system-prompt' +import type {} from '@deepseek-ai/dsh-user-interaction' +// Type-only edge: resolves `ctx.commands` for the optional command child. +import type {} from '@deepseek-ai/dsh-commands' + +declare module '@deepseek-ai/dsh-session' { + interface SessionEventMap { + /** + * Whether plan mode is in force from this point on: log-only, non-surface, + * whole-value replace. The last `plan/mode` wins; a log with none folds to + * inactive through {@link foldPlanMode}. + */ + 'plan/mode': { active: boolean } + } +} + +declare module 'cordis' { + interface Context { + planMode: PlanModeService + } +} + +/** + * The model-facing exit tool's name. It stays registered while plan mode is + * inactive so the request tool catalog is stable across transitions. + */ +export const EXIT_PLAN_MODE = 'exit_plan_mode' + +/** Deployment-owned plan guidance. */ +export interface PlanModeConfig { + /** Guidance rendered as the `plan:policy` prompt section while plan mode is active. */ + section: string +} + +/** The review question's approve option label. */ +const APPROVE_LABEL = 'Approve' + +/** The review question's keep-planning option label. */ +const KEEP_PLANNING_LABEL = 'Keep planning' + +const EXIT_DESCRIPTION + = 'Use only in plan mode. Present your plan for the user\'s review and, on approval, leave plan mode. ' + + 'Send the COMPLETE plan as markdown, starting with a # heading that names it. ' + + 'The user may approve (carry out the plan from your next step) or keep ' + + 'planning — their feedback comes back in the tool result; revise and present again.' + +/** The plan's first markdown heading (any level), or `undefined` when it has none. */ +function firstHeading(plan: string): string | undefined { + for (const line of plan.split('\n')) { + const match = /^#{1,6}\s+(.+?)\s*$/.exec(line) + if (match) return match[1] + } + return undefined +} + +/** + * Validate deployment-owned plan guidance. Missing, blank, non-string, or + * unknown fields fail at plugin load rather than silently shaping nothing. + * + * @param config Raw plugin config. + * @returns A detached validated config. + */ +export function resolveConfig(config: PlanModeConfig): PlanModeConfig { + const section = (config as Partial).section + if (typeof section !== 'string') { + throw new Error('PlanModeConfig needs a string `section`') + } + if (section.trim() === '') { + throw new Error('PlanModeConfig needs a non-empty `section`') + } + const unknown = Object.keys(config).filter(key => key !== 'section') + if (unknown.length > 0) { + throw new Error(`PlanModeConfig has unknown key(s) ${unknown.join(', ')} — config is { section }`) + } + return { section } +} + +/** + * Whether plan mode is active after the first `end` events. The last + * `plan/mode` wins; a prefix with none is inactive. + * + * @param events The session log or any prefix of it. + * @param end Fold `events[0, end)`; defaults to the whole log. + * @returns Whether plan mode is active. + */ +export function foldPlanMode(events: readonly SessionEvent[], end = events.length): boolean { + let active = false + let index = 0 + for (const event of events) { + if (index >= end) break + index++ + if (event.type === 'plan/mode') active = event.data.active + } + return active +} + +/** Plan state at the last logged request header, or `undefined` before the first header. */ +function planModeAtLastHeader(events: readonly SessionEvent[]): boolean | undefined { + let lastHeader = -1 + let index = 0 + for (const event of events) { + if (event.type === 'request/header') lastHeader = index + index++ + } + if (lastHeader < 0) return undefined + return foldPlanMode(events, lastHeader + 1) +} + +/** + * `ctx.planMode`: owns logged plan state, boundary application and narration, + * the `plan:policy` section, the `/plan` command, and the stable exit tool. + * UIs observe committed flips through `session/event`; there is no live mirror. + */ +export class PlanModeService extends Service { + static inject = ['tools', 'systemPrompt'] + + /** Validated deployment-owned guidance. */ + private readonly section: string + + /** + * Latest selection per session awaiting a turn-boundary flush. `narrate` is + * true for user selections and false for the exit tool, whose result already + * narrates the transition. + */ + private readonly pendingIntents = new WeakMap() + + constructor(ctx: Context, config: PlanModeConfig = { section: '' }) { + super(ctx, 'planMode') + this.section = resolveConfig(config).section + let disposed = false + + // Boundary flushes use loop interception seams, not post-commit + // `session/event` observation. Flush after next(): a selection arriving + // while a downstream async listener awaits must still shape the request + // this boundary precedes. Failures are contained so policy cannot block a + // prompt or turn; a failed append remains pending for a later boundary. + const flushAfter = async (agent: Agent, next: () => Promise): Promise => { + const decision = await next() + if (!disposed) { + try { + this.onBoundary(agent) + } catch (error) { + ctx.logger.warn('dsh-plan-mode: boundary flush failed: %o', error) + } + } + return decision + } + ctx.on('agent/prompt-submit', (agent, _content, _source, _signal, next) => + flushAfter(agent, next), { prepend: true }) + ctx.on('agent/turn-continuation', (agent, _turn, _decision, _signal, next) => + flushAfter(agent, next), { prepend: true }) + ctx.on('agent/request-error', async ( + agent, + _turn, + _step, + _error, + _failure, + _priorFailures, + _signal, + next, + ) => { + const decision = await next() + // A waterfall can retain this wrapper after Cordis unregisters it. + if (disposed || decision.action !== 'retry') return decision + try { + this.onBoundary(agent) + } catch (error) { + ctx.logger.warn('dsh-plan-mode: boundary flush failed: %o', error) + } + return decision + }, { prepend: true }) + ctx.effect(() => () => { disposed = true }, 'dsh-plan-mode: close boundary lifetime') + + ctx.systemPrompt.section({ + name: 'plan:policy', + order: 50, + text: context => context.agent !== undefined && foldPlanMode(context.agent.session.events) + ? this.section + : '', + }) + + // The command child activates only when a command registry is composed. + ctx.inject(['commands'], (commandCtx) => { + commandCtx.commands.register({ + name: 'plan', + description: 'Enter plan mode', + input: { hint: '[message]' }, + handler: ({ agent, rawInput }) => { + const message = rawInput.trim() + this.set(agent, true) + if (message !== '') agent.steer([{ type: 'text', text: message }]) + return { kind: 'success', text: 'Entering plan mode (applies from the next step).' } + }, + }) + }) + + ctx.tools.register(defineTool({ + name: EXIT_PLAN_MODE, + description: EXIT_DESCRIPTION, + parameters: { + plan: { type: 'string', required: true, description: 'The complete plan, as markdown, starting with a # heading that names it.' }, + }, + execute: async (args, exec) => { + const agent = exec.agent + if (agent === undefined) throw new Error(`${EXIT_PLAN_MODE} requires a calling agent (no session to switch)`) + if (!foldPlanMode(agent.session.events)) { + throw new Error(`${EXIT_PLAN_MODE} is only available in plan mode`) + } + if (!/^#\s+\S/.test(args.plan.trim())) { + throw new Error(`${EXIT_PLAN_MODE} requires a non-empty markdown plan starting with a # heading`) + } + const interaction = ctx.get('userInteraction') + if (interaction === undefined) { + throw new Error('no user-interaction channel is available to review the plan; ask the user to switch the session mode instead') + } + const answer = await interaction.ask({ + questions: [{ + id: 'plan-review', + header: 'Plan review', + question: 'Approve this plan and leave plan mode?', + detail: args.plan, + options: [ + { label: APPROVE_LABEL, description: 'Leave plan mode; the plan is carried out from the next step.' }, + { label: KEEP_PLANNING_LABEL, description: 'Stay in plan mode; feedback goes back to the model.' }, + ], + }], + agent, + signal: exec.signal, + }) + // A review may outlive this plugin fiber. Without boundary listeners, + // an approved result could never land, so fail and keep planning. + if (disposed) { + throw new Error('the plan-mode service was reloaded while the plan was under review; present the plan again') + } + const reviewItems = answer.answers.filter(entry => entry.id === 'plan-review') + const item = reviewItems.length === 1 ? reviewItems[0] : undefined + if (item?.selected.length !== 1 || item.selected[0] !== APPROVE_LABEL || item.custom !== undefined) { + const feedback = item?.custom ?? '' + throw new Error(feedback === '' + ? 'The user chose to keep planning; revise the plan and present it again.' + : `The user chose to keep planning; their feedback: ${feedback}`) + } + // Keep plan guidance for the rest of this assistant tool batch. The + // silent intent flushes after the step, before the next assembly. + this.pendingIntents.set(agent.session, { active: false, narrate: false }) + return [{ type: 'text', text: 'Plan approved — plan mode exited; carry out the plan starting with your next step.' }] + }, + presentCall: args => ({ + card: 'generic', + title: firstHeading(args.plan) ?? 'Plan', + kind: 'other', + content: [{ type: 'text', text: args.plan }], + }), + presentResult: (_args, result) => ({ + card: 'generic', + title: 'Plan review', + content: result.content, + }), + })) + } + + /** + * Read the logged plan state and any selected state awaiting a boundary. + * + * @param agent The agent to read. + * @returns Current logged state plus a pending selection, when present. + */ + get(agent: Agent): { active: boolean; pending?: boolean } { + const active = foldPlanMode(agent.session.events) + const pending = this.pendingIntents.get(agent.session) + return pending === undefined ? { active } : { active, pending: pending.active } + } + + /** + * Select whether plan mode should be active from the next turn boundary. + * Repeated selection of the current or already-pending state is a no-op. + * + * @param agent The agent to switch. + * @param active Whether plan mode should be active. + */ + set(agent: Agent, active: boolean): void { + const session = agent.session + const target = this.pendingIntents.get(session)?.active ?? foldPlanMode(session.events) + if (active === target) return + this.pendingIntents.set(session, { active, narrate: true }) + } + + /** Flush one pending selection before the next request assembly. */ + private onBoundary(agent: Agent): void { + const session = agent.session + const pending = this.pendingIntents.get(session) + if (pending === undefined) return + const target = pending.active + if (target === foldPlanMode(session.events)) { + this.pendingIntents.delete(session) + return + } + session.append('plan/mode', { active: target }) + // Delete only after append succeeds so a later boundary can retry a failed + // durable write. + this.pendingIntents.delete(session) + if (!pending.narrate) return + const told = planModeAtLastHeader(session.events) + if (told === undefined || told === target) return + const text = target + ? 'The user switched this session to plan mode.' + : 'The user switched this session back to the default mode.' + session.append('context/message', { + content: [{ type: 'text', text }], + source: { kind: 'plugin', plugin: 'plan-mode' }, + }, { surfaceOp: 'append' }) + } +} + +export default PlanModeService diff --git a/packages/plan/plan-mode/src/invariant.ts b/packages/plan/plan-mode/src/invariant.ts new file mode 100644 index 0000000000..f501d2fe44 --- /dev/null +++ b/packages/plan/plan-mode/src/invariant.ts @@ -0,0 +1,43 @@ +/** Package-owned durable plan-mode invariants. @module @deepseek-ai/dsh-plan-mode/invariant */ + +import type { Context } from 'cordis' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-plan-mode' + +/** Cordis companion plugin name. */ +export const name = 'plan-mode-invariant' +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] + +/** Validate one `plan/mode` payload before it reaches the durable log. */ +function validateEvent(event: SessionEvent, fail: InvariantFailure): void { + if (event.type !== 'plan/mode') return + const active = (event.data as { active?: unknown }).active + if (typeof active !== 'boolean') { + fail(`plan/mode carries invalid active state ${JSON.stringify(active)}; expected a boolean`) + } +} + +/* jscpd:ignore-start -- package companions share replay and dispatch plumbing */ +/** Install validation for loaded and newly appended plan-mode state. */ +const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { + for (const session of ctx.sessions.list()) { + for (const event of session.events) validateEvent(event, fail) + } + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName !== 'session/event') return + const event = (args as [Session, SessionEvent])[1] + validateEvent(event, fail) + }, { global: true }) +}, { inject: ['sessions'] }) +/* jscpd:ignore-end */ + +/** + * Register the plan-mode invariant companion. + * @param ctx - Cordis context carrying the invariant service. + * @returns the installed registration's disposer after setup succeeds. + */ +export const apply = (ctx: Context): Promise<() => void> => + Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) diff --git a/packages/plan/plan-mode/tests/integration.spec.ts b/packages/plan/plan-mode/tests/integration.spec.ts new file mode 100644 index 0000000000..f1c57b5938 --- /dev/null +++ b/packages/plan/plan-mode/tests/integration.spec.ts @@ -0,0 +1,170 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import LlmService, { type StreamChunk } from '@deepseek-ai/dsh-llm' +import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' +import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import PlanModeService, { foldPlanMode } from '@deepseek-ai/dsh-plan-mode' +import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' + +const PLAN_CONFIG = { section: 'Test plan mode instructions.' } + +/** + * Full-loop integration: a scripted mock model drives the REAL plan-mode plugin + * through the agent loop — the pending-intent flush at the turn boundary, the + * assembly the soft layer shapes (the exit tool + mode section), and the + * `request/header` snapshots every transition leaves. + * Only the model is mocked; the loop, the session log, and the plugin are + * real. + */ +async function harness(adapter: MockAdapter): Promise { + const ctx = new Context() + await ctx.plugin(LlmService) + await ctx.plugin(SessionStore) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentLoop, { agents: [] }) + await ctx.plugin(PlanModeService, PLAN_CONFIG) + ctx.llm.registerAdapter(['mock'], adapter) + for (const name of ['read', 'write']) { + ctx.tools.register(defineTool({ + name, + description: `test tool ${name}`, + parameters: {}, + execute: () => Promise.resolve([{ type: 'text', text: `ran ${name}` }]), + })) + } + return ctx +} + +function waitForIdle(ctx: Context, agent: Agent): Promise { + return new Promise((resolve) => { + const dispose = ctx.on('agent/status', (subject, status) => { + if (subject === agent && status === 'idle') { + dispose() + resolve() + } + }) + }) +} + +function findEvent( + log: readonly SessionEvent[], + type: T, + position: 'first' | 'last' = 'first', +): Extract { + const found = position === 'first' + ? log.find(event => event.type === type) + : log.findLast(event => event.type === type) + if (!found) throw new Error(`no ${type} event in the session log`) + return found as Extract +} + +describe('plan mode through the agent loop', () => { + it('a pre-turn set() makes the FIRST header plan-shaped, and a non-shell call is guidance-constrained only', async () => { + const adapter = new MockAdapter([ + toolCallResponse('call-1', 'write', {}, 'Writing during plan.'), + textResponse('Noted in the plan.'), + ]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('it-plan-seed'), { provider: 'mock', model: 'mock' }) + // Selected while idle (the ACP picker shape): the pending intent flushes at + // the first prompt-submit, BEFORE the first assembly. + ctx.planMode.set(agent, true) + + agent.send([{ type: 'text', text: 'explore the repo' }]) + await waitForIdle(ctx, agent) + + const log = agent.session.events + const planMode = findEvent(log, 'plan/mode') + const header = findEvent(log, 'request/header') + expect(planMode.seq).toBeLessThan(header.seq) + expect(header.data.reason).toBe('initial') + expect(header.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write']) + expect(header.data.header.system).toContain('plan mode') + + // No tool gate: the write RUNS — plan restrains by the section's + // guidance alone (enforcement lives on the independent sandbox/approval + // axes). The mode itself stays plan throughout. + const result = findEvent(log, 'tool/result') + expect(result.data.isError).toBe(false) + expect(foldPlanMode(log)).toBe(true) + expect(log.some(event => event.type === 'context/message')).toBe(false) + }) + + it('a user flip between turns lands at the boundary: one notice and a changed header with stable tool schemas', async () => { + const adapter = new MockAdapter([ + textResponse('First turn, default mode.'), + textResponse('Second turn, plan mode.'), + ]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('it-plan-flip'), { provider: 'mock', model: 'mock' }) + + agent.send([{ type: 'text', text: 'hello' }]) + 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.send([{ type: 'text', text: 'now plan' }]) + await waitForIdle(ctx, agent) + + const log = agent.session.events + expect(foldPlanMode(log)).toBe(true) + const notices = log.filter(event => event.type === 'context/message') + expect(notices).toHaveLength(1) + expect(findEvent(log, 'context/message').data.content).toEqual([ + { type: 'text', text: 'The user switched this session to plan mode.' }, + ]) + // The changed request is logged as a complete snapshot. + const second = findEvent(log, 'request/header', 'last') + expect(second.data.reason).toBe('change') + expect(second.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write']) + expect(second.data.header.tools).toEqual(first.data.header.tools) + expect(second.data.header.system).toContain('plan mode') + }) + + it('a mode flip during request recovery shapes the retry before its assembly', async () => { + const failedRequest = [{ + type: 'finish', + reason: { kind: 'error', failure: { message: 'temporarily unavailable', code: 'SERVER', status: 503 } }, + }] satisfies StreamChunk[] + const adapter = new MockAdapter([failedRequest, textResponse('Recovered in plan mode.')]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('it-plan-retry-flip'), { provider: 'mock', model: 'mock' }) + const recoveryEntered = Promise.withResolvers() + const releaseRecovery = Promise.withResolvers() + ctx.on('agent/request-error', async (subject, _turn, _step, _error, _failure, _history, _signal, next) => { + if (subject !== agent) return next() + recoveryEntered.resolve(true) + await releaseRecovery.promise + return { action: 'retry' } + }) + + const idle = waitForIdle(ctx, agent) + agent.send([{ type: 'text', text: 'plan after the transient failure' }]) + await recoveryEntered.promise + ctx.planMode.set(agent, true) + releaseRecovery.resolve(true) + await idle + + expect(adapter.requests).toHaveLength(2) + expect(adapter.requests[0]?.system).not.toContain(PLAN_CONFIG.section) + expect(adapter.requests[1]?.system).toContain(PLAN_CONFIG.section) + expect(adapter.requests[1]?.tools).toEqual(adapter.requests[0]?.tools) + const log = agent.session.events + const planMode = findEvent(log, 'plan/mode') + const firstEnd = log.find(event => event.type === 'step/end' && event.data.step === 1) + const retryStart = log.find(event => event.type === 'step/start' && event.data.step === 2) + expect(firstEnd?.seq).toBeLessThan(planMode.seq) + expect(planMode.seq).toBeLessThan(retryStart?.seq ?? 0) + expect(findEvent(log, 'request/header', 'last').data.header.system).toContain(PLAN_CONFIG.section) + expect(findEvent(log, 'context/message').data.content).toEqual([ + { type: 'text', text: 'The user switched this session to plan mode.' }, + ]) + }) +}) diff --git a/packages/plan/plan-mode/tests/invariant.spec.ts b/packages/plan/plan-mode/tests/invariant.spec.ts new file mode 100644 index 0000000000..e5bb13687e --- /dev/null +++ b/packages/plan/plan-mode/tests/invariant.spec.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import SessionStore, { type Session, type SessionEvent } from '@deepseek-ai/dsh-session' +import * as PlanModeInvariant from '@deepseek-ai/dsh-plan-mode/invariant' +import InvariantService from '@deepseek-ai/dsh-invariants' + +async function setup(): Promise { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(InvariantService, { enabled: true }) + await ctx.plugin(PlanModeInvariant) + return ctx +} + +function event(active: unknown): SessionEvent { + return { type: 'plan/mode', seq: 0, time: 0, data: { active } } as SessionEvent +} + +describe('plan-mode stream invariants', () => { + it('accepts either boolean state', async () => { + const ctx = await setup() + expect(() => { ctx.emit('session/event', {} as Session, event(true)) }).not.toThrow() + expect(() => { ctx.emit('session/event', {} as Session, event(false)) }).not.toThrow() + }) + + it.each([42, 'plan', undefined])('rejects invalid durable plan state %j', async (active) => { + const ctx = await setup() + expect(() => { ctx.emit('session/event', {} as Session, event(active)) }) + .toThrow(/expected a boolean/) + }) + + it('ignores unrelated dispatches and session events', async () => { + const ctx = await setup() + expect(() => { + ctx.emit('tools/change') + ctx.emit('session/event', {} as Session, { + type: 'turn/start', seq: 0, time: 0, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }, + }) + }).not.toThrow() + }) + + it('rejects invalid existing state on late registration', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + ctx.sessions.create().append('plan/mode', { active: 'plan' as unknown as boolean }) + await ctx.plugin(InvariantService, { enabled: true }) + + await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).rejects.toThrow(/expected a boolean/) + }) +}) diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts new file mode 100644 index 0000000000..b5dc9723de --- /dev/null +++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts @@ -0,0 +1,934 @@ +import { describe, expect, it, vi } from 'vitest' +import { Context } from 'cordis' +import { CallId } from '@deepseek-ai/dsh-llm' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import ToolRegistry, { RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools' +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import { agentEvents, type Agent, type RequestErrorDecision } from '@deepseek-ai/dsh-agent' +import { createScope } from '@deepseek-ai/dsh-scope' +import UserInteractionService, { type AskUserQuestionRequest } from '@deepseek-ai/dsh-user-interaction' +import CommandService from '@deepseek-ai/dsh-commands' +import { CodeRuntime, type CodeRunRequest, type CodeRunResult } from '@deepseek-ai/dsh-code-runtime' +import PlanModeService, { EXIT_PLAN_MODE, foldPlanMode, resolveConfig } from '../src/index.ts' +import type { PlanModeConfig } from '../src/index.ts' + +const TEST_PLAN_SECTION = 'Test plan mode instructions.' +const PLAN_CONFIG = { section: TEST_PLAN_SECTION } satisfies PlanModeConfig + +/** + * Drives the REAL plugin: mounts `dsh-plan-mode` beside real `SystemPrompt` and + * `ToolRegistry` services, with fake Agents carrying real `Session`s and a + * real scoped `agent.ctx` minted through `createScope`. + * Turn boundaries are simulated by appending the real boundary events and + * dispatching the interception seams the loop fires there. Recovery retries + * exercise the separate `agent/request-error` wrapper. + */ + +async function agentWithSession(ctx: Context, id = 'agent-1', { active }: { active?: boolean } = {}): Promise { + const session = new Session(SessionId(id)) + const agent = { id: SessionId(id), session, options: {} } as unknown as Agent & { session: Session } + let scoped!: Context + await ctx.plugin(Object.assign((inner: Context) => { scoped = createScope(inner, agent).ctx }, { + inject: ['tools'], + })) + ;(agent as { ctx?: Context }).ctx = scoped + // Seeded plan state lands before the creation announcement, matching resume. + if (active !== undefined) session.append('plan/mode', { active }) + // The loop announces creation after publication. + ctx.emit('agent/created', agent) + return agent +} + +/** Assemble exactly as the loop does: the agent is both subject and scope. */ +function assembleFor(ctx: Context, agent: Agent) { + return ctx.systemPrompt.assemble({ agent, scope: agent }) +} + +async function setup(config: PlanModeConfig = PLAN_CONFIG): Promise { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(PlanModeService, config) + return ctx +} + +/** + * Append a boundary event and dispatch the interception seam the loop fires + * there — `agent/prompt-submit` inside the just-opened turn, + * `agent/turn-continuation` after the step closed. Recovery retries use the + * separately covered `agent/request-error` wrapper; post-commit + * `session/event` observers remain observe-only. + */ +async function boundary(ctx: Context, agent: Agent & { session: Session }, type: 'turn/start' | 'step/end'): Promise { + const events = agentEvents(ctx, agent) + if (type === 'turn/start') { + agent.session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + await events.waterfall('agent/prompt-submit', [{ type: 'text', text: 'boundary probe' }], { kind: 'user' }, new AbortController().signal, () => Promise.resolve({ kind: 'allow' })) + return + } + agent.session.append('step/end', { turn: 1, step: 1 }) + await events.waterfall('agent/turn-continuation', 1, { action: 'stop' }, new AbortController().signal, () => Promise.resolve({ action: 'stop' })) +} + +/** Dispatch the closed-step recovery seam with one terminal decision. */ +function recoveryBoundary( + ctx: Context, + agent: Agent & { session: Session }, + decision: RequestErrorDecision, +): Promise { + return agentEvents(ctx, agent).waterfall( + 'agent/request-error', + 1, + 1, + new Error('request failed'), + { message: 'request failed', code: 'SERVER' }, + [], + new AbortController().signal, + () => Promise.resolve(decision), + ) +} + +/** Append a minimal `request/header` snapshot so the log has a "what the model was told" anchor. */ +function header(session: Session): void { + session.append('request/header', { header: { config: { provider: 'test', model: 'test-model' } }, reason: 'initial' }) +} + +function noticeTexts(session: Session): string[] { + return session.events + .filter(event => event.type === 'context/message') + .map(event => (event.data as { content: { type: string; text?: string }[] }).content.map(block => block.text ?? '').join('')) +} + +function registerNamedTools(ctx: Context, names: string[]): void { + for (const name of names) { + ctx.tools.register(defineTool({ + name, + description: `test tool ${name}`, + parameters: {}, + execute: () => Promise.resolve([{ type: 'text', text: `ran ${name}` }]), + })) + } +} + +let callCounter = 0 +function execute(ctx: Context, name: string, agent?: Agent) { + return ctx.tools.execute({ + callId: CallId(`call-${++callCounter}`), + name, + arguments: {}, + signal: new AbortController().signal, + ...agent ? { agent } : {}, + }) +} + +describe('resolveConfig', () => { + it('requires string, non-empty plan instructions', () => { + expect(() => resolveConfig({} as PlanModeConfig)) + .toThrow('needs a string `section`') + expect(() => resolveConfig({ section: 5 } as unknown as PlanModeConfig)) + .toThrow('needs a string `section`') + expect(() => resolveConfig({ section: ' ' })) + .toThrow('needs a non-empty `section`') + }) + + it('returns a detached plan config', () => { + const config = { section: TEST_PLAN_SECTION } + const resolved = resolveConfig(config) + expect(resolved).toEqual(config) + expect(resolved).not.toBe(config) + }) + + it('rejects fields outside the plan policy config', () => { + expect(() => resolveConfig({ section: TEST_PLAN_SECTION, tools: ['read'] } as unknown as PlanModeConfig)) + .toThrow('unknown key(s) tools — config is { section }') + }) +}) + +describe('foldPlanMode', () => { + it('folds an empty log to inactive and takes the last plan/mode otherwise', () => { + const session = new Session(SessionId('fold')) + expect(foldPlanMode(session.events)).toBe(false) + session.append('plan/mode', { active: true }) + session.append('plan/mode', { active: false }) + session.append('plan/mode', { active: true }) + expect(foldPlanMode(session.events)).toBe(true) + }) + + it('folds a prefix when `end` is given', () => { + const session = new Session(SessionId('fold-prefix')) + session.append('plan/mode', { active: true }) + session.append('plan/mode', { active: false }) + expect(foldPlanMode(session.events, 1)).toBe(true) + expect(foldPlanMode(session.events, 0)).toBe(false) + }) +}) + +describe('ctx.planMode: get/set', () => { + it('reads the folded state', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + expect(ctx.planMode.get(agent)).toEqual({ active: false }) + agent.session.append('plan/mode', { active: true }) + expect(ctx.planMode.get(agent)).toEqual({ active: true }) + }) + + it('selects inactive as the plan exit target', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + agent.session.append('plan/mode', { active: true }) + ctx.planMode.set(agent, false) + expect(ctx.planMode.get(agent)).toEqual({ active: true, pending: false }) + }) + + it('drops a no-op set (target equals pending, else the current fold)', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + ctx.planMode.set(agent, false) + expect(ctx.planMode.get(agent)).toEqual({ active: false }) + ctx.planMode.set(agent, true) + ctx.planMode.set(agent, true) + expect(ctx.planMode.get(agent)).toEqual({ active: false, pending: true }) + }) +}) + +describe('the boundary flush', () => { + it('flushes the pending intent as a plan/mode at turn/start', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + ctx.planMode.set(agent, true) + await boundary(ctx, agent, 'turn/start') + expect(foldPlanMode(agent.session.events)).toBe(true) + expect(ctx.planMode.get(agent)).toEqual({ active: true }) + }) + + it('flushes a set() that arrives while a downstream listener is still awaiting (post-next ordering)', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + // A downstream async listener (the shipped hooks listeners' shape): the + // selection lands DURING its await — after this boundary began, before it + // returns. The prepended flush runs after next(), so the plan/mode still + // precedes the request this boundary gates. + ctx.on('agent/turn-continuation', async (_agent, _turn, decision, _signal, next) => { + await new Promise(resolve => setTimeout(resolve, 5)) + ctx.planMode.set(agent, true) + await next() + return decision + }) + agent.session.append('step/end', { turn: 1, step: 1 }) + await agentEvents(ctx, agent).waterfall( + 'agent/turn-continuation', 1, { action: 'stop' }, new AbortController().signal, + () => Promise.resolve({ action: 'stop' }), + ) + expect(foldPlanMode(agent.session.events)).toBe(true) + expect(ctx.planMode.get(agent)).toEqual({ active: true }) + }) + + it('skips the flush after the plugin fiber is disposed (a captured wrapper must not write into a dead service)', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG) + const agent = await agentWithSession(ctx) + ctx.planMode.set(agent, true) + // A downstream listener captured before disposal keeps the waterfall + // continuation alive across the unload; the resumed wrapper must not + // append through the disposed service. + ctx.on('agent/turn-continuation', async (_agent, _turn, decision, _signal, next) => { + await fiber.dispose() + await next() + return decision + }) + agent.session.append('step/end', { turn: 1, step: 1 }) + await agentEvents(ctx, agent).waterfall( + 'agent/turn-continuation', 1, { action: 'stop' }, new AbortController().signal, + () => Promise.resolve({ action: 'stop' }), + ) + expect(agent.session.events.some(event => event.type === 'plan/mode')).toBe(false) + }) + + it('flushes at step/end too (a mid-turn flip lands on the following step)', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + ctx.planMode.set(agent, true) + await boundary(ctx, agent, 'step/end') + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('keeps the pending intent parked when recovery does not retry', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + ctx.planMode.set(agent, true) + expect(await recoveryBoundary(ctx, agent, { action: 'fail' })).toEqual({ action: 'fail' }) + expect(ctx.planMode.get(agent)).toEqual({ active: false, pending: true }) + }) + + it('contains an append failure at the retry boundary without changing its decision', async () => { + const ctx = await setup() + const warn = vi.fn() + ctx.logger.warn = warn as never + const agent = await agentWithSession(ctx) + ctx.planMode.set(agent, true) + const original = agent.session.append.bind(agent.session) + agent.session.append = (((type: string, ...rest: unknown[]) => { + if (type === 'plan/mode') throw new Error('backend gone') + return (original as (...args: unknown[]) => unknown)(type, ...rest) + }) as unknown) as typeof agent.session.append + + expect(await recoveryBoundary(ctx, agent, { action: 'retry' })).toEqual({ action: 'retry' }) + expect(warn).toHaveBeenCalledOnce() + expect(ctx.planMode.get(agent)).toEqual({ active: false, pending: true }) + }) + + it('nets out a flip sequence that returns to the folded mode (no append, no notice)', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + ctx.planMode.set(agent, true) + ctx.planMode.set(agent, false) + await boundary(ctx, agent, 'turn/start') + expect(agent.session.events.some(event => event.type === 'plan/mode')).toBe(false) + expect(noticeTexts(agent.session)).toEqual([]) + }) + + it('narrates nothing before the first request header (the section is the state statement)', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + ctx.planMode.set(agent, true) + await boundary(ctx, agent, 'turn/start') + expect(noticeTexts(agent.session)).toEqual([]) + }) + + it('narrates once when the flushed mode differs from what the last header told the model', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + header(agent.session) + ctx.planMode.set(agent, true) + await boundary(ctx, agent, 'turn/start') + expect(noticeTexts(agent.session)).toEqual(['The user switched this session to plan mode.']) + await boundary(ctx, agent, 'step/end') + expect(noticeTexts(agent.session)).toEqual(['The user switched this session to plan mode.']) + }) + + it('narrates a switch back to the default mode with the default wording', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + agent.session.append('plan/mode', { active: true }) + header(agent.session) + ctx.planMode.set(agent, false) + await boundary(ctx, agent, 'step/end') + expect(noticeTexts(agent.session)).toEqual(['The user switched this session back to the default mode.']) + }) + + it('stays silent when the header already reflects the flushed mode', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + agent.session.append('plan/mode', { active: true }) + header(agent.session) + agent.session.append('plan/mode', { active: false }) + ctx.planMode.set(agent, true) + await boundary(ctx, agent, 'step/end') + expect(foldPlanMode(agent.session.events)).toBe(true) + expect(noticeTexts(agent.session)).toEqual([]) + }) + + + it('contains an append failure instead of blocking the prompt or the turn', async () => { + const ctx = await setup() + const warn = vi.fn() + ctx.logger.warn = warn as never + const agent = await agentWithSession(ctx) + ctx.planMode.set(agent, true) + const original = agent.session.append.bind(agent.session) + // Only the flush's own plan/mode append fails; the boundary event itself + // lands (the loop appended it before the seam fires). + agent.session.append = (((type: string, ...rest: unknown[]) => { + if (type === 'plan/mode') throw new Error('backend gone') + return (original as (...args: unknown[]) => unknown)(type, ...rest) + }) as unknown) as typeof agent.session.append + await boundary(ctx, agent, 'step/end') + expect(warn).toHaveBeenCalledOnce() + // The failed flush re-parks the intent (cleared only after a landed + // append), so the next healthy boundary converges the log with the + // picker's optimistic state instead of dropping the switch forever. + expect(ctx.planMode.get(agent)).toEqual({ active: false, pending: true }) + agent.session.append = original + await boundary(ctx, agent, 'step/end') + expect(foldPlanMode(agent.session.events)).toBe(true) + expect(ctx.planMode.get(agent).pending).toBeUndefined() + }) + + it('contains an append failure on the prompt-submit seam the same way', async () => { + const ctx = await setup() + const warn = vi.fn() + ctx.logger.warn = warn as never + const agent = await agentWithSession(ctx) + ctx.planMode.set(agent, true) + const original = agent.session.append.bind(agent.session) + agent.session.append = (((type: string, ...rest: unknown[]) => { + if (type === 'plan/mode') throw new Error('backend gone') + return (original as (...args: unknown[]) => unknown)(type, ...rest) + }) as unknown) as typeof agent.session.append + await boundary(ctx, agent, 'turn/start') + expect(warn).toHaveBeenCalledOnce() + expect(ctx.planMode.get(agent)).toEqual({ active: false, pending: true }) + }) +}) + +describe('the soft layer', () => { + it('keeps the tool schemas identical across default and plan mode', async () => { + const ctx = await setup() + registerNamedTools(ctx, ['read', 'write']) + const agent = await agentWithSession(ctx) + const defaultAssembly = await assembleFor(ctx, agent) + expect(defaultAssembly.tools.map(tool => tool.name)).toEqual([EXIT_PLAN_MODE, 'read', 'write']) + expect(defaultAssembly.sections.find(section => section.name === 'plan:policy')?.text).toBe('') + + agent.session.append('plan/mode', { active: true }) + const planAssembly = await assembleFor(ctx, agent) + expect(planAssembly.tools).toEqual(defaultAssembly.tools) + expect(planAssembly.sections.find(section => section.name === 'plan:policy')?.text).toBe(TEST_PLAN_SECTION) + }) + + it('leaves an agent-less assembly untouched', async () => { + const ctx = await setup() + registerNamedTools(ctx, ['read']) + const assembly = await ctx.systemPrompt.assemble() + expect(assembly.tools.map(tool => tool.name)).toEqual([EXIT_PLAN_MODE, 'read']) + expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe('') + }) + + it('keeps the full toolset in plan mode and renders the configured mode section', async () => { + const ctx = await setup() + registerNamedTools(ctx, ['read', 'write', 'todo_write']) + const agent = await agentWithSession(ctx, 'agent-1', { active: true }) + const assembly = await assembleFor(ctx, agent) + expect(assembly.tools.map(tool => tool.name).sort()).toEqual([EXIT_PLAN_MODE, 'read', 'todo_write', 'write']) + expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe(TEST_PLAN_SECTION) + }) + + it('leaves foreign assemble additions alone (no assemble-layer filtering)', async () => { + // Plan guidance does not filter the registry or later assembly additions. + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + ctx.on('system-prompt/assemble', async (_assembly, _context, next) => { + const final = await next() + final.tools = [...final.tools, { name: 'added-later', description: 'added after next()', parameters: {} }] + return final + }) + await ctx.plugin(PlanModeService, PLAN_CONFIG) + registerNamedTools(ctx, ['read']) + const planning = await agentWithSession(ctx, 'planning', { active: true }) + expect((await assembleFor(ctx, planning)).tools.map(tool => tool.name)) + .toEqual(['exit_plan_mode', 'read', 'added-later']) + const defaulted = await agentWithSession(ctx, 'defaulted') + expect((await assembleFor(ctx, defaulted)).tools.map(tool => tool.name)) + .toEqual(['exit_plan_mode', 'read', 'added-later']) + }) + + it('keeps run_code the only wire tool in plan mode under the registry Code Mode; the SDK gains the exit binding', async () => { + // Minimal scriptable runtime: the SDK section resolves ctx.codeRuntime at + // assembly time (the code-mode.spec fake's shape). + class FakeRuntime extends CodeRuntime { + readonly language = 'typescript' + readonly isolation = 'fake' + run(_request: CodeRunRequest): Promise { return Promise.resolve({ logs: [] }) } + } + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry, { mode: 'code' }) + await ctx.plugin(FakeRuntime) + await ctx.plugin(PlanModeService, PLAN_CONFIG) + registerNamedTools(ctx, ['read', 'write']) + const agent = await agentWithSession(ctx, 'agent-1', { active: true }) + const assembly = await assembleFor(ctx, agent) + expect(assembly.tools.map(tool => tool.name)).toEqual(['run_code']) + // The SDK documents the full binding set plus the exit; plan mode never + // prunes capabilities and restrains through guidance alone. + const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? '' + expect(sdk).toContain('read(args:') + expect(sdk).toContain('write(args:') + expect(sdk).toContain('exit_plan_mode(args:') + }) + + it('keeps native wire schemas and the SDK in step under mode both', async () => { + class FakeRuntime extends CodeRuntime { + readonly language = 'typescript' + readonly isolation = 'fake' + run(_request: CodeRunRequest): Promise { return Promise.resolve({ logs: [] }) } + } + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry, { mode: 'both' }) + await ctx.plugin(FakeRuntime) + await ctx.plugin(PlanModeService, PLAN_CONFIG) + registerNamedTools(ctx, ['read', 'write']) + const agent = await agentWithSession(ctx, 'agent-1', { active: true }) + const assembly = await assembleFor(ctx, agent) + // The stable registry contribution reaches both surfaces: the exit tool + // is present on the wire AND in the SDK alongside the untouched toolset. + expect(assembly.tools.map(tool => tool.name).sort()).toEqual(['exit_plan_mode', 'read', 'run_code', 'write']) + const sdk = assembly.sections.find(section => section.name === 'tools:sdk')?.text ?? '' + expect(sdk).toContain('read(args:') + expect(sdk).toContain('write(args:') + expect(sdk).toContain('exit_plan_mode(args:') + }) + + it('keeps the Code Mode SDK byte-identical across mode switches', async () => { + class FakeRuntime extends CodeRuntime { + readonly language = 'typescript' + readonly isolation = 'fake' + run(_request: CodeRunRequest): Promise { return Promise.resolve({ logs: [] }) } + } + const withPlanMode = new Context() + await withPlanMode.plugin(SystemPrompt) + await withPlanMode.plugin(ToolRegistry, { mode: 'code' }) + await withPlanMode.plugin(FakeRuntime) + await withPlanMode.plugin(PlanModeService, PLAN_CONFIG) + registerNamedTools(withPlanMode, ['read', 'write']) + const agent = await agentWithSession(withPlanMode) + const defaultSdk = (await assembleFor(withPlanMode, agent)).sections.find(section => section.name === 'tools:sdk')?.text ?? '' + expect(defaultSdk).toContain('read(args:') + expect(defaultSdk).toContain('write(args:') + expect(defaultSdk).toContain('exit_plan_mode(args:') + agent.session.append('plan/mode', { active: true }) + const planSdk = (await assembleFor(withPlanMode, agent)).sections.find(section => section.name === 'tools:sdk')?.text ?? '' + expect(planSdk).toBe(defaultSdk) + + // Loading the plan-mode plugin deliberately adds one stable binding compared + // with a deployment that does not compose plan mode at all. + const bare = new Context() + await bare.plugin(SystemPrompt) + await bare.plugin(ToolRegistry, { mode: 'code' }) + await bare.plugin(FakeRuntime) + registerNamedTools(bare, ['read', 'write']) + const bareSdk = (await bare.systemPrompt.assemble({ agent })).sections.find(section => section.name === 'tools:sdk')?.text ?? '' + expect(bareSdk).not.toContain('exit_plan_mode(args:') + expect(defaultSdk).not.toBe(bareSdk) + }) +}) + +describe('no execution gating beyond the exit tool', () => { + it('passes agent-less and default-mode executions through', async () => { + const ctx = await setup() + registerNamedTools(ctx, ['write']) + const agentless = await execute(ctx, 'write') + expect(agentless.isError).toBe(false) + const agent = await agentWithSession(ctx) + const defaulted = await execute(ctx, 'write', agent) + expect(defaulted.isError).toBe(false) + }) + + it('runs every call in plan mode untouched — guidance and enforcement are separate axes', async () => { + const ctx = await setup() + registerNamedTools(ctx, ['read', 'write', 'bash']) + const agent = await agentWithSession(ctx, 'agent-1', { active: true }) + for (const name of ['read', 'write', 'bash']) { + const result = await execute(ctx, name, agent) + expect(result.isError).toBe(false) + } + }) +}) + +describe('/plan', () => { + it('registers only when a commands service is composed and optionally submits the next-step message', async () => { + const bare = await setup() + expect(bare.get('commands')).toBeUndefined() + + const ctx = await setup() + await ctx.plugin(CommandService) + // The `ctx.inject` child mounts asynchronously once `commands` resolves. + await new Promise(resolve => setImmediate(resolve)) + const plainAgent = await agentWithSession(ctx, 'plain-plan-command') + const plainSteer = vi.fn() + ;(plainAgent as unknown as { steer: typeof plainSteer }).steer = plainSteer + expect(ctx.commands.list(plainAgent)).toEqual([ + { name: 'plan', description: 'Enter plan mode', input: { hint: '[message]' } }, + ]) + + const signal = new AbortController().signal + expect(await ctx.commands.execute(plainAgent, '/mode', signal)).toBeUndefined() + expect(await ctx.commands.execute(plainAgent, '/review', signal)).toBeUndefined() + const plain = await ctx.commands.execute(plainAgent, '/plan', signal) + expect(plain).toEqual({ kind: 'success', text: 'Entering plan mode (applies from the next step).' }) + expect(ctx.planMode.get(plainAgent)).toEqual({ active: false, pending: true }) + expect(plainSteer).not.toHaveBeenCalled() + + const messageAgent = await agentWithSession(ctx, 'message-plan-command') + const messageSteer = vi.fn() + ;(messageAgent as unknown as { steer: typeof messageSteer }).steer = messageSteer + const plan = await ctx.commands.execute(messageAgent, '/plan draft the migration ', signal) + expect(plan).toEqual({ kind: 'success', text: 'Entering plan mode (applies from the next step).' }) + expect(ctx.planMode.get(messageAgent)).toEqual({ active: false, pending: true }) + expect(messageSteer).toHaveBeenCalledExactlyOnceWith([{ type: 'text', text: 'draft the migration' }]) + }) + + it('removes the contributed command when the plan-mode plugin is disposed', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(CommandService) + const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG) + await new Promise(resolve => setImmediate(resolve)) + const agent = await agentWithSession(ctx) + expect(ctx.commands.list(agent).map(command => command.name)).toEqual(['plan']) + + await fiber.dispose() + + expect(ctx.commands.list(agent)).toEqual([]) + }) +}) + +describe('exit_plan_mode', () => { + async function setupWithReview(answer?: { selected: string[]; custom?: string }) { + const ctx = await setup() + await ctx.plugin(UserInteractionService) + const asked: AskUserQuestionRequest[] = [] + if (answer !== undefined) { + ctx.userInteraction.registerProvider({ + ask: (request) => { + asked.push(request) + return Promise.resolve({ answers: [{ id: 'plan-review', ...answer }] }) + }, + }) + } + const agent = await agentWithSession(ctx, 'agent-1', { active: true }) + return { ctx, agent, asked } + } + + function callExit(ctx: Context, agent: Agent | undefined, plan = '# The plan\n\ndo things') { + return ctx.tools.execute({ + callId: CallId(`call-exit-${++callCounter}`), + name: EXIT_PLAN_MODE, + arguments: { plan }, + signal: new AbortController().signal, + ...agent ? { agent } : {}, + }) + } + + it('registers the tool with one required plan argument', async () => { + const ctx = await setup() + const schema = ctx.tools.schemas().find(entry => entry.name === EXIT_PLAN_MODE) + const parameters = schema?.parameters as { required?: string[]; properties?: Record } + expect(schema?.description).toMatch(/^Use only in plan mode\./) + expect(Object.keys(parameters.properties ?? {})).toEqual(['plan']) + expect(parameters.required).toEqual(['plan']) + }) + + it('rejects an agent-less call', async () => { + const ctx = await setup() + const result = await callExit(ctx, undefined) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: exit_plan_mode requires a calling agent (no session to switch)' }]) + }) + + it('rejects a call outside plan mode while remaining advertised', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx) + expect(ctx.tools.schemas().map(tool => tool.name)).toContain(EXIT_PLAN_MODE) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: exit_plan_mode is only available in plan mode' }]) + }) + + it('rejects an empty or heading-less plan before asking the reviewer', async () => { + const { ctx, agent, asked } = await setupWithReview({ selected: ['Approve'] }) + for (const plan of ['', 'do things']) { + const result = await callExit(ctx, agent, plan) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: exit_plan_mode requires a non-empty markdown plan starting with a # heading' }]) + } + expect(asked).toHaveLength(0) + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('degrades to the manual exit when no user-interaction seam is composed', async () => { + const ctx = await setup() + const agent = await agentWithSession(ctx, 'agent-1', { active: true }) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: no user-interaction channel is available to review the plan; ask the user to switch the session mode instead' }]) + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('degrades the same way when the seam has no provider (NO_PROVIDER)', async () => { + const { ctx, agent } = await setupWithReview() + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: no user-interaction provider is registered' }]) + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('approve: records the boundary-applied switch and confirms (the fold flips at the flush)', async () => { + const { ctx, agent, asked } = await setupWithReview({ selected: ['Approve'] }) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(false) + expect(result.content).toEqual([{ type: 'text', text: 'Plan approved — plan mode exited; carry out the plan starting with your next step.' }]) + // Boundary-applied, not a direct append: the fold stays plan until the + // step's end, so the plan policy covers any remaining call of the SAME batch. + expect(foldPlanMode(agent.session.events)).toBe(true) + expect(ctx.planMode.get(agent)).toEqual({ active: true, pending: false }) + await boundary(ctx, agent, 'step/end') + expect(foldPlanMode(agent.session.events)).toBe(false) + expect(asked).toHaveLength(1) + expect(asked[0]?.agent).toBe(agent) + expect(asked[0]?.questions[0]?.detail).toBe('# The plan\n\ndo things') + expect(asked[0]?.questions[0]?.options?.map(option => option.label)).toEqual(['Approve', 'Keep planning']) + }) + + it('carries the exact plan through a Code Mode review and logs the nested dispatch', async () => { + const plan = '# Code Mode plan\n\nUse the existing seam.' + class ExitRuntime extends CodeRuntime { + readonly language = 'typescript' + readonly isolation = 'fake' + async run(request: CodeRunRequest): Promise { + const exit = request.bindings[0]?.functions[EXIT_PLAN_MODE] + if (exit === undefined) throw new Error('missing exit_plan_mode binding') + return { logs: [], value: await exit({ plan }) } + } + } + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry, { mode: 'code' }) + await ctx.plugin(ExitRuntime) + await ctx.plugin(PlanModeService, PLAN_CONFIG) + await ctx.plugin(UserInteractionService) + const asked: AskUserQuestionRequest[] = [] + ctx.userInteraction.registerProvider({ + ask: (request) => { + asked.push(request) + return Promise.resolve({ answers: [{ id: 'plan-review', selected: ['Approve'] }] }) + }, + }) + const agent = await agentWithSession(ctx, 'code-mode-exit', { active: true }) + + const result = await ctx.tools.execute({ + callId: CallId(`call-exit-${++callCounter}`), + name: RUN_CODE_NAME, + arguments: { code: `return await tools.${EXIT_PLAN_MODE}({ plan: ${JSON.stringify(plan)} })` }, + signal: new AbortController().signal, + agent, + }) + + expect(result.isError).toBe(false) + expect(asked).toHaveLength(1) + expect(asked[0]?.questions[0]).toMatchObject({ + header: 'Plan review', + question: 'Approve this plan and leave plan mode?', + detail: plan, + }) + expect(agent.session.events.find(event => event.type === 'tool/code-dispatch')?.data).toMatchObject({ + name: EXIT_PLAN_MODE, + arguments: { plan }, + isError: false, + }) + expect(ctx.planMode.get(agent)).toEqual({ active: true, pending: false }) + }) + + it('an approved exit keeps plan guidance until the boundary and never removes the tool', async () => { + const { ctx, agent } = await setupWithReview({ selected: ['Approve'] }) + const approved = await callExit(ctx, agent) + expect(approved.isError).toBe(false) + // Calls of the SAME assistant response (no boundary between) were + // requested under the plan-shaped header — the fold stays plan for that + // whole batch; the boundary flush is what flips the next step. + expect(foldPlanMode(agent.session.events)).toBe(true) + const assembly = await ctx.systemPrompt.assemble({ agent }) + expect(assembly.tools.some(tool => tool.name === EXIT_PLAN_MODE)).toBe(true) + expect(assembly.sections.find(section => section.name === 'plan:policy')?.text).toBe(TEST_PLAN_SECTION) + await boundary(ctx, agent, 'step/end') + expect(foldPlanMode(agent.session.events)).toBe(false) + const afterExit = await ctx.systemPrompt.assemble({ agent }) + expect(afterExit.tools).toEqual(assembly.tools) + expect(afterExit.sections.find(section => section.name === 'plan:policy')?.text).toBe('') + }) + + it('the exit flush narrates nothing — the tool result is the narration', async () => { + const { ctx, agent } = await setupWithReview({ selected: ['Approve'] }) + header(agent.session) + await callExit(ctx, agent) + await boundary(ctx, agent, 'step/end') + expect(foldPlanMode(agent.session.events)).toBe(false) + expect(noticeTexts(agent.session)).toEqual([]) + }) + + it('keep planning returns the corrective error carrying the feedback verbatim', async () => { + const { ctx, agent } = await setupWithReview({ selected: ['Keep planning'], custom: 'consider the resume path' }) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: The user chose to keep planning; their feedback: consider the resume path' }]) + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('keep planning without feedback returns the generic corrective error', async () => { + const { ctx, agent } = await setupWithReview({ selected: ['Keep planning'] }) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: The user chose to keep planning; revise the plan and present it again.' }]) + }) + + it('a custom-text-only answer is feedback, never consent', async () => { + const { ctx, agent } = await setupWithReview({ selected: [], custom: 'add tests first' }) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: The user chose to keep planning; their feedback: add tests first' }]) + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('requires exactly the single Approve selection', async () => { + const { ctx, agent } = await setupWithReview({ selected: ['Approve', 'Keep planning'] }) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: The user chose to keep planning; revise the plan and present it again.' }]) + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('treats custom text alongside Approve as feedback, not consent', async () => { + const { ctx, agent } = await setupWithReview({ selected: ['Approve'], custom: 'change the tests' }) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: The user chose to keep planning; their feedback: change the tests' }]) + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('treats duplicate review answer items as non-consent', async () => { + const { ctx, agent } = await setupWithReview() + ctx.userInteraction.registerProvider({ + ask: () => Promise.resolve({ answers: [ + { id: 'plan-review', selected: ['Approve'] }, + { id: 'plan-review', selected: ['Keep planning'] }, + ] }), + }) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: The user chose to keep planning; revise the plan and present it again.' }]) + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('a missing answer item reads as keep-planning', async () => { + const { ctx, agent } = await setupWithReview() + ctx.userInteraction.registerProvider({ ask: () => Promise.resolve({ answers: [] }) }) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: The user chose to keep planning; revise the plan and present it again.' }]) + }) + + it('forwards the execution abort signal to the review question', async () => { + const { ctx, agent, asked } = await setupWithReview({ selected: ['Approve'] }) + const controller = new AbortController() + const result = await ctx.tools.execute({ + callId: CallId(`call-exit-${++callCounter}`), + name: EXIT_PLAN_MODE, + arguments: { plan: '# P' }, + agent, + signal: controller.signal, + }) + expect(result.isError).toBe(false) + expect(asked[0]?.signal).toBe(controller.signal) + }) + + it('fails the call when the plugin is disposed while the review awaits (no phantom exit)', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG) + await ctx.plugin(UserInteractionService) + let answer!: (value: { answers: { id: string; selected: string[] }[] }) => void + ctx.userInteraction.registerProvider({ + ask: () => new Promise((resolve) => { answer = resolve }), + }) + const agent = await agentWithSession(ctx, 'agent-1', { active: true }) + const pending = callExit(ctx, agent) + // Let execute reach the review await, then unload the plugin (HMR) and + // only afterwards approve. The boundary listeners are gone, so a success + // would claim an exit that can never flush — the call must fail instead. + await new Promise(resolve => setImmediate(resolve)) + await fiber.dispose() + answer({ answers: [{ id: 'plan-review', selected: ['Approve'] }] }) + const result = await pending + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: the plan-mode service was reloaded while the plan was under review; present the plan again' }]) + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('a throwing provider surfaces as the corrective isError and the mode stays plan', async () => { + const { ctx, agent } = await setupWithReview() + ctx.userInteraction.registerProvider({ ask: () => { throw new Error('review aborted') } }) + const result = await callExit(ctx, agent) + expect(result.isError).toBe(true) + expect(result.content).toEqual([{ type: 'text', text: 'Error: review aborted' }]) + expect(foldPlanMode(agent.session.events)).toBe(true) + }) + + it('presents the call as a generic card titled by the plan first heading', async () => { + const ctx = await setup() + const def = ctx.tools.get(EXIT_PLAN_MODE)! + expect(def.presentCall?.({ plan: '## Fix the flake\n\nsteps' })).toEqual({ + card: 'generic', + title: 'Fix the flake', + kind: 'other', + content: [{ type: 'text', text: '## Fix the flake\n\nsteps' }], + }) + expect(def.presentCall?.({ plan: 'no heading here' })).toEqual({ + card: 'generic', + title: 'Plan', + kind: 'other', + content: [{ type: 'text', text: 'no heading here' }], + }) + }) + + it('presents the result as a generic review card', async () => { + const ctx = await setup() + const def = ctx.tools.get(EXIT_PLAN_MODE)! + const content = [{ type: 'text' as const, text: 'ok' }] + expect(def.presentResult?.({ plan: '# P' }, { content, isError: false })).toEqual({ + card: 'generic', + title: 'Plan review', + content, + }) + }) +}) + +describe('HMR disposal', () => { + it('does not flush a retry boundary that resumes after plugin disposal', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG) + const agent = await agentWithSession(ctx, 'disposed-in-flight-recovery') + const recoveryEntered = Promise.withResolvers() + const releaseRecovery = Promise.withResolvers() + ctx.on('agent/request-error', async (_agent, _turn, _step, _error, _failure, _history, _signal, _next) => { + recoveryEntered.resolve(true) + await releaseRecovery.promise + return { action: 'retry' } + }) + ctx.planMode.set(agent, true) + + const recovery = recoveryBoundary(ctx, agent, { action: 'fail' }) + await recoveryEntered.promise + await fiber.dispose() + releaseRecovery.resolve(true) + + expect(await recovery).toEqual({ action: 'retry' }) + expect(agent.session.events.some(event => event.type === 'plan/mode')).toBe(false) + }) + + it('unregisters the service, listeners, prompt section, and stable exit tool with the plugin fiber', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG) + const agent = await agentWithSession(ctx, 'disposed-recovery') + ctx.planMode.set(agent, true) + expect(ctx.get('planMode')).toBeInstanceOf(PlanModeService) + expect(ctx.tools.get(EXIT_PLAN_MODE)).toBeDefined() + expect((await ctx.systemPrompt.assemble()).sections.map(section => section.name)).toContain('plan:policy') + + await fiber.dispose() + expect(ctx.get('planMode')).toBeUndefined() + expect(ctx.tools.get(EXIT_PLAN_MODE)).toBeUndefined() + expect((await ctx.systemPrompt.assemble()).sections.map(section => section.name)).not.toContain('plan:policy') + expect(await recoveryBoundary(ctx, agent, { action: 'retry' })).toEqual({ action: 'retry' }) + expect(agent.session.events.some(event => event.type === 'plan/mode')).toBe(false) + }) +}) diff --git a/packages/plan/plan-mode/tsconfig.json b/packages/plan/plan-mode/tsconfig.json new file mode 100644 index 0000000000..6ca0874232 --- /dev/null +++ b/packages/plan/plan-mode/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../core/tools" + }, + { + "path": "../../core/agent" + }, + { + "path": "../../core/session" + }, + { + "path": "../../core/system-prompt" + }, + { + "path": "../../ui/user-interaction" + }, + { + "path": "../../ui/commands" + }, + { + "path": "../../support/invariants" + } + ] +} diff --git a/packages/session-persistence/session-persistence/src/coordinator.ts b/packages/session-persistence/session-persistence/src/coordinator.ts index beb5fca483..7dc0c9a0ba 100644 --- a/packages/session-persistence/session-persistence/src/coordinator.ts +++ b/packages/session-persistence/session-persistence/src/coordinator.ts @@ -125,6 +125,11 @@ function assertSupportedEvents(events: readonly SessionEvent[], id: SessionId): if (legacy !== undefined) { throw new Error(`session "${id}" contains unsupported legacy request/header-delta event at seq ${legacy.seq}`) } + const legacyModeType: string = 'mode/set' + const legacyMode = events.find(event => event.type === legacyModeType) + if (legacyMode !== undefined) { + throw new Error(`session "${id}" contains unsupported legacy mode/set event at seq ${legacyMode.seq}`) + } const fallback = events.find(event => event.type === 'request/header' && (event.data as { reason?: string }).reason === 'fallback') if (fallback !== undefined) { diff --git a/packages/session-persistence/session-persistence/tests/persistence.spec.ts b/packages/session-persistence/session-persistence/tests/persistence.spec.ts index e083ac3543..510ae38136 100644 --- a/packages/session-persistence/session-persistence/tests/persistence.spec.ts +++ b/packages/session-persistence/session-persistence/tests/persistence.spec.ts @@ -22,6 +22,16 @@ function legacyHeaderDelta(seq = 0): SessionEvent { } as unknown as SessionEvent } +/** An unsupported named-mode fixture emulating an untyped producer. */ +function legacyModeSet(seq = 0): SessionEvent { + return { + type: 'mode/set', + seq, + time: 1, + data: { mode: 'plan' }, + } as unknown as SessionEvent +} + /** An obsolete full-header reason fixture from the removed delta codec. */ function legacyFallbackHeader(seq = 0): SessionEvent { return { @@ -510,6 +520,19 @@ describe('SessionPersistence service registration', () => { await fiber.dispose() }) + it('rejects a stored legacy named-mode event during load', async () => { + const id = SessionId('legacy-mode-load') + const m = meta(id, '/legacy') + const store: MemoryStore = new Map([[id, { meta: m, events: [legacyModeSet()] }]]) + const ctx = new Context() + await ctx.plugin(SessionStore) + const fiber = await ctx.plugin(MemoryPersistence, { store }) + + await expect(ctx.sessionPersistence.load(id)) + .rejects.toThrow('unsupported legacy mode/set event at seq 0') + await fiber.dispose() + }) + it('retires all coordinator bookkeeping for disposed sessions', async () => { const ctx = new Context() await ctx.plugin(SessionStore) diff --git a/packages/skill/skill-local/tests/skill-local.spec.ts b/packages/skill/skill-local/tests/skill-local.spec.ts index 43cc70c7ad..0c7d473b13 100644 --- a/packages/skill/skill-local/tests/skill-local.spec.ts +++ b/packages/skill/skill-local/tests/skill-local.spec.ts @@ -391,6 +391,9 @@ describe('LocalSkillProvider', () => { await empty.plugin(SkillService) SkillLocal.apply(empty, {}) expect(await empty.skills.list()).toEqual([]) + + delete process.env.DSH_AGENTS_HOME + expect(new SkillLocal.LocalSkillProvider(empty, { dshHome: join(envHome, 'empty-dsh') }).name).toBe('local') } finally { if (previousDshHome === undefined) { delete process.env.DSH_HOME diff --git a/packages/support/acp-snapshot/src/harness.ts b/packages/support/acp-snapshot/src/harness.ts index bef803d69e..c29886fdbb 100644 --- a/packages/support/acp-snapshot/src/harness.ts +++ b/packages/support/acp-snapshot/src/harness.ts @@ -24,6 +24,8 @@ import { basename, dirname, join, delimiter } from 'node:path' import { ClientSideConnection, PROTOCOL_VERSION, + type CreateElicitationRequest, + type CreateElicitationResponse, type RequestPermissionRequest, type RequestPermissionResponse, type SessionNotification, @@ -59,6 +61,8 @@ export type InputStep = waitForToolCallUpdate?: string } | { op: 'cancel' } + | { op: 'setMode'; modeId: string } + | { op: 'setModeExpectError'; modeId: string } | { op: 'setConfigOption'; configId: string; value: string } | { op: 'setConfigOptionExpectError'; configId: string; value: string } @@ -78,6 +82,16 @@ export interface InputScript { * agent itself just sees `cancelled`, so it cannot absorb the bug). */ permissionAnswers?: PermissionAnswer[] + /** + * Ordered answers for the agent's `elicitation/create` round-trips (the + * ask_user_question / plan-review forms), consumed FIFO — the Nth request + * gets the Nth answer. Exhaustion (or no queue) answers `cancel`, the same + * fail-closed stub an elicitation-free scenario relies on. Unlike permission + * kinds, the scripted strings are not validated against the offered form — + * a stray `choice` reaches the agent verbatim, which reads it as a custom + * (non-consenting) answer, so a scenario bug fails safe in the transcript. + */ + elicitationAnswers?: ElicitationAnswer[] } /** One scripted answer to a permission request: which offered option kind to select. */ @@ -86,6 +100,16 @@ export interface PermissionAnswer { kind: 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always' } +/** One scripted answer to an elicitation form (accept with choice/custom content, or cancel). */ +export interface ElicitationAnswer { + /** Accept the form with the content below, or cancel it. */ + action: 'accept' | 'cancel' + /** The selected option label (the form's `choice` field). */ + choice?: string + /** Free-form text (the form's `custom` field). */ + custom?: string +} + /** One harvested session log plus the identifying facts off its header line. */ export interface HarvestedLog { /** The recorded session id (header `id`). */ @@ -215,6 +239,8 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise // Permission answers are consumed FIFO across the whole run; exhaustion // falls back to `cancelled` so approval-free scenarios keep the plain stub. const permissionQueue = [...input.permissionAnswers ?? []] + // Elicitation answers mirror the permission queue: FIFO, cancel on exhaustion. + const elicitationQueue = [...input.elicitationAnswers ?? []] // A scenario bug detected inside a client callback (a scripted permission // kind the agent never offered). It cannot fail the run from in there: a // callback throw only becomes a JSON-RPC error RESPONSE to the agent, and @@ -244,6 +270,17 @@ export async function runScenario(input: InputScript, opts: RunOptions): Promise } return Promise.resolve({ outcome: { outcome: 'selected', optionId: option.optionId } }) }, + createElicitation(_params: CreateElicitationRequest): Promise { + const answer = elicitationQueue.shift() + if (answer === undefined || answer.action !== 'accept') return Promise.resolve({ action: 'cancel' }) + return Promise.resolve({ + action: 'accept', + content: { + ...answer.choice !== undefined ? { choice: answer.choice } : {}, + ...answer.custom !== undefined ? { custom: answer.custom } : {}, + }, + }) + }, }) const active = launched await active.spawned @@ -399,6 +436,24 @@ async function runStep( await client.cancel({ sessionId }) return } + case 'setMode': { + const sessionId = getSessionId() + if (sessionId === undefined) throw new Error('snapshot-harness: setMode before newSession') + await client.setSessionMode({ sessionId, modeId: step.modeId }) + return + } + case 'setModeExpectError': { + const sessionId = getSessionId() + if (sessionId === undefined) throw new Error('snapshot-harness: setModeExpectError before newSession') + // The bridge rejects an unknown/uncomposed mode id with invalidParams; + // that rejection IS the expected wire behavior — swallow it so the run + // completes and the error frame is captured in the transcript. + await client.setSessionMode({ sessionId, modeId: step.modeId }).then( + () => { throw new Error('snapshot-harness: expected session/set_mode to be rejected but it succeeded') }, + () => { /* expected: the bridge rejected the mode id */ }, + ) + return + } case 'setConfigOption': { const sessionId = getSessionId() if (sessionId === undefined) throw new Error('snapshot-harness: setConfigOption before newSession') diff --git a/packages/support/acp-snapshot/src/index.ts b/packages/support/acp-snapshot/src/index.ts index 2a03947fef..53e7b0d5f9 100644 --- a/packages/support/acp-snapshot/src/index.ts +++ b/packages/support/acp-snapshot/src/index.ts @@ -18,6 +18,7 @@ export { runScenario, + type ElicitationAnswer, type HarvestedLog, type InputScript, type InputStep, diff --git a/packages/support/acp-snapshot/src/launcher.ts b/packages/support/acp-snapshot/src/launcher.ts index 441ab463d7..d10d05c15b 100644 --- a/packages/support/acp-snapshot/src/launcher.ts +++ b/packages/support/acp-snapshot/src/launcher.ts @@ -15,6 +15,8 @@ import { ndJsonStream, type Agent as AcpAgent, type Client, + type CreateElicitationRequest, + type CreateElicitationResponse, type RequestPermissionRequest, type RequestPermissionResponse, type SessionNotification, @@ -47,6 +49,8 @@ export interface AcpTestLaunchOptions { env?: NodeJS.ProcessEnv /** Permission handler; omitted requests fail closed as `cancelled`. */ requestPermission?: (params: RequestPermissionRequest) => Promise + /** Elicitation handler; omitted requests fail closed as `cancel`. */ + createElicitation?: (params: CreateElicitationRequest) => Promise } /** A running ACP test process and its captured client-side surfaces. */ @@ -152,6 +156,8 @@ export function launchAcpTestAgent(options: AcpTestLaunchOptions): LaunchedAcpTe } const requestPermission = options.requestPermission ?? (() => Promise.resolve({ outcome: { outcome: 'cancelled' as const } })) + const createElicitation = options.createElicitation + ?? (() => Promise.resolve({ action: 'cancel' as const })) const makeClient = (_agent: AcpAgent): Client => ({ sessionUpdate(params: SessionNotification): Promise { return trackClientCallback(() => { @@ -175,6 +181,7 @@ export function launchAcpTestAgent(options: AcpTestLaunchOptions): LaunchedAcpTe }) }, requestPermission: params => trackClientCallback(() => requestPermission(params)), + unstable_createElicitation: params => trackClientCallback(() => createElicitation(params)), }) const client = new ClientSideConnection(makeClient, stream) // `exit` only reports the parent process's status. Descendants may retain diff --git a/packages/support/acp-snapshot/tests/fixtures/fake-acp-agent.ts b/packages/support/acp-snapshot/tests/fixtures/fake-acp-agent.ts index 58d38ec7ba..26d2c41638 100644 --- a/packages/support/acp-snapshot/tests/fixtures/fake-acp-agent.ts +++ b/packages/support/acp-snapshot/tests/fixtures/fake-acp-agent.ts @@ -1,7 +1,19 @@ /** - * Scripted ACP agent for snapshot-kit tests. A fixture-adjacent `behavior.json` controls the - * subprocess reached through the real harness path; the bin reports observations over ACP and - * writes scripted logs before exiting on stdin EOF. + * Scripted fake ACP agent bin for `dsh-acp-snapshot`'s unit specs. Speaks + * newline-delimited JSON-RPC on stdio like the real `dsh-acp-agent` bin, but + * every behavior — how prompts settle, whether session/new rejects, which + * session logs get persisted, what filesystem noise to leave — comes from a + * `behavior.json` sitting NEXT to the `$DSH_SNAPSHOT_FILE` fixture, so a spec + * scripts a whole subprocess run from data. The specs launch it through the + * REAL `runScenario` spawn path (tsx loader, temp cwd, env plumbing), so the + * harness plumbing is exercised for real; only the agent behind the protocol + * is scripted. + * + * The specs (not the golden tier) own this bin: it asserts nothing, echoes + * observable facts into `session/update` text chunks (env probe, permission + * outcome, seeded-workspace listing) for the spec to read off `rawStdout`, and + * exits 0 on stdin EOF after writing the scripted logs — mirroring the real + * bin's dispose-flush-exit shape. */ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' @@ -39,6 +51,10 @@ interface Behavior { cancelToolCallUpdate?: boolean /** Before responding to a prompt, send a `session/request_permission` request and echo its outcome as a chunk. */ permissionProbe?: boolean + /** Before responding to a prompt, send an `elicitation/create` request and echo its response as a chunk. */ + elicitationProbe?: boolean + /** How `session/set_mode` settles: an empty response (echoing the modeId as a chunk) or a JSON-RPC error. */ + setMode?: 'respond' | 'error' /** Echo the `DSH_SNAPSHOT_*` env the harness set as a chunk (spec-side env-plumbing assertions). */ echoEnv?: boolean /** Echo the sorted cwd listing as a chunk (spec-side workspace-seeding assertions). */ @@ -84,8 +100,8 @@ let sessionId = '' let sessionCwd = '' /** The parked prompt request id while `hang-until-cancel` waits for the cancel notification. */ let parkedPromptId: number | string | null = null -/** Resolvers for permission-probe responses, keyed by outbound request id. */ -const pendingPermission = new Map void>() +/** Resolvers for outbound probe responses (permission/elicitation), keyed by request id. */ +const pendingOutbound = new Map void>() /** Per-run `session/set_config_option` state: config id → current value (first vocabulary entry until set). */ const currentConfig: Record = {} @@ -160,8 +176,8 @@ async function handlePrompt(id: number | string): Promise { } if (behavior.permissionProbe === true) { const requestId = nextOutboundId++ - const outcome = await new Promise((resolve) => { - pendingPermission.set(requestId, resolve) + const result = await new Promise((resolve) => { + pendingOutbound.set(requestId, resolve) send({ id: requestId, method: 'session/request_permission', @@ -175,7 +191,24 @@ async function handlePrompt(id: number | string): Promise { }, }) }) - chunk(`permission:${JSON.stringify(outcome)}`) + chunk(`permission:${JSON.stringify((result as { outcome?: unknown } | undefined)?.outcome ?? null)}`) + } + if (behavior.elicitationProbe === true) { + const requestId = nextOutboundId++ + const result = await new Promise((resolve) => { + pendingOutbound.set(requestId, resolve) + send({ + id: requestId, + method: 'elicitation/create', + params: { + sessionId, + mode: 'form', + message: 'Approve this plan and leave plan mode?', + requestedSchema: { type: 'object', title: 'Plan review', properties: { choice: { type: 'string' }, custom: { type: 'string' } }, required: [] }, + }, + }) + }) + chunk(`elicitation:${JSON.stringify(result ?? null)}`) } switch (behavior.prompt ?? 'respond') { case 'respond': @@ -195,10 +228,10 @@ function handleFrame(frame: Record): void { const method = frame.method as string | undefined const params = (frame.params ?? {}) as Record // A response to one of OUR outbound requests (the permission probe). - if (method === undefined && id !== undefined && typeof id === 'number' && pendingPermission.has(id)) { - const resolve = pendingPermission.get(id) as (outcome: unknown) => void - pendingPermission.delete(id) - resolve((frame.result as { outcome?: unknown } | undefined)?.outcome ?? null) + if (method === undefined && id !== undefined && typeof id === 'number' && pendingOutbound.has(id)) { + const resolve = pendingOutbound.get(id) as (result: unknown) => void + pendingOutbound.delete(id) + resolve(frame.result) return } switch (method) { @@ -219,6 +252,14 @@ function handleFrame(frame: Record): void { case 'session/prompt': void handlePrompt(id as number | string) return + case 'session/set_mode': + if ((behavior.setMode ?? 'respond') === 'error') { + respondError(id as number | string, 'unknown mode') + return + } + chunk(`setMode:${String(params.modeId)}`) + respond(id as number | string, {}) + return case 'session/set_config_option': { const vocabulary = behavior.configOptions const configId = params.configId as string diff --git a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/session.1.jsonl b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/session.1.jsonl new file mode 100644 index 0000000000..1caf2610b3 --- /dev/null +++ b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/session.1.jsonl @@ -0,0 +1,2 @@ +{"type":"session","id":"abababab-cdcd-4efe-8ada-badabadabada","createdAt":800,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-KBQJbW","parentSession":"f6fa7fcf-dd9c-4b39-8815-b25ddcebfd88"} +{"type":"request/header","seq":0,"time":2,"data":{"header":{"config":{"model":"fake"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} diff --git a/packages/support/acp-snapshot/tests/harness.spec.ts b/packages/support/acp-snapshot/tests/harness.spec.ts index f50fdd8905..7363ecff16 100644 --- a/packages/support/acp-snapshot/tests/harness.spec.ts +++ b/packages/support/acp-snapshot/tests/harness.spec.ts @@ -95,8 +95,8 @@ describe('runScenario', () => { expect(clientClosed).toBe(true) }) - it('centralizes ACP boot, captures, updates, fail-closed permissions, and shutdown', { timeout: 20_000 }, async () => { - const { dir, fixtureFile } = await scenario({ permissionProbe: true, echoEnv: true, stderrNote: 'launcher stderr' }) + it('centralizes ACP boot, captures, updates, fail-closed interactions, and shutdown', { timeout: 20_000 }, async () => { + const { dir, fixtureFile } = await scenario({ permissionProbe: true, elicitationProbe: true, echoEnv: true, stderrNote: 'launcher stderr' }) const sessionsRoot = await mkdtemp(join(tmpdir(), 'acp-launcher-sessions-')) tempDirs.push(sessionsRoot) const launched = launchAcpTestAgent({ @@ -120,6 +120,7 @@ describe('runScenario', () => { expect((await nextChunk).sessionUpdate).toBe('agent_message_chunk') expect(launched.updates.some(update => update.sessionUpdate === 'agent_message_chunk')).toBe(true) expect(launched.rawStdout()).toContain('permission:{\\"outcome\\":\\"cancelled\\"}') + expect(launched.rawStdout()).toContain('elicitation:{\\"action\\":\\"cancel\\"}') expect(launched.stderr()).toContain('launcher stderr') const unmatched = expect(launched.waitForUpdate(() => false)).rejects.toThrow(/update stream closed/) await launched.close() @@ -716,6 +717,69 @@ describe('runScenario', () => { expect(result.sessionLogs).toHaveLength(0) }) + it('drives session/set_mode and swallows the expected rejection of setModeExpectError', { timeout: 20_000 }, async () => { + const { fixtureFile } = await scenario({}) + const result = await runScenario( + { steps: [...boot, { op: 'setMode', modeId: 'plan' }] }, + { agent: AGENT, mode: 'replay', fixtureFile }, + ) + expect(result.rawStdout).toContain('setMode:plan') + + const rejecting = await scenario({ setMode: 'error' }) + const rejected = await runScenario( + { steps: [...boot, { op: 'setModeExpectError', modeId: 'yolo' }] }, + { agent: AGENT, mode: 'replay', fixtureFile: rejecting.fixtureFile }, + ) + expect(rejected.rawStdout).toContain('unknown mode') + }) + + it('fails the run when setModeExpectError unexpectedly succeeds, and both mode ops require a session', { timeout: 20_000 }, async () => { + const { fixtureFile } = await scenario({}) + await expect(runScenario( + { steps: [...boot, { op: 'setModeExpectError', modeId: 'plan' }] }, + { agent: AGENT, mode: 'replay', fixtureFile }, + )).rejects.toThrow(/expected session\/set_mode to be rejected/) + await expect(runScenario( + { steps: [{ op: 'initialize' }, { op: 'setMode', modeId: 'plan' }] }, + { agent: AGENT, mode: 'replay', fixtureFile }, + )).rejects.toThrow(/setMode before newSession/) + await expect(runScenario( + { steps: [{ op: 'initialize' }, { op: 'setModeExpectError', modeId: 'plan' }] }, + { agent: AGENT, mode: 'replay', fixtureFile }, + )).rejects.toThrow(/setModeExpectError before newSession/) + }) + + it('answers elicitations from the scripted queue, falling back to cancel on exhaustion', { timeout: 20_000 }, async () => { + const { fixtureFile } = await scenario({ elicitationProbe: true }) + // Three prompts → three elicitations: an accept-with-choice, an + // accept-with-custom (feedback), then the exhausted-queue cancel. + const result = await runScenario( + { + steps: [...boot, { op: 'prompt', text: 'one' }, { op: 'prompt', text: 'two' }, { op: 'prompt', text: 'three' }], + elicitationAnswers: [ + { action: 'accept', choice: 'Approve' }, + { action: 'accept', custom: 'add tests first' }, + ], + }, + { agent: AGENT, mode: 'replay', fixtureFile }, + ) + const first = result.rawStdout.indexOf('elicitation:{\\"action\\":\\"accept\\",\\"content\\":{\\"choice\\":\\"Approve\\"}}') + const second = result.rawStdout.indexOf('elicitation:{\\"action\\":\\"accept\\",\\"content\\":{\\"custom\\":\\"add tests first\\"}}') + const third = result.rawStdout.indexOf('elicitation:{\\"action\\":\\"cancel\\"}') + expect(first).toBeGreaterThanOrEqual(0) + expect(second).toBeGreaterThan(first) + expect(third).toBeGreaterThan(second) + }) + + it('a scripted elicitation cancel answers cancel', { timeout: 20_000 }, async () => { + const { fixtureFile } = await scenario({ elicitationProbe: true }) + const result = await runScenario( + { steps: [...boot, { op: 'prompt', text: 'one' }], elicitationAnswers: [{ action: 'cancel' }] }, + { agent: AGENT, mode: 'replay', fixtureFile }, + ) + expect(result.rawStdout).toContain('elicitation:{\\"action\\":\\"cancel\\"}') + }) + it('answers permission requests from the scripted queue by option kind, falling back to cancelled', { timeout: 20_000 }, async () => { const { fixtureFile } = await scenario({ permissionProbe: true }) // Two prompts → two permission round-trips; one scripted answer, so the @@ -744,8 +808,11 @@ describe('runScenario', () => { it('rejects the run on a scripted permission kind the agent never offered', { timeout: 20_000 }, async () => { const { fixtureFile } = await scenario({ permissionProbe: true }) - // The fake offers only allow_once/reject_once. The harness must reject an impossible click, - // not merely send an RPC error that a tolerant agent could absorb. + // The fake bin offers allow_once/reject_once; scripting allow_always is a + // scenario bug. The agent is answered `cancelled` (it must not be able to + // absorb the bug as an error-means-denial), and the RUN fails: a callback + // throw would only reach the agent as a JSON-RPC error response, letting + // a tolerant agent carry on and the scenario pass — or record. await expect(runScenario( { steps: [...boot, { op: 'prompt', text: 'impossible click' }], permissionAnswers: [{ kind: 'allow_always' }] }, { agent: AGENT, mode: 'replay', fixtureFile }, diff --git a/packages/ui/acp/acp-feature-support.md b/packages/ui/acp/acp-feature-support.md index 60c7b19adf..8b5730ee85 100644 --- a/packages/ui/acp/acp-feature-support.md +++ b/packages/ui/acp/acp-feature-support.md @@ -10,7 +10,7 @@ Legend: ✅ supported · ⚠️ partial / fallback · ❌ not yet · — n/a. Th ## At a glance -The bridge implements the **core prompt-turn loop** for N concurrent sessions: initialize, session new/load, prompt, cancel, streamed assistant/thought chunks, tool-call rendering (including Zed terminal cards), resumable session replay, slash commands, one-shot permission prompts, per-session model selection, and permission presets. The largest **unbuilt** areas are **MCP passthrough** and **agent plans**, plus the client **filesystem** and **terminal** method families (which the adapters mostly do NOT drive either — see rows 43-49). See [Gap summary](#gap-summary). +The bridge implements the **core prompt-turn loop** for N concurrent sessions: initialize, session new/load, prompt, cancel, streamed assistant/thought chunks, tool-call rendering (including Zed terminal cards), resumable session replay, slash commands, one-shot permission prompts, per-session model selection and permission presets, and **session modes** (the picker, via `@deepseek-ai/dsh-plan-mode`). The largest **unbuilt** areas are **MCP passthrough** and **agent plans**, plus the client **filesystem** and **terminal** method families (which the adapters mostly do NOT drive either — see rows 43-49). See [Gap summary](#gap-summary). ## 1. Agent methods (client → agent) @@ -25,7 +25,7 @@ The bridge implements the **core prompt-turn loop** for N concurrent sessions: i | `session/close` | S | ❌ | ✅ | ✅ | No `session/close` handler — the SDK dispatch returns `method_not_found`. The bridge tears sessions down on client disconnect / Cordis disposal (cross-cutting, see [§8](#8-cross-cutting)), but that is not the on-demand per-session method. | | `session/prompt` | S | ✅ | ✅ | ✅ | A flattened prompt beginning with `/` dispatches through `ctx.commands` without a model request; ordinary input maps to `agent.send`. One request is in flight per session. | | `session/cancel` | S | ✅ | ✅ | ✅ | Aborts the exact direct command, or applies queue-aware `agent.cancel` and settles its prompt `cancelled`, scoped to one session. | -| `session/set_mode` | S | ❌ | ✅ | ✅ | Session modes deliberately skipped: config options are the spec's replacement and modes are slated for removal in ACP v2 (see [§6](#6-session-modes--config-options--models)). | +| `session/set_mode` | S | ✅ | ✅ | ✅ | Composed opportunistically: with `@deepseek-ai/dsh-plan-mode` mounted, `session/new`/`session/load` advertise the fixed `default` / `plan` projection and `session/set_mode` records the boolean pending intent (optimistic `current_mode_update`; logged `plan/mode` lands at the turn boundary). Without the plugin: no `modes` advertised, `set_mode` rejected (see [§6 Modes](#6-session-modes--config-options--models)). | | `session/set_config_option` | S | ✅ | ✅ | ✅ | A provider/model select is present for a complete registered target; one `permission` select is added when `ctx.permission` is composed. Every response carries the complete refreshed state. | | model selection | S | ✅ | ✅ | ✅ | No distinct stable `session/set_model` — model is the `model`-category `session/set_config_option`. Values preserve the provider/model pair, catalogs come from `ctx.llm`, selection is per session, and `session/load` restores the last requested pair. Codex also supports the legacy `unstable_setSessionModel` ext method. | | `session/list` | S | ❌ | ✅ | ✅ | Gated by `sessionCapabilities.list`. The harness HAS `sessionPersistence.list()` (used internally for load-cwd validation) but does not expose it over ACP. | @@ -85,7 +85,7 @@ These are capabilities the bridge would *drive* on the editor. The harness runs | `tool_call_update` | S | ✅ | ✅ | ✅ | From appended `tool/result` via `presentResult`; replacement results rewrite model context and do not duplicate or overwrite execution presentation. | | `plan` | S | ❌ | ✅ | ✅ | No agent plan emitted. Both adapters emit real plan entries (Codex's `CodexEventHandler.updatePlan` maps `turn/plan/updated` → `{ sessionUpdate: 'plan', entries }`). | | `available_commands_update` | S | ✅ | ✅ | ✅ | Full effective snapshot after create/load and registry changes; names, descriptions, and unstructured-input hints come from `ctx.commands`. | -| `current_mode_update` | S | ❌ | ✅ | ✅ | No session modes. | +| `current_mode_update` | S | ✅ | ✅ | ✅ | Echoed optimistically on `session/set_mode` and re-notified when a logged `plan/mode` maps to a different wire id (covers the `exit_plan_mode` tool flipping the session back). | | `config_option_update` | S | ❌ | ✅ | ✅ | Config options exist (advertised in `session/new`/`session/load`, switched via `session/set_config_option`), but the bridge never pushes agent-initiated changes — an operator default drift is narrated to the MODEL, not echoed to the editor. Future work in the [sandbox Agent Note § Per-session mode switching](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). | | `usage_update` | S | ❌ | ✅ | ✅ | Token/cost reporting not surfaced (the harness records token usage internally on `assistant/message`). | | `session_info_update` | S | ❌ | ⚠️ | ⚠️ | Session title/metadata not pushed. | @@ -111,7 +111,7 @@ Tool-call presentation is **owned by each tool** (`presentCall` / `presentResult ## 6. Session modes / config options / models -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. Session modes stay deliberately unmodeled because config options replace them in ACP v2. 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/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). ## 7. Content blocks diff --git a/packages/ui/acp/package.json b/packages/ui/acp/package.json index ed8121a806..1d0e480889 100644 --- a/packages/ui/acp/package.json +++ b/packages/ui/acp/package.json @@ -38,6 +38,7 @@ "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-llm-retry": "^0.0.1", + "@deepseek-ai/dsh-plan-mode": "^0.0.1", "@deepseek-ai/dsh-permission": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", @@ -61,6 +62,7 @@ "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", + "@deepseek-ai/dsh-plan-mode": "workspace:^", "@deepseek-ai/dsh-permission": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", diff --git a/packages/ui/acp/src/index.ts b/packages/ui/acp/src/index.ts index f3b18c1c50..2293c9b0a1 100644 --- a/packages/ui/acp/src/index.ts +++ b/packages/ui/acp/src/index.ts @@ -36,11 +36,14 @@ import { type PromptRequest, type PromptResponse, type SessionConfigOption, + type SessionModeState, type SessionConfigSelectGroup, type SessionConfigSelectOption, type SessionNotification, type SetSessionConfigOptionRequest, type SetSessionConfigOptionResponse, + type SetSessionModeRequest, + type SetSessionModeResponse, type Stream, type StopReason, } from '@agentclientprotocol/sdk' @@ -64,6 +67,9 @@ import type { ToolCallView, ToolRegistry, ToolResultView, TerminalResultView } f // Side-effect type import: declaration-merges `ctx.sessionPersistence` onto // Context (the bridge injects it and reads `list()` for load cwd validation). import type {} from '@deepseek-ai/dsh-session-persistence' +// Type-only edge: resolves `ctx.get('planMode')` when dsh-plan-mode is composed; +// the runtime read stays opportunistic. +import type {} from '@deepseek-ai/dsh-plan-mode' // Side-effect type import: declaration-merges prompt assembly onto Context and // the scoped waterfall used to keep persona variables aligned with requests. import type {} from '@deepseek-ai/dsh-system-prompt' @@ -94,6 +100,18 @@ function invalidParams(detail: string): RequestError { return RequestError.invalidParams(undefined, detail) } +const DEFAULT_SESSION_MODE_ID = 'default' +const PLAN_SESSION_MODE_ID = 'plan' +const AVAILABLE_SESSION_MODES = [ + { id: DEFAULT_SESSION_MODE_ID, name: DEFAULT_SESSION_MODE_ID }, + { id: PLAN_SESSION_MODE_ID, name: PLAN_SESSION_MODE_ID }, +] + +/** Map plan state onto ACP's named collaboration-mode protocol. */ +function sessionModeId(active: boolean): string { + return active ? PLAN_SESSION_MODE_ID : DEFAULT_SESSION_MODE_ID +} + /** Render arbitrary thrown values without trusting their string coercion. */ function renderThrown(value: unknown): string { try { @@ -186,11 +204,14 @@ function elicitationForQuestion( options: AskUserQuestionOption[], ): CreateElicitationRequest { const title = question.header ?? 'Question' + const message = question.detail === undefined + ? question.question + : `${question.question}\n\n${question.detail}` if (options.length === 0) { return { sessionId, mode: 'form', - message: question.question, + message, requestedSchema: { type: 'object', title, @@ -224,7 +245,7 @@ function elicitationForQuestion( return { sessionId, mode: 'form', - message: question.question, + message, requestedSchema: { type: 'object', title, @@ -286,6 +307,13 @@ interface SessionRecord { presenter: ToolPresenter /** Terminal capability snapshot shared by matching call and result updates. */ terminalEnabled: boolean + /** + * The last mode id this session sent to the client (advertised at + * session/new+load, echoed optimistically on session/set_mode, re-notified on + * each logged `plan/mode` that differs). `undefined` when dsh-plan-mode is + * not composed, so no mode surface is advertised or notified. + */ + lastModeId: string | undefined /** Session-local provider/model selection and the current step snapshot. */ target: LlmTargetRef /** In-flight prompt and its captured turn number for exact settlement. */ @@ -542,6 +570,21 @@ export function apply(ctx: Context, config: AcpConfig): void { // --- Stream the harness event taxonomy to ACP session/update -------------- + // --- Session modes (dsh-plan-mode, opportunistic) ------------------------- + // ACP's generic mode picker projects the one plan capability as the fixed + // `default` / `plan` vocabulary. A selection is echoed optimistically; the + // logged `plan/mode` follows at the boundary and tool-driven exits are + // re-notified from that event. Environment knobs remain config options. + const modesStateFor = (agent: Agent): SessionModeState | undefined => { + const planMode = ctx.get('planMode') + if (planMode === undefined) return undefined + const { active, pending } = planMode.get(agent) + return { + availableModes: AVAILABLE_SESSION_MODES, + currentModeId: sessionModeId(pending ?? active), + } + } + // All content streaming AND the prompt settle flow through `session/event`, // the canonical log: every assistant/chunk and tool/call/result is logged, so // translating from the log makes live streaming and `session/load` replay @@ -567,6 +610,20 @@ export function apply(ctx: Context, config: AcpConfig): void { cwd: session.header.cwd, }, { includeUserMessages: false }) } finally { + // Re-notify from the EVENT's value, not from planMode.get(): the service + // holds one coalesced pending slot (every flush reads the latest + // selection, so a flush can never be stale against the picker), and for + // any other writer — the exit tool, a test, a foreign plugin — the logged + // value IS the truth the picker should track, in log order. Inside the + // containment `finally` like the prompt settlement: a throwing presenter + // must not desync the picker. + if (event.type === 'plan/mode') { + const modeId = sessionModeId(event.data.active) + if (modeId !== rec.lastModeId) { + rec.lastModeId = modeId + notify({ sessionId: rec.agent.session.id, update: { sessionUpdate: 'current_mode_update', currentModeId: modeId } }) + } + } const inflight = rec.inflight if (inflight !== undefined && event.type === 'turn/start') { // The first message-triggered turn after prompt installation owns the @@ -726,11 +783,13 @@ export function apply(ctx: Context, config: AcpConfig): void { await handle.dispose() throw internalError('connection closed during session/new') } + const modes = modesStateFor(handle.agent) const record: SessionRecord = { agent: handle.agent, dispose: () => handle.dispose(), presenter: makePresenter(handle.agent), terminalEnabled: terminalOutputCap, + lastModeId: modes?.currentModeId, target, inflight: undefined, commandAbort: undefined, @@ -739,7 +798,11 @@ export function apply(ctx: Context, config: AcpConfig): void { sessions.set(sessionId, record) pendingCommandSnapshots.set(sessionId, record) const configOptions = configOptionsFor(handle.agent, directory) - return { sessionId, ...configOptions.length > 0 ? { configOptions } : {} } + return { + sessionId, + ...modes !== undefined ? { modes } : {}, + ...configOptions.length > 0 ? { configOptions } : {}, + } }, async loadSession(params: LoadSessionRequest): Promise { @@ -811,11 +874,13 @@ export function apply(ctx: Context, config: AcpConfig): void { // the replay below and the post-load live stream) so a later // `initialize` can't desync the call/result of a tool card. const terminalEnabled = terminalOutputCap + const modes = modesStateFor(agent) const record: SessionRecord = { agent, dispose: () => handle.dispose(), presenter: makePresenter(agent), terminalEnabled, + lastModeId: modes?.currentModeId, target, inflight: undefined, commandAbort: undefined, @@ -845,12 +910,33 @@ export function apply(ctx: Context, config: AcpConfig): void { } notifyCommands(record) const configOptions = configOptionsFor(agent, directory) - return configOptions.length > 0 ? { configOptions } : {} + return { + ...modes !== undefined ? { modes } : {}, + ...configOptions.length > 0 ? { configOptions } : {}, + } } finally { loadingIds.delete(sessionId) } }, + setSessionMode(params: SetSessionModeRequest): Promise { + assertOpen() + const rec = requireSession(SessionId(params.sessionId)) + const planMode = ctx.get('planMode') + if (planMode === undefined) throw invalidParams('session modes are not composed in this deployment') + if (params.modeId !== DEFAULT_SESSION_MODE_ID && params.modeId !== PLAN_SESSION_MODE_ID) { + throw invalidParams(`unknown session mode ${JSON.stringify(params.modeId)} — available modes: default, plan`) + } + planMode.set(rec.agent, params.modeId === PLAN_SESSION_MODE_ID) + // Optimistic echo: the pending mode IS the user's selection; the logged + // `plan/mode` lands at the next turn boundary and, matching lastModeId, + // is not re-notified. A no-op selection (already current) echoes too — + // cheap, idempotent, and the picker settles regardless. + rec.lastModeId = params.modeId + notify({ sessionId: rec.agent.session.id, update: { sessionUpdate: 'current_mode_update', currentModeId: params.modeId } }) + return Promise.resolve({}) + }, + async prompt(params: PromptRequest): Promise { assertOpen() const rec = requireSession(SessionId(params.sessionId)) diff --git a/packages/ui/acp/tests/bridge.spec.ts b/packages/ui/acp/tests/bridge.spec.ts index e7170c380f..093bc38ca3 100644 --- a/packages/ui/acp/tests/bridge.spec.ts +++ b/packages/ui/acp/tests/bridge.spec.ts @@ -143,15 +143,18 @@ describe('acp bridge', () => { questions: [{ id: 'language', question: 'Which language?', + detail: 'Choose the implementation language for this project.', options: [{ label: 'TypeScript' }], }], }) expect(result).toEqual({ answers: [{ id: 'language', selected: [], custom: 'Use Zig' }] }) expect(harness.elicitationRequests[0]).toMatchObject({ + message: 'Which language?\n\nChoose the implementation language for this project.', requestedSchema: { properties: { choice: { + title: 'Which language?', description: 'Choose one option, or fill a custom answer below.', oneOf: [{ const: 'TypeScript', title: 'TypeScript' }], }, diff --git a/packages/ui/acp/tests/harness.ts b/packages/ui/acp/tests/harness.ts index b728ee5bfc..626a6118b2 100644 --- a/packages/ui/acp/tests/harness.ts +++ b/packages/ui/acp/tests/harness.ts @@ -17,6 +17,7 @@ import * as FsPolicy from '@deepseek-ai/dsh-fs-policy' import * as ToolBash from '@deepseek-ai/dsh-tool-bash' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' import * as ToolTodo from '@deepseek-ai/dsh-tool-todo' +import PlanModeService from '@deepseek-ai/dsh-plan-mode' import { ClientSideConnection, ndJsonStream, @@ -191,6 +192,8 @@ export async function makeBridgeHarness(options: { * tool + the bridge's own todo/write→plan mapping, not a stand-in. */ withTodo?: boolean + /** Plug the REAL `dsh-plan-mode` plugin so a test can drive the session-mode picker. */ + withModes?: boolean /** * Plug the REAL filesystem stack (`dsh-fs-local` + `dsh-fs-policy` + * `dsh-tool-fs`) so a test can drive `read`/`write`/`edit` through the bridge @@ -225,6 +228,9 @@ export async function makeBridgeHarness(options: { if (options.withTodo) { await ctx.plugin(ToolTodo) } + if (options.withModes) { + await ctx.plugin(PlanModeService, { section: 'Test plan mode instructions.' }) + } if (options.withFs) { await ctx.plugin(LocalFileSystem, { cwd: options.fsCwd ?? options.storageDir }) await ctx.plugin(FsPolicy) diff --git a/packages/ui/acp/tests/modes.spec.ts b/packages/ui/acp/tests/modes.spec.ts new file mode 100644 index 0000000000..7680d611c4 --- /dev/null +++ b/packages/ui/acp/tests/modes.spec.ts @@ -0,0 +1,116 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk' +import { SessionId } from '@deepseek-ai/dsh-session' +import { makeBridgeHarness, textResponse, type BridgeHarness, type CapturedUpdate } from './harness.ts' + +/** The `current_mode_update` notifications, in order. */ +function modeUpdates(updates: CapturedUpdate[]): string[] { + return updates + .filter(update => update.sessionUpdate === 'current_mode_update') + .map(update => update.currentModeId) +} + +describe('acp bridge — plan mode projection', () => { + let storageDir: string + let harness: BridgeHarness | undefined + let loader: BridgeHarness | undefined + + beforeEach(async () => { storageDir = await mkdtemp(join(tmpdir(), 'acp-modes-')) }) + afterEach(async () => { + if (harness) await harness.dispose() + if (loader) await loader.dispose() + harness = loader = undefined + await rm(storageDir, { recursive: true, force: true }) + }) + + it('advertises no mode surface and rejects session/set_mode when plan mode is not composed', async () => { + harness = await makeBridgeHarness({ storageDir }) + await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + const res = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] }) + expect(res.modes).toBeUndefined() + await expect(harness.client.setSessionMode({ sessionId: res.sessionId, modeId: 'plan' })) + .rejects.toMatchObject({ message: expect.stringContaining('session modes are not composed') as string }) + }) + + it('advertises availableModes/currentModeId on session/new', async () => { + harness = await makeBridgeHarness({ storageDir, withModes: true }) + await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + const res = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] }) + expect(res.modes).toEqual({ + availableModes: [ + { id: 'default', name: 'default' }, + { id: 'plan', name: 'plan' }, + ], + currentModeId: 'default', + }) + }) + + it('session/set_mode records the pending intent and echoes one optimistic current_mode_update', async () => { + harness = await makeBridgeHarness({ storageDir, withModes: true }) + await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] }) + await harness.client.setSessionMode({ sessionId, modeId: 'plan' }) + expect(modeUpdates(harness.updates)).toEqual(['plan']) + const agent = harness.ctx.agents.get(SessionId(sessionId))! + expect(harness.ctx.planMode.get(agent)).toEqual({ active: false, pending: true }) + }) + + it('rejects an unknown ACP mode id at the adapter boundary', async () => { + harness = await makeBridgeHarness({ storageDir, withModes: true }) + await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] }) + await expect(harness.client.setSessionMode({ sessionId, modeId: 'nope' })) + .rejects.toMatchObject({ message: expect.stringContaining('unknown session mode "nope"') as string }) + expect(modeUpdates(harness.updates)).toEqual([]) + }) + + it('does not re-notify when the boundary flush logs the mode the picker already showed', async () => { + harness = await makeBridgeHarness({ storageDir, withModes: true, script: [textResponse('planning')] }) + await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] }) + await harness.client.setSessionMode({ sessionId, modeId: 'plan' }) + await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go plan' }] }) + const agent = harness.ctx.agents.get(SessionId(sessionId))! + expect(agent.session.events.some(event => event.type === 'plan/mode')).toBe(true) + expect(modeUpdates(harness.updates)).toEqual(['plan']) + }) + + it('re-notifies on a logged flip the picker has not seen (the tool-driven exit shape)', async () => { + harness = await makeBridgeHarness({ storageDir, withModes: true, script: [textResponse('planning')] }) + await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] }) + await harness.client.setSessionMode({ sessionId, modeId: 'plan' }) + await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go plan' }] }) + // A writer other than the picker (exit_plan_mode's execute) appends the + // flip back; the bridge must re-notify the client off the logged event. + const agent = harness.ctx.agents.get(SessionId(sessionId))! + agent.session.append('plan/mode', { active: false }) + // The notification crosses the in-memory JSON-RPC transport asynchronously. + await new Promise(resolve => setTimeout(resolve, 20)) + expect(modeUpdates(harness.updates)).toEqual(['plan', 'default']) + }) + + it('advertises the folded mode on session/load', async () => { + harness = await makeBridgeHarness({ storageDir, withModes: true, script: [textResponse('planning')] }) + await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] }) + await harness.client.setSessionMode({ sessionId, modeId: 'plan' }) + await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go plan' }] }) + await harness.dispose() + harness = undefined + + loader = await makeBridgeHarness({ storageDir, withModes: true, script: [] }) + await loader.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + const res = await loader.client.loadSession({ sessionId, cwd: process.cwd(), mcpServers: [] }) + expect(res.modes).toEqual({ + availableModes: [ + { id: 'default', name: 'default' }, + { id: 'plan', name: 'plan' }, + ], + currentModeId: 'plan', + }) + }) +}) diff --git a/packages/ui/acp/tsconfig.json b/packages/ui/acp/tsconfig.json index bd6d074c11..f334d4a5ea 100644 --- a/packages/ui/acp/tsconfig.json +++ b/packages/ui/acp/tsconfig.json @@ -41,6 +41,9 @@ { "path": "../user-interaction" }, + { + "path": "../../plan/plan-mode" + }, { "path": "../../session-persistence/session-persistence" }, diff --git a/packages/ui/commands/README.md b/packages/ui/commands/README.md index 6e0efa458b..0c9eb04267 100644 --- a/packages/ui/commands/README.md +++ b/packages/ui/commands/README.md @@ -10,7 +10,7 @@ Plugin-owned human-command registry shared by the TUI and ACP adapters. The [plu `parseCommand()` recognizes a slash at byte zero, a lowercase name containing letters, digits, `_`, or `-`, and either end-of-input or whitespace. It returns every byte after the name as `rawInput`, including separator whitespace; consumers own their command-specific grammar and may normalize only what that grammar permits. -Handlers return `success` or `error` plus optional UI text. Results are rendered directly by the adapter and never enter model history. The registry races handler completion against the supplied abort signal, but an uncooperative handler may continue its own external side effects after the caller stops awaiting it. +Handlers return `success` or `error` plus optional UI text. Results are rendered directly by the adapter and never enter model history. The registry never submits `rawInput` to the agent implicitly; a command producer may explicitly schedule model-visible work through the receiving `Agent`, in which case that producer owns the resulting message contract. The registry races handler completion against the supplied abort signal, but an uncooperative handler may continue its own external side effects after the caller stops awaiting it. ## Composition @@ -22,11 +22,11 @@ The terminal and ACP app bundles mount this service with their consuming front d #### What the model sees -Nothing. Known slash commands execute in the UI command plane, and their `CommandResult` text is not submitted as a user message. Unknown slash-command input is rejected by shipped adapters instead of becoming a model prompt. +The registry itself submits nothing. Known slash commands execute in the UI command plane, and their `CommandResult` text is not submitted as a user message. Unknown slash-command input is rejected by shipped adapters instead of becoming a model prompt. A command producer may explicitly use the receiving `Agent`; for example, [`dsh-plan-mode`](../../plan/plan-mode/README.md#model-and-human-surfaces) submits the optional message in `/plan [message]` after selecting plan mode. #### Token effect -Command discovery, execution, and UI output add no model tokens. A command plugin may separately mutate a model-visible domain through that domain's durable APIs. +Command discovery, execution, and UI output add no model tokens. Explicit agent work scheduled by a command producer has the same token effect as the corresponding agent input. #### KV Cache effect diff --git a/packages/ui/tui/README.md b/packages/ui/tui/README.md index 119803fd9c..0840ec7c9d 100644 --- a/packages/ui/tui/README.md +++ b/packages/ui/tui/README.md @@ -14,7 +14,7 @@ An embedding may provide `TuiRuntime.formatCwd` when its logical workspace label Before model output, session events, tool presenters, questions, configuration, or diagnostics reach pi-tui's ANSI-aware renderers or the terminal title, the TUI renders C0 and C1 controls other than line feeds as visible `\xNN` text. Those sources cannot add terminal control sequences; the TUI and pi-tui retain ownership of terminal rendering and styling. -While the agent is running, ordinary editor submissions call `agent.steer()`; otherwise they call `agent.send()`. A slash at the start of the submitted line enters `ctx.commands` instead: known commands execute directly, unknown commands produce a warning, and neither path reaches the model. The TUI registers `/help`, `/model`, `/clear`, `/reasoning`, `/tools`, `/redraw`, `/reload`, `/resume`, `/status`, and `/exit` as agent-scoped definitions; every other effective command joins autocomplete and `/help` dynamically, as do `/skill:` completions. A status line above the editor reports the turn phase the TUI derives from session events — waiting for the first token, thinking, responding, or executing tools — with the elapsed time in that phase and the running step total, refreshed each second, and ends with the `Enter sends steering, Esc cancels` hint; while steering messages wait to reach the model it inserts a `N queued ·` badge before the hint that clears as each drains. Ctrl+C or Escape cancels a running turn. Tool cards collapse long bodies into a configurable head/tail preview; Ctrl+O toggles every card between its preview and full output. Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle. +While the agent is running, ordinary editor submissions call `agent.steer()`; otherwise they call `agent.send()`. A slash at the start of the submitted line enters `ctx.commands` instead: known commands execute directly, unknown commands produce a warning, and neither path automatically reaches the model. A command producer may explicitly schedule agent work; [`dsh-plan-mode`](../../plan/plan-mode/README.md#model-and-human-surfaces) uses that contract for `/plan [message]`. The TUI registers `/help`, `/model`, `/clear`, `/reasoning`, `/tools`, `/redraw`, `/reload`, `/resume`, `/status`, and `/exit` as agent-scoped definitions; every other effective command joins autocomplete and `/help` dynamically, as do `/skill:` completions. A status line above the editor reports the turn phase the TUI derives from session events — waiting for the first token, thinking, responding, or executing tools — with the elapsed time in that phase and the running step total, refreshed each second, and ends with the `Enter sends steering, Esc cancels` hint; while steering messages wait to reach the model it inserts a `N queued ·` badge before the hint that clears as each drains. Ctrl+C or Escape cancels a running turn. Tool cards collapse long bodies into a configurable head/tail preview; Ctrl+O toggles every card between its preview and full output. Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle. `/model` opens the advisory `ctx.llm` catalog as a keyboard selector: Up/Down moves, Enter selects, and Escape closes it. `/model ` still selects an unambiguous model id directly, while `/model /` selects an exact target. The configured target or latest logged request header initializes the selector, and an unlisted current model remains visible because catalogs are advisory. Selection is local to this TUI session. Prompt assembly snapshots the target for one step, replaces `{{provider}}` and `{{model}}`, and applies the same pair through `agent/request`; a switch during assembly therefore starts with a later step. The request header durably records targets that reach the model, while an unused selection remains process-local. @@ -67,7 +67,7 @@ The palette uses the standard 16-color ANSI foregrounds and SGR attributes, whic #### What the model sees -Each non-empty ordinary editor submission becomes one text block, sent with `agent.send()` while the target agent is idle and `agent.steer()` while it is running. Slash commands and keybindings are TUI-only; command results remain terminal notices. +Each non-empty ordinary editor submission becomes one text block, sent with `agent.send()` while the target agent is idle and `agent.steer()` while it is running. Slash commands and keybindings are TUI-only; command results remain terminal notices. A command producer may schedule a separate agent input, such as the optional message accepted by `/plan [message]`. #### Token effect diff --git a/packages/ui/tui/src/index.ts b/packages/ui/tui/src/index.ts index cd6ab99f2b..4e59747128 100644 --- a/packages/ui/tui/src/index.ts +++ b/packages/ui/tui/src/index.ts @@ -1201,9 +1201,15 @@ class QuestionDialog implements Component, Focusable { const lines = [ this.palette.muted(header), ...wrapTextWithAnsi(this.palette.text(displayText(this.question.question)), innerWidth), - '', ] const push = (line: string): void => { lines.push(line) } + // Supporting detail (e.g. the full plan under review) renders between the + // question and the answer surface, kept out of option labels. + if (this.question.detail !== undefined) { + push('') + for (const line of wrapTextWithAnsi(displayText(this.question.detail), innerWidth)) push(line) + } + push('') if (this.mode === 'custom') { for (const line of this.input.render(innerWidth)) push(line) push(this.palette.dim(this.options.length > 0 ? 'Enter submit • Esc options' : 'Enter submit • Esc cancel')) diff --git a/packages/ui/tui/tests/tui.spec.ts b/packages/ui/tui/tests/tui.spec.ts index a089389f0e..0c8450b04c 100644 --- a/packages/ui/tui/tests/tui.spec.ts +++ b/packages/ui/tui/tests/tui.spec.ts @@ -1688,12 +1688,13 @@ describe('TUI user-interaction dialogs', () => { const single = result.ctx.userInteraction.ask({ questions: [{ - id: 'mode', header: 'Mode', question: 'Choose a mode', + id: 'mode', header: 'Mode', question: 'Choose a mode', detail: 'This choice controls the next turn.', options: [{ label: 'Safe', description: 'Use checks' }, { label: 'Fast' }], }], }) await tick() expect(result.terminal.output).toContain('Choose a mode') + expect(result.terminal.output).toContain('This choice controls the next turn.') expect(result.terminal.output).toContain('Question 1/1 (1 unanswered) · Mode') expect(result.terminal.output).toContain('1/2') result.terminal.send('\x1b[B') diff --git a/packages/ui/user-interaction/README.md b/packages/ui/user-interaction/README.md index c026ff0395..153e9a270d 100644 --- a/packages/ui/user-interaction/README.md +++ b/packages/ui/user-interaction/README.md @@ -11,7 +11,7 @@ Abstract user-interaction seam. It owns `ctx.userInteraction`, the service a mod ### Key Types -- `AskUserQuestionRequest` — `{ questions: [{ id, question, header?, options?, multiSelect? }], agent?, signal? }`. +- `AskUserQuestionRequest` — `{ questions: [{ id, question, detail?, header?, options?, multiSelect? }], agent?, signal? }`; `detail` supplies supporting text that providers render with the question without turning it into an option label. - `AskUserQuestionOption` — `{ label, description? }`. - `AskUserQuestionAnswer` — `{ answers: [{ id, selected, custom? }] }`. - `UserInteractionProvider` — UI implementation with `ask(request)`. diff --git a/packages/ui/user-interaction/src/types.ts b/packages/ui/user-interaction/src/types.ts index 2ebb4503ce..861ab7c12a 100644 --- a/packages/ui/user-interaction/src/types.ts +++ b/packages/ui/user-interaction/src/types.ts @@ -13,12 +13,14 @@ export interface AskUserQuestionOption { description?: string } -/** One question in an ask_user_question request. */ +/** One question in a user-interaction request. */ export interface AskUserQuestionItem { - /** Stable model-provided question id, echoed in the answer. */ + /** Stable caller-provided question id, echoed in the answer. */ id: string /** The question to display. */ question: string + /** Optional supporting detail rendered with the question but kept out of option labels. */ + detail?: string /** Optional short heading/group label. */ header?: string /** Optional choices the UI can render as a menu. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2aba52e701..773b05f87d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -249,6 +249,9 @@ importers: '@deepseek-ai/dsh-lsp-local': specifier: workspace:* version: link:../packages/lsp/lsp-local + '@deepseek-ai/dsh-plan-mode': + specifier: workspace:* + version: link:../packages/plan/plan-mode '@deepseek-ai/dsh-permission': specifier: workspace:* version: link:../packages/ui/permission @@ -294,6 +297,9 @@ importers: '@deepseek-ai/dsh-token-meter': specifier: workspace:* version: link:../packages/llm/token-meter + '@deepseek-ai/dsh-tool-ask-user': + specifier: workspace:* + version: link:../packages/ui/tool-ask-user '@deepseek-ai/dsh-tool-cordis': specifier: workspace:* version: link:../packages/cordis/tool-cordis @@ -2259,6 +2265,42 @@ importers: specifier: ^4.4.3 version: 4.4.3 + packages/plan/plan-mode: + devDependencies: + '@deepseek-ai/dsh-agent': + specifier: workspace:^ + version: link:../../core/agent + '@deepseek-ai/dsh-agent-loop': + specifier: workspace:^ + version: link:../../core/agent-loop + '@deepseek-ai/dsh-code-runtime': + specifier: workspace:^ + version: link:../../code-runtime/code-runtime + '@deepseek-ai/dsh-commands': + specifier: workspace:^ + version: link:../../ui/commands + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt + '@deepseek-ai/dsh-tools': + specifier: workspace:^ + version: link:../../core/tools + '@deepseek-ai/dsh-user-interaction': + specifier: workspace:^ + version: link:../../ui/user-interaction + cordis: + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) + packages/sandbox/sandbox: devDependencies: '@deepseek-ai/dsh-invariants': @@ -3221,6 +3263,9 @@ importers: '@deepseek-ai/dsh-llm-retry': specifier: workspace:^ version: link:../../llm/llm-retry + '@deepseek-ai/dsh-plan-mode': + specifier: workspace:^ + version: link:../../plan/plan-mode '@deepseek-ai/dsh-permission': specifier: workspace:^ version: link:../permission @@ -3946,6 +3991,9 @@ importers: '@deepseek-ai/dsh-llm-retry': specifier: workspace:^ version: link:../../packages/llm/llm-retry + '@deepseek-ai/dsh-plan-mode': + specifier: workspace:^ + version: link:../../packages/plan/plan-mode '@deepseek-ai/dsh-paths': specifier: workspace:^ version: link:../../packages/util/paths diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index 32f2fa072b..b794f84368 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -39,6 +39,7 @@ "@deepseek-ai/dsh-llm-deepseek": "workspace:^", "@deepseek-ai/dsh-llm-pi-ai": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", + "@deepseek-ai/dsh-plan-mode": "workspace:^", "@deepseek-ai/dsh-permission": "workspace:^", "@deepseek-ai/dsh-paths": "workspace:^", "@deepseek-ai/dsh-repeat-tool-guard": "workspace:^", diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 11f63f89b2..73b62114ba 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -70,6 +70,7 @@ const GROUP_ORDER = [ 'web', 'spill', 'todo', + 'plan', 'cordis', 'hooks', 'session-persistence', @@ -170,6 +171,14 @@ const SERVICE_ROLES: ServiceRole[] = [ consumers: ['tool-ask-user', 'tui', 'acp'], note: 'UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise.', }, + { + key: 'planMode', + pkg: 'plan-mode', + title: 'Plan collaboration state', + mode: 'core', + consumers: ['acp'], + note: 'Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions.', + }, { key: 'commands', pkg: 'commands', diff --git a/scripts/gen-module-graph.ts b/scripts/gen-module-graph.ts index 66c0e12f7c..f2d7514d23 100644 --- a/scripts/gen-module-graph.ts +++ b/scripts/gen-module-graph.ts @@ -31,6 +31,7 @@ const GROUP_ORDER = [ 'spill', 'timeout', 'todo', + 'plan', 'cordis', 'hooks', 'session-persistence', diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 867e67542b..e99af7fcc6 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -19,6 +19,7 @@ import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from ' import LocalBashExecutor from '@deepseek-ai/dsh-bash-local' import LocalFileSystem from '@deepseek-ai/dsh-fs-local' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' +import PlanModeService from '@deepseek-ai/dsh-plan-mode' import WebService from '@deepseek-ai/dsh-web' import * as WebSearchExa from '@deepseek-ai/dsh-web-search-exa' import * as WebFetchLocal from '@deepseek-ai/dsh-web-fetch-local' @@ -172,6 +173,18 @@ const TOOL_PACKAGES: ToolPackage[] = [ note: 'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.', }, + { + pkg: '@deepseek-ai/dsh-plan-mode', + dir: 'plan-mode', + source: 'packages/plan/plan-mode/src/index.ts', + requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.userInteraction (execution time, opportunistic)'], + writes: ['tool/call', 'plan/mode inactive on an approved review', 'tool/result'], + async mount(ctx) { + await ctx.plugin(PlanModeService, { section: 'Tool catalog schema harvest.' }) + }, + note: + 'exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-interaction seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary.', + }, { pkg: '@deepseek-ai/dsh-tool-bash', dir: 'tool-bash', diff --git a/scripts/prepare-ci-bubblewrap.sh b/scripts/prepare-ci-bubblewrap.sh new file mode 100755 index 0000000000..00a513db8f --- /dev/null +++ b/scripts/prepare-ci-bubblewrap.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Ubuntu's package transaction scans the hosted image's full dpkg database and +# runs post-install hooks. CI needs only the signed-archive payload, so pin and +# verify that payload before extracting it into the ephemeral runner directory. +readonly BUBBLEWRAP_VERSION='0.9.0-1ubuntu0.1' +readonly BUBBLEWRAP_SHA256='1b506492bd9c7fd0cdb4f02ac822f1d3e336b0aead5113c1239baf8db5db562a' +readonly BUBBLEWRAP_URL="https://archive.ubuntu.com/ubuntu/pool/main/b/bubblewrap/bubblewrap_${BUBBLEWRAP_VERSION}_amd64.deb" + +: "${RUNNER_TEMP:?prepare-ci-bubblewrap requires RUNNER_TEMP}" +: "${GITHUB_PATH:?prepare-ci-bubblewrap requires GITHUB_PATH}" + +if [[ "$(uname -s)" != 'Linux' || "$(uname -m)" != 'x86_64' ]]; then + echo 'prepare-ci-bubblewrap supports only Linux x86_64 hosted runners' >&2 + exit 1 +fi + +archive="${RUNNER_TEMP}/bubblewrap_${BUBBLEWRAP_VERSION}_amd64.deb" +root="${RUNNER_TEMP}/dsh-bubblewrap" + +curl --fail --silent --show-error --location --retry 3 --output "$archive" "$BUBBLEWRAP_URL" +printf '%s %s\n' "$BUBBLEWRAP_SHA256" "$archive" | sha256sum --check --status +mkdir -p "$root" +dpkg-deb --extract "$archive" "$root" +printf '%s\n' "$root/usr/bin" >> "$GITHUB_PATH" + +sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 \ + || echo 'apparmor userns knob absent — the functional probe decides' +"$root/usr/bin/bwrap" --version +"$root/usr/bin/bwrap" --ro-bind / / --dev /dev --proc /proc --die-with-parent -- true +echo 'bubblewrap functional probe passed' diff --git a/scripts/publint-all.spec.ts b/scripts/publint-all.spec.ts new file mode 100644 index 0000000000..22dd80d6b0 --- /dev/null +++ b/scripts/publint-all.spec.ts @@ -0,0 +1,61 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { spawnSync } from 'node:child_process' +import { afterEach, describe, expect, it } from 'vitest' + +const repositoryRoot = fileURLToPath(new URL('..', import.meta.url)) +const runner = fileURLToPath(new URL('./publint-all.ts', import.meta.url)) +const roots: string[] = [] + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +function fixture(exportPath = './lib/index.js'): string { + const root = mkdtempSync(join(tmpdir(), 'dsh-publint-all-')) + roots.push(root) + const packageDir = join(root, 'packages/core/probe') + mkdirSync(join(packageDir, 'lib'), { recursive: true }) + writeFileSync(join(packageDir, 'package.json'), `${JSON.stringify({ + name: '@deepseek-ai/dsh-probe', + version: '0.0.1', + type: 'module', + license: 'MIT', + engines: { node: '>=22.19' }, + sideEffects: false, + files: ['lib'], + exports: { '.': { default: exportPath } }, + }, null, 2)}\n`) + writeFileSync(join(packageDir, 'README.md'), '# Probe\n') + writeFileSync(join(packageDir, 'lib/index.js'), 'export const probe = true\n') + writeFileSync(join(packageDir, 'unpublished.js'), 'export const hidden = true\n') + return root +} + +function run(root: string) { + return spawnSync(process.execPath, [ + '--import', 'tsx', runner, + '--packages-root', root, + ], { + cwd: repositoryRoot, + encoding: 'utf8', + timeout: 5_000, + }) +} + +describe('publint package runner', () => { + it('lints recursively declared files from an in-memory publication view', () => { + const result = run(fixture()) + expect(result.status, result.stderr).toBe(0) + expect(result.stdout).toContain('linting 1 package(s)') + expect(result.stdout).toContain('All good!') + }) + + it('rejects an export that exists in the workspace but is not published', () => { + const result = run(fixture('./unpublished.js')) + expect(result.status).toBe(1) + expect(result.stdout).toContain('unpublished.js') + }) +}) diff --git a/scripts/publint-all.ts b/scripts/publint-all.ts index 0911316f18..2ed1906763 100644 --- a/scripts/publint-all.ts +++ b/scripts/publint-all.ts @@ -1,46 +1,53 @@ -import { execFile } from 'node:child_process' -import { existsSync, readdirSync } from 'node:fs' +/** Run publint over the exact manifest-declared publication view of every package. */ + +import { + globSync, + readFileSync, + readdirSync, + statSync, +} from 'node:fs' import { availableParallelism } from 'node:os' -import { resolve } from 'node:path' -import { promisify } from 'node:util' +import { dirname, relative, resolve, sep } from 'node:path' +import { publint, type Message, type PackFile } from 'publint' +import { formatMessage } from 'publint/utils' -const execFileAsync = promisify(execFile) const CONCURRENCY_ENV = 'DSH_PUBLINT_CONCURRENCY' +const repositoryRoot = resolve(import.meta.dirname, '..') +const options = parseOptions(process.argv.slice(2)) +const packagesRoot = resolve(options.get('--packages-root') ?? repositoryRoot) -// Discover harness packages at packages//; group containers, -// examples, and private vendored sources are not package targets. -const root = resolve(import.meta.dirname, '..') -const packagesRoot = resolve(root, 'packages') +interface PackageTarget { + path: string + directory: string + manifest: PackageManifest +} -// Run publint's JS CLI through the current node, not the .bin shim: the -// extensionless shim isn't spawnable on Windows (CVE-2024-27980) and the .cmd -// variant needs shell:true, which space-joins args UNESCAPED (DEP0190) and -// breaks when the repo path contains spaces. The JS entry is identical on every -// platform (`bin` is `./src/cli.js` per publint's package.json). -const publintCli = resolve(root, 'node_modules/publint/src/cli.js') +interface PackageManifest { + name?: string + files?: unknown +} type PublintResult = - | { path: string; status: 'passed'; stdout: string; stderr: string } - | { path: string; status: 'failed'; stdout: string; stderr: string; message: string } + | { path: string; status: 'passed'; messages: Message[]; manifest: Record } + | { path: string; status: 'failed'; messages: Message[]; manifest: Record; failure?: string } -function workspacePackages(): string[] { - return readdirSync(packagesRoot, { withFileTypes: true }) - .filter(group => group.isDirectory()) - .flatMap(group => - readdirSync(resolve(packagesRoot, group.name), { withFileTypes: true }) - .filter(pkg => pkg.isDirectory()) - .filter(pkg => existsSync(resolve(packagesRoot, group.name, pkg.name, 'package.json'))) - .map(pkg => `packages/${group.name}/${pkg.name}`), - ) +function workspacePackages(): PackageTarget[] { + return globSync('packages/*/*/package.json', { cwd: packagesRoot }) + .sort() + .map((manifestPath) => { + const absoluteManifestPath = resolve(packagesRoot, manifestPath) + const manifest = JSON.parse(readFileSync(absoluteManifestPath, 'utf8')) as PackageManifest + return { path: dirname(manifestPath), directory: dirname(absoluteManifestPath), manifest } + }) } function publintConcurrency(total: number): number { if (total === 0) return 0 const raw = process.env[CONCURRENCY_ENV] - if (raw !== undefined) { + if (raw !== undefined && raw !== '') { const parsed = Number.parseInt(raw, 10) - if (!Number.isSafeInteger(parsed) || parsed < 1) { + if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) { throw new Error(`publint-all: ${CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`) } return Math.min(total, parsed) @@ -49,57 +56,106 @@ function publintConcurrency(total: number): number { return Math.min(total, availableParallelism()) } -function outputText(value: unknown): string { - if (typeof value === 'string') return value - if (Buffer.isBuffer(value)) return value.toString() - return '' +function publicationFiles(target: PackageTarget): PackFile[] { + const paths = new Set() + addPath(resolve(target.directory, 'package.json'), paths) + const declared = Array.isArray(target.manifest.files) + ? target.manifest.files.filter((value): value is string => typeof value === 'string') + : [] + for (const pattern of [ + ...declared, + 'README*', + 'LICENSE*', + 'LICENCE*', + 'CHANGELOG*', + 'CHANGES*', + 'HISTORY*', + 'NOTICE*', + ]) { + for (const match of globSync(pattern, { cwd: target.directory })) { + addPath(resolve(target.directory, match), paths) + } + } + + return [...paths] + .sort() + .map(path => ({ + name: `package/${relative(target.directory, path).split(sep).join('/')}`, + data: readFileSync(path), + })) } -async function runPublint(path: string): Promise { +function addPath(path: string, paths: Set): void { + const stat = statSync(path) + if (stat.isDirectory()) { + for (const entry of readdirSync(path)) addPath(resolve(path, entry), paths) + } else if (stat.isFile()) { + paths.add(path) + } +} + +async function runPublint(target: PackageTarget): Promise { try { - const { stdout, stderr } = await execFileAsync(process.execPath, [publintCli, path], { - cwd: root, - encoding: 'utf8', - maxBuffer: 10 * 1024 * 1024, + const result = await publint({ + pkgDir: 'package', + pack: { files: publicationFiles(target) }, }) - return { path, status: 'passed', stdout, stderr } + const manifest = result.pkg as Record + return result.messages.some(message => message.type === 'error') + ? { path: target.path, status: 'failed', messages: result.messages, manifest } + : { path: target.path, status: 'passed', messages: result.messages, manifest } } catch (error: unknown) { - const failed = error as { stdout?: unknown; stderr?: unknown; message?: string } return { - path, + path: target.path, status: 'failed', - stdout: outputText(failed.stdout), - stderr: outputText(failed.stderr), - message: failed.message ?? 'publint failed', + messages: [], + manifest: target.manifest as Record, + failure: error instanceof Error ? error.message : String(error), } } } -async function runAll(paths: string[], concurrency: number): Promise { +async function runAll(targets: PackageTarget[], concurrency: number): Promise { let next = 0 const results: Array = [] await Promise.all(Array.from({ length: concurrency }, async () => { for (;;) { const index = next next += 1 - const path = paths[index] - if (path === undefined) return - results[index] = await runPublint(path) + const target = targets[index] + if (target === undefined) return + results[index] = await runPublint(target) } })) - return paths.map((path, index) => { + return targets.map((target, index) => { const result = results[index] - if (result === undefined) throw new Error(`publint-all: missing result for ${path}.`) + if (result === undefined) throw new Error(`publint-all: missing result for ${target.path}.`) return result }) } function printResult(result: PublintResult): void { console.log(`Running publint for ${result.path}...`) - process.stdout.write(result.stdout) - process.stderr.write(result.stderr) - if (result.status === 'failed') console.error(result.message) + if ('failure' in result) console.error(result.failure) + for (const message of result.messages) { + console.log(formatMessage(message, result.manifest, { color: false }) ?? message.code) + } + if (result.status === 'passed' && result.messages.length === 0) console.log('All good!') +} + +function parseOptions(args: string[]): Map { + const parsed = new Map() + for (let index = 0; index < args.length; index += 2) { + const name = args[index] + const value = args[index + 1] + if (name !== '--packages-root' || value === undefined || value.startsWith('--')) { + throw new Error(`publint-all: expected [--packages-root PATH], got ${JSON.stringify(args)}.`) + } + if (parsed.has(name)) throw new Error(`publint-all: duplicate option ${name}.`) + parsed.set(name, value) + } + return parsed } const packages = workspacePackages() diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 964fe7ef7e..59ee171572 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -16,6 +16,9 @@ type Mode = | 'ci-coverage' | 'ci-snapshot' | 'ci-artifacts' + | 'ci-windows-blocking' + | 'ci-windows-complete' + | 'ci-windows-observational' | 'node-compat' | 'pre-push' | 'manual-push' @@ -32,6 +35,7 @@ interface Gate { env?: Record input?: string verify?: (result: GateResult) => Promise + allowFailure?: boolean } interface GateResult { @@ -77,7 +81,9 @@ console.log(`run-gates: ${mode} running ${gates.length} gate(s) with ${maxConcur const results = await runGates(gates, maxConcurrency) printSummary(results, performance.now() - startedAt) -if (results.some(result => result.status === 'failed' || result.status === 'skipped')) process.exit(1) +if (results.some(result => result.gate.allowFailure !== true && (result.status === 'failed' || result.status === 'skipped'))) { + process.exit(1) +} function parseMode(raw: string | undefined): Mode { switch (raw) { @@ -87,13 +93,17 @@ function parseMode(raw: string | undefined): Mode { case 'ci-coverage': case 'ci-snapshot': case 'ci-artifacts': + case 'ci-windows-blocking': + case 'ci-windows-complete': + case 'ci-windows-observational': case 'node-compat': case 'pre-push': + case 'manual-push': case 'doc-sync': return raw default: throw new Error( - `run-gates: expected mode ci-primary | ci-static | ci-lint | ci-coverage | ci-snapshot | ci-artifacts | node-compat | pre-push | doc-sync, got ${JSON.stringify(raw)}.`, + `run-gates: expected mode ci-primary | ci-static | ci-lint | ci-coverage | ci-snapshot | ci-artifacts | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | pre-push | manual-push | doc-sync, got ${JSON.stringify(raw)}.`, ) } } @@ -167,31 +177,19 @@ function gatesForMode(selected: Mode): Gate[] { pnpmScript('duplication', 'duplication'), ] case 'ci-coverage': - return [ - pnpmScript('build', 'build'), - coverageGate(), - ] + return [coverageGate()] case 'ci-snapshot': - return [ - pnpmScript('build', 'build'), - snapshotGate(), - ] + return [pnpmScript('build', 'build'), snapshotGate()] case 'ci-artifacts': return ciArtifactGates() + case 'ci-windows-blocking': + return ciWindowsBlockingGates() + case 'ci-windows-complete': + return ciWindowsCompleteGates() + case 'ci-windows-observational': + return ciWindowsObservationalGates() case 'node-compat': - return [ - pnpmScript('typecheck', 'typecheck'), - pnpmExec('source-worker-smoke', [ - 'vitest', - 'run', - 'packages/workflow/workflow-workerthread/tests/source-worker.compat.spec.ts', - ], { label: 'source worker smoke' }), - pnpmExec('jsonl-zstd-smoke', [ - 'vitest', - 'run', - 'packages/session-persistence/session-persistence-jsonl/tests/zstd.compat.spec.ts', - ], { label: 'JSONL Zstandard smoke' }), - ] + return nodeCompatGates() case 'pre-push': return [] case 'manual-push': return [ @@ -225,11 +223,12 @@ function ciPrimaryGates(): Gate[] { lintGate(), pnpmScript('duplication', 'duplication'), coverageGate(), + ...nodeCompatSmokeGates(), snapshotGate(), ...docSyncLeafGates(), pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }), pnpmScript('knip', 'knip'), - pnpmScript('build', 'build', { needs: ['typecheck'] }), + pnpmScript('build', 'build'), pnpmScript('publint', 'publint', { needs: ['build'] }), pnpmScript('node-next-types', 'verify-node-next-types', { label: 'node-next types', @@ -240,13 +239,40 @@ function ciPrimaryGates(): Gate[] { ] } +function nodeCompatGates(): Gate[] { + return [ + ...flagEnabled('DSH_NODE_COMPAT_SKIP_TYPECHECK') ? [] : [pnpmScript('typecheck', 'typecheck')], + ...nodeCompatSmokeGates(), + ] +} + +function nodeCompatSmokeGates(): Gate[] { + return [ + pnpmExec('source-worker-smoke', [ + 'vitest', + 'run', + 'packages/workflow/workflow-workerthread/tests/source-worker.compat.spec.ts', + ], { label: 'source worker smoke' }), + pnpmExec('jsonl-zstd-smoke', [ + 'vitest', + 'run', + 'packages/session-persistence/session-persistence-jsonl/tests/zstd.compat.spec.ts', + ], { label: 'JSONL Zstandard smoke' }), + ] +} + function ciStaticGates(): Gate[] { return [ pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }), pnpmScript('constraints', 'constraints'), pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }), pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }), - ...docSyncLeafGates(), + pnpmScript('build', 'build'), + ...docSyncLeafGates({ + docTypecheckNeeds: ['build'], + docTypecheckEnv: { DSH_DOC_TYPECHECK_USE_BUILD_OUTPUT: '1' }, + docsBuildScript: 'docs:build:mpa', + }), pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }), pnpmScript('knip', 'knip'), ] @@ -265,11 +291,54 @@ function ciArtifactGates(): Gate[] { ] } -function lintGate(): Gate { +function ciWindowsBlockingGates(): Gate[] { + return [ + pnpmScript('windows-build', 'build', { label: 'build' }), + pnpmScript('windows-site', 'docs:build', { label: 'production site' }), + ] +} + +function ciWindowsCompleteGates(): Gate[] { + const observational = ciWindowsObservationalGates() + // The required production site replaces the observational MPA build; both + // VitePress modes write the same output directory and cannot overlap. + .filter(gate => gate.id !== 'build' && gate.id !== 'docs-site-build') + .map(gate => ({ ...gate, allowFailure: true })) + return [ + pnpmScript('build', 'build'), + pnpmScript('windows-site', 'docs:build', { label: 'production site' }), + ...observational, + ] +} + +function ciWindowsObservationalGates(): Gate[] { + return [ + ...ciStaticGates(), + lintGate(), + pnpmScript('duplication', 'duplication'), + { + ...coverageGate(), + env: { DSH_EXAMPLE_MODE: 'lib' }, + needs: ['build'], + }, + snapshotGate(), + pnpmScript('publint', 'publint', { needs: ['build'] }), + pnpmScript('node-next-types', 'verify-node-next-types', { + label: 'node-next types', + needs: ['build'], + }), + builtPackageInvariantsGate(['build']), + builtBinSmokeGate(), + ] +} + +function lintGate(eslintTargets: readonly string[] = ['.']): Gate { + const concurrencyArgs = eslintConcurrencyArgs() if (process.env.DSH_ESLINT_CACHE === '1') { return pnpmExec('lint', [ 'eslint', - '.', + ...eslintTargets, + ...concurrencyArgs, '--cache', '--cache-location', '.cache/eslint/', @@ -280,11 +349,28 @@ function lintGate(): Gate { env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') }, }) } + if (concurrencyArgs.length > 0) { + return pnpmExec('lint', ['eslint', ...eslintTargets, ...concurrencyArgs], { + label: 'lint', + env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') }, + }) + } return pnpmScript('lint', 'lint', { env: { NODE_OPTIONS: nodeOptions('--max-old-space-size=8192') }, }) } +function eslintConcurrencyArgs(): string[] { + const raw = process.env.DSH_ESLINT_CONCURRENCY + if (raw === undefined || raw === '') return [] + if (raw === 'auto') return ['--concurrency=auto'] + const parsed = Number.parseInt(raw, 10) + if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) { + throw new Error(`run-gates: DSH_ESLINT_CONCURRENCY must be a positive integer or auto, got ${JSON.stringify(raw)}.`) + } + return [`--concurrency=${raw}`] +} + function coverageGate(): Gate { return pnpmExec('coverage', [ 'vitest', @@ -293,8 +379,6 @@ function coverageGate(): Gate { ...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'), ], { label: 'test:coverage', - env: { DSH_EXAMPLE_MODE: 'lib' }, - needs: ['build'], }) } @@ -325,6 +409,13 @@ function positiveIntArg(envName: string, flag: string): string[] { return [`${flag}=${raw}`] } +function flagEnabled(envName: string): boolean { + const raw = process.env[envName] + if (raw === undefined || raw === '') return false + if (raw !== '1') throw new Error(`run-gates: ${envName} must be 1 when set, got ${JSON.stringify(raw)}.`) + return true +} + function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] { const artifactOptions = options.artifactNeeds === undefined ? {} : { needs: options.artifactNeeds } return [ @@ -343,6 +434,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] { function docSyncLeafGates(options: { docTypecheckNeeds?: string[] docTypecheckEnv?: Record + docsBuildScript?: 'docs:build' | 'docs:build:mpa' } = {}): Gate[] { const docTypecheckOptions: Partial = {} if (options.docTypecheckNeeds !== undefined) docTypecheckOptions.needs = options.docTypecheckNeeds @@ -369,8 +461,11 @@ function docSyncLeafGates(options: { pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }), pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }), pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }), - // Keep the VitePress build in this single gate because projection rewrites website/.generated. - pnpmScript('docs-site', 'docs:check', { label: 'documentation site' }), + pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts'], { + label: 'documentation projection', + }), + // Keep the VitePress build itself in one gate because projection rewrites website/.generated. + pnpmScript('docs-site-build', options.docsBuildScript ?? 'docs:build', { label: 'documentation build' }), pnpmScript('package-readme-limitations', 'verify-package-readme-limitations', { label: 'package README limitations' }), ] } @@ -540,7 +635,8 @@ function printSummary(results: GateResult[], durationMs: number): void { for (const result of unsuccessful) { const duration = (result.durationMs / 1000).toFixed(2) const reason = result.error ?? (result.exitCode === null ? 'no exit code' : `exit ${result.exitCode}`) - console.error(` - ${result.status.toUpperCase()} ${result.gate.label} (${duration}s, ${reason})`) + const disposition = result.gate.allowFailure === true ? 'NON-BLOCKING ' : '' + console.error(` - ${disposition}${result.status.toUpperCase()} ${result.gate.label} (${duration}s, ${reason})`) console.error(` ${result.gate.displayCommand}`) } } diff --git a/scripts/verify-built-package-invariants.mjs b/scripts/verify-built-package-invariants.mjs index 4b298946d1..9c672e05f0 100644 --- a/scripts/verify-built-package-invariants.mjs +++ b/scripts/verify-built-package-invariants.mjs @@ -1,102 +1,106 @@ -/** Verify every packed companion through its package self-reference under plain Node. */ +/** Verify every compiled companion through its staged package self-reference under plain Node. */ -import { spawnSync } from 'node:child_process' import { copyFileSync, + cpSync, + existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, + writeFileSync, } from 'node:fs' import { dirname, resolve } from 'node:path' import { pathToFileURL } from 'node:url' -const root = resolve(import.meta.dirname, '..') -const loaderUrl = pathToFileURL(resolve(root, 'vendor/loader/lib/index.js')).href +const repositoryRoot = resolve(import.meta.dirname, '..') +const options = parseOptions(process.argv.slice(2)) +const packagesRoot = resolve(options.get('--packages-root') ?? repositoryRoot) +const loaderUrl = options.get('--loader-url') + ?? pathToFileURL(resolve(repositoryRoot, 'vendor/loader/lib/index.js')).href const failures = [] -const manifests = globSync('packages/*/*/package.json', { cwd: root }).sort() -const packArgs = ['pack', '--dry-run', '--json', '--ignore-scripts'] -// Windows cannot spawn npm's .cmd shim directly; setup-node installs this JS -// entrypoint beside node.exe, so the probe stays shell-free on every runner. -const npmInvocation = process.platform === 'win32' - ? [process.execPath, [resolve(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'), ...packArgs]] - : ['npm', packArgs] +const manifests = globSync('packages/*/*/package.json', { cwd: packagesRoot }).sort() +const { default: Loader } = await import(loaderUrl) +const loader = Object.create(Loader.prototype) for (const manifestPath of manifests) { - const packageDir = dirname(resolve(root, manifestPath)) - const manifest = JSON.parse(readFileSync(resolve(root, manifestPath), 'utf8')) + const packageDir = dirname(resolve(packagesRoot, manifestPath)) + const manifest = JSON.parse(readFileSync(resolve(packagesRoot, manifestPath), 'utf8')) const packageName = manifest.name if (typeof packageName !== 'string' || packageName.length === 0) { failures.push(`${manifestPath}: missing package name`) continue } - - const pack = spawnSync(npmInvocation[0], npmInvocation[1], { - cwd: packageDir, - encoding: 'utf8', - }) - if (pack.status !== 0) { - const detail = pack.error?.message - ?? (pack.stderr.trim() || pack.stdout.trim() || `npm pack exited ${pack.status}`) - failures.push(`${packageName}: ${detail}`) + const invariantExport = manifest.exports?.['./invariant'] + if (typeof invariantExport !== 'object' + || invariantExport.default !== './lib/invariant.js' + || !manifest.files?.includes('lib/invariant.js')) { + failures.push(`${packageName}: manifest does not publish ./lib/invariant.js as ./invariant`) continue } - let files - try { - const result = JSON.parse(pack.stdout) - files = result[0]?.files - if (!Array.isArray(files)) throw new Error('npm pack returned no file inventory') - } catch (error) { - failures.push(`${packageName}: cannot parse npm pack inventory: ${String(error)}`) - continue - } - - // Keep the packed view below its owning package so Node reaches the real + // Keep the staged view below its owning package so Node reaches the real // pnpm dependency links. Junctioning node_modules elsewhere breaks pnpm's - // relative workspace links on Windows. - const stagedPackageDir = mkdtempSync(resolve(packageDir, '.dsh-packed-invariant-')) + // relative workspace links on Windows. Copy the manifest-declared lib view + // so a companion that imports an undeclared runtime chunk fails here. + const stagedPackageDir = mkdtempSync(resolve(packageDir, '.dsh-built-invariant-')) try { - for (const file of files) { - if (typeof file.path !== 'string' - || (file.path !== 'package.json' && !file.path.startsWith('lib/'))) continue - const target = resolve(stagedPackageDir, file.path) - mkdirSync(dirname(target), { recursive: true }) - copyFileSync(resolve(packageDir, file.path), target) - } - - const probe = ` - const companion = await import(${JSON.stringify(`${packageName}/invariant`)}); - const { default: Loader } = await import(${JSON.stringify(loaderUrl)}); - if ('default' in companion) throw new Error('companion has a default export'); - const loader = Object.create(Loader.prototype); - const unwrapped = loader.unwrapExports(companion); - if (unwrapped !== companion) throw new Error('Loader collapsed the companion namespace'); - if (typeof unwrapped.name !== 'string') throw new Error('companion name is missing'); - if (!Array.isArray(unwrapped.inject) || !unwrapped.inject.includes('invariants')) { - throw new Error('companion does not inject invariants'); - } - if (typeof unwrapped.apply !== 'function') throw new Error('companion apply is missing'); - ` - const result = spawnSync(process.execPath, ['--input-type=module', '--eval', probe], { - cwd: stagedPackageDir, - encoding: 'utf8', - }) - if (result.status !== 0) { - const detail = result.error?.message - ?? (result.stderr.trim() || result.stdout.trim() || `node exited ${result.status}`) - failures.push(`${packageName}: ${detail}`) + copyFileSync(resolve(packageDir, 'package.json'), resolve(stagedPackageDir, 'package.json')) + copyDeclaredLibFiles(packageDir, stagedPackageDir, manifest.files) + const probePath = resolve(stagedPackageDir, 'probe.mjs') + writeFileSync( + probePath, + `import * as companion from ${JSON.stringify(`${packageName}/invariant`)}\nexport default companion\n`, + ) + const { default: companion } = await import(pathToFileURL(probePath).href) + if ('default' in companion) throw new Error('companion has a default export') + const unwrapped = loader.unwrapExports(companion) + if (unwrapped !== companion) throw new Error('Loader collapsed the companion namespace') + if (typeof unwrapped.name !== 'string') throw new Error('companion name is missing') + if (!Array.isArray(unwrapped.inject) || !unwrapped.inject.includes('invariants')) { + throw new Error('companion does not inject invariants') } + if (typeof unwrapped.apply !== 'function') throw new Error('companion apply is missing') + } catch (error) { + failures.push(`${packageName}: ${error instanceof Error ? error.message : String(error)}`) } finally { rmSync(stagedPackageDir, { recursive: true, force: true }) } } if (failures.length > 0) { - console.error('verify-built-package-invariants: packed companion failures:') + console.error('verify-built-package-invariants: compiled companion failures:') for (const failure of failures) console.error(` ${failure}`) process.exit(1) } -console.log(`verify-built-package-invariants: ${manifests.length} packed companion(s) passed plain-Node Loader checks.`) +console.log(`verify-built-package-invariants: ${manifests.length} compiled companion(s) passed plain-Node Loader checks.`) + +function parseOptions(args) { + const allowed = new Set(['--packages-root', '--loader-url']) + const parsed = new Map() + for (let index = 0; index < args.length; index += 2) { + const name = args[index] + const value = args[index + 1] + if (!allowed.has(name) || value === undefined || value.startsWith('--')) { + throw new Error(`verify-built-package-invariants: expected [--packages-root PATH] [--loader-url URL], got ${JSON.stringify(args)}.`) + } + if (parsed.has(name)) throw new Error(`verify-built-package-invariants: duplicate option ${name}.`) + parsed.set(name, value) + } + return parsed +} + +function copyDeclaredLibFiles(packageDir, stagedPackageDir, files) { + for (const pattern of files) { + if (!pattern.startsWith('lib/')) continue + for (const relativePath of globSync(pattern, { cwd: packageDir })) { + const source = resolve(packageDir, relativePath) + if (!existsSync(source)) continue + const target = resolve(stagedPackageDir, relativePath) + mkdirSync(dirname(target), { recursive: true }) + cpSync(source, target, { recursive: true }) + } + } +} diff --git a/scripts/verify-built-package-invariants.spec.ts b/scripts/verify-built-package-invariants.spec.ts new file mode 100644 index 0000000000..4863e30c54 --- /dev/null +++ b/scripts/verify-built-package-invariants.spec.ts @@ -0,0 +1,88 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { spawnSync } from 'node:child_process' +import { afterEach, describe, expect, it } from 'vitest' + +const verifier = fileURLToPath(new URL('./verify-built-package-invariants.mjs', import.meta.url)) +const roots: string[] = [] + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +function fixture(options: { + invariantSource?: string + invariantExport?: string + runtimeChunk?: string +} = {}): { root: string; loaderUrl: string } { + const root = mkdtempSync(join(tmpdir(), 'dsh-built-package-invariants-')) + roots.push(root) + const packageDir = join(root, 'packages/core/probe') + mkdirSync(join(packageDir, 'lib'), { recursive: true }) + writeFileSync(join(packageDir, 'package.json'), `${JSON.stringify({ + name: '@deepseek-ai/dsh-probe', + type: 'module', + files: ['lib/invariant.js'], + exports: { + './invariant': { + default: options.invariantExport ?? './lib/invariant.js', + }, + }, + }, null, 2)}\n`) + writeFileSync( + join(packageDir, 'lib/invariant.js'), + options.invariantSource ?? "export const name = 'probe-invariant'\nexport const inject = ['invariants']\nexport const apply = () => {}\n", + ) + if (options.runtimeChunk !== undefined) { + writeFileSync(join(packageDir, 'lib/chunk.js'), options.runtimeChunk) + } + const loaderPath = join(root, 'loader.mjs') + writeFileSync(loaderPath, 'export default class Loader { unwrapExports(value) { return value } }\n') + return { root, loaderUrl: pathToFileURL(loaderPath).href } +} + +function verify(root: string, loaderUrl: string) { + return spawnSync(process.execPath, [ + verifier, + '--packages-root', root, + '--loader-url', loaderUrl, + ], { + encoding: 'utf8', + timeout: 5_000, + }) +} + +describe('built package invariant verifier', () => { + it('loads the staged compiled self-reference through plain Node and Loader normalization', () => { + const { root, loaderUrl } = fixture() + const result = verify(root, loaderUrl) + expect(result.status, result.stderr).toBe(0) + expect(result.stdout).toContain('1 compiled companion(s) passed plain-Node Loader checks') + }) + + it('rejects a default export and a broken invariant export map', () => { + const withDefault = fixture({ + invariantSource: "export default {}\nexport const name = 'probe-invariant'\nexport const inject = ['invariants']\nexport const apply = () => {}\n", + }) + const defaultResult = verify(withDefault.root, withDefault.loaderUrl) + expect(defaultResult.status).toBe(1) + expect(defaultResult.stderr).toContain('companion has a default export') + + const brokenExport = fixture({ invariantExport: './lib/missing.js' }) + const exportResult = verify(brokenExport.root, brokenExport.loaderUrl) + expect(exportResult.status).toBe(1) + expect(exportResult.stderr).toContain('@deepseek-ai/dsh-probe') + }) + + it('rejects an invariant bundle that needs an unstaged runtime chunk', () => { + const { root, loaderUrl } = fixture({ + invariantSource: "export * from './chunk.js'\n", + runtimeChunk: "export const name = 'probe-invariant'\nexport const inject = ['invariants']\nexport const apply = () => {}\n", + }) + const result = verify(root, loaderUrl) + expect(result.status).toBe(1) + expect(result.stderr).toContain('chunk.js') + }) +}) diff --git a/scripts/verify-mermaid.ts b/scripts/verify-mermaid.ts index d44ca9bcdf..df523d2a1a 100644 --- a/scripts/verify-mermaid.ts +++ b/scripts/verify-mermaid.ts @@ -79,7 +79,10 @@ Object.defineProperty(globalThis, 'window', { value: window }) Object.defineProperty(globalThis, 'document', { value: window.document }) Object.defineProperty(globalThis, 'navigator', { value: window.navigator }) const mermaid = (await import('mermaid')).default -mermaid.initialize({ startOnLoad: false }) +// maxEdges: mermaid's default 500-edge render guard; the module graph grows +// with every package edge and crossed it legitimately. Raise the guard here +// (a secure config settable only via initialize) rather than trimming edges. +mermaid.initialize({ startOnLoad: false, maxEdges: 1000 }) for (const block of blocks) { try { await mermaid.parse(block.source, { suppressErrors: false }) diff --git a/tsconfig.base.json b/tsconfig.base.json index a44f5fe7b3..c8af560652 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -58,6 +58,7 @@ "./packages/context/*/src/invariant.ts", "./packages/goal/*/src/invariant.ts", "./packages/guard/*/src/invariant.ts", + "./packages/plan/*/src/invariant.ts", "./packages/subagent/*/src/invariant.ts", "./packages/tasks/*/src/invariant.ts", "./packages/workflow/*/src/invariant.ts", @@ -120,6 +121,7 @@ "./packages/context/*/src", "./packages/goal/*/src", "./packages/guard/*/src", + "./packages/plan/*/src", "./packages/subagent/*/src", "./packages/tasks/*/src", "./packages/workflow/*/src", diff --git a/tsconfig.build.json b/tsconfig.build.json index 3336b1b796..2c1c30cfd0 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -102,6 +102,7 @@ { "path": "./packages/workflow/tool-workflow" }, { "path": "./packages/workflow/tool-ralph" }, { "path": "./packages/todo/tool-todo" }, + { "path": "./packages/plan/plan-mode" }, { "path": "./packages/guard/repeat-tool-guard" }, { "path": "./packages/cordis/tool-cordis" }, { "path": "./packages/hooks/hook-protocol" }, diff --git a/tsconfig.json b/tsconfig.json index b6e5bbaa17..430aafa029 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -122,6 +122,7 @@ { "path": "./packages/workflow/tool-workflow" }, { "path": "./packages/workflow/tool-ralph" }, { "path": "./packages/todo/tool-todo" }, + { "path": "./packages/plan/plan-mode" }, { "path": "./packages/guard/repeat-tool-guard" }, { "path": "./packages/cordis/tool-cordis" }, { "path": "./packages/hooks/hook-protocol" }, diff --git a/vitest.config.ts b/vitest.config.ts index 04f637a645..2f3f80220e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -22,6 +22,23 @@ const windowsCoverageExclusions = process.platform === 'win32' ] : [] +const testIncludes = [ + 'packages/*/*/tests/**/*.spec.{ts,tsx}', + 'examples/*/tests/**/*.spec.ts', + 'scripts/**/*.spec.ts', +] + +// These suites exercise process-global state, process APIs, or timing-sensitive process I/O +// that worker threads cannot isolate reliably under aggregate gate contention. +// Keep the narrow exception in forks while the rest of the inventory avoids per-file processes. +const processBoundTests = [ + 'packages/bash/bash-local/tests/run.spec.ts', + 'packages/context/time-context/tests/time-context.spec.ts', + 'packages/llm/llm-pi-ai/tests/adapter.spec.ts', + 'packages/ui/app-boot/tests/app-boot.spec.ts', + 'packages/workflow/workflow-workerthread/tests/session.spec.ts', +] + export default defineConfig({ // Native path resolution reads each package's nearest tsconfig, but only the root defines // workspace paths. Keep this plugin pinned to the root map so bare package imports resolve @@ -32,8 +49,35 @@ export default defineConfig({ test: { setupFiles: ['./scripts/test-invariants.ts'], // .tsx: client component specs (jsdom via per-file @vitest-environment pragma). - include: ['packages/*/*/tests/**/*.spec.{ts,tsx}', 'examples/*/tests/**/*.spec.ts', 'scripts/**/*.spec.ts'], + include: testIncludes, exclude: windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), + // One coverage invocation aggregates both projects. Most suites use threads + // for lower startup/IPC overhead; only explicit process-bound suites fork. + projects: [ + { + plugins: [tsconfigPaths({ projects: ['./tsconfig.vitest.json'] })], + test: { + name: 'thread-safe', + pool: 'threads', + setupFiles: ['./scripts/test-invariants.ts'], + include: testIncludes, + exclude: [ + ...windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), + ...processBoundTests, + ], + }, + }, + { + plugins: [tsconfigPaths({ projects: ['./tsconfig.vitest.json'] })], + test: { + name: 'process-bound', + pool: 'forks', + setupFiles: ['./scripts/test-invariants.ts'], + include: processBoundTests, + exclude: windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), + }, + }, + ], coverage: { provider: 'v8', // Coverage measures OUR runtime source. Types-only files carry no