diff --git a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md index 35d40ad0f4..1133b990c3 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md @@ -10,7 +10,7 @@ The harness needs one internal language for messages that the loop, session log, Own the vocabulary: messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`, `tool-result`), with the union derived from the merge-extensible `ContentBlockMap` so plugins add block types via declaration merging. The same merge-extensible-map pattern types every "stringly" field (`MessageSource`, `FinishReason`, `TurnTrigger`, `TurnEndReason`). Streaming is a raw chunk protocol; `BlockAssembler` is the single shared assembly implementation. Adapters translate to provider wire formats — mapping cost lives in adapters, where it belongs. -In-session context injection (`context/message`, `steering/message`) renders as tagged user-role envelopes (the system-reminder pattern) rather than a new role, so adapters carry zero burden. Live-adapter validation confirms this rendering for current DeepSeek behavior; a future provider-specific mismatch belongs in that adapter rather than a new canonical role. +In-session context injection (`context/message`) and mid-turn steering (`steering/message`) originally rendered as tagged user-role envelopes (the system-reminder pattern) rather than a new role, so adapters carry zero burden. Both now project as plain user content with no wrapper; see [the injected-content-envelope Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.md). Live-adapter validation confirms this rendering for current DeepSeek behavior; a future provider-specific mismatch belongs in that adapter rather than a new canonical role. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md index 125ef0946c..ef5bb5847a 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md +++ b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md @@ -12,7 +12,7 @@ Three seams: the queue-aware cancel, the `AgentHandle` disposer, and the bash ow ### 1. Queue-aware `Agent.cancel(reason?)` -A new `cancel()` verb on the `Agent` interface — the single public stop primitive. (It originally shipped alongside a narrower step-only `abort()`; that verb was later removed as unused, leaving `cancel()` the only public way to stop work.) It clears the inbox's queued + steering FIFOs, aborts the in-flight step if any, and drives a **turn-scoped cancellation marker** the driver loop checks at every turn-decision point — so a prompt that is queued-but-not-yet-started never runs, a cancel landing in the pre-step / continuation window drops the about-to-run turn (ending it `aborted`), and a later prompt cannot be batched into the cancelled turn. `whenIdle()` reaches post-cancel quiescence. ACP `session/cancel` maps to `cancel()`. The marker is armed ONLY when there is something to cancel, so an idle no-op cancel cannot strand the next prompt. +A new `cancel()` verb on the `Agent` interface — the single public stop primitive. (It originally shipped alongside a narrower step-only `abort()`; that verb was later removed as unused, leaving `cancel()` the only public way to stop work.) It clears the inbox's queued + steering FIFOs, aborts the in-flight step if any, and drives a **turn-scoped cancellation marker** the driver loop checks at every turn-decision point — so a prompt that is queued-but-not-yet-started never runs, a cancel landing in the pre-step / continuation window drops the about-to-run turn (ending it `aborted`), and a later accepted prompt remains an independent queued turn. `whenIdle()` reaches post-cancel quiescence. ACP `session/cancel` maps to `cancel()`. The marker is armed ONLY when there is something to cancel, so an idle no-op cancel cannot strand the next prompt. ### 2. `AgentHandle` async disposer @@ -29,7 +29,7 @@ Background-task ownership moved from a `tool-bash` plugin-local `Map end` numerically is normal and valid (the range is positional, validated by the surface's `indexOf` checks that remain). The turn-enclosure invariant is reused unchanged. -- **Wiring**: `examples/repl-agent/cordis.yml` loads zero-config `dsh-token-meter` before `dsh-compact-basic`; the service-wide window and compact defaults make the pair usable without repeated numeric policy. +- **`dsh-compact`** owns `toolPairingBalancedBefore(session, seq)` and `toolPairingBalancedAfter(session, seq)`, the cached surface-edge checks that `compactRegion` and `compactIfNeeded` use to avoid splitting a tool-call/result pair. The cache validates current membership by seq and answers both edges from one per-cut balance sequence; stale or missing seqs and orphan results reject. +- **`dsh-session`** validates positional replacement, complete provenance, and content-only single-node `tool/result` rewrites through its one surface manager. `dsh-invariants` treats fresh appended tool results as executions that require an open step and pending call; validated replacements remain turn-enclosed rewrites. +- **Wiring**: `examples/repl-agent/cordis.yml` loads zero-config `dsh-token-meter`, `dsh-compact-tool-result-prune`, then `dsh-compact-basic`; service-wide defaults make the composition usable without repeated numeric policy. ## Testing -- **Unit:** Real Loader and invariant plugins cover whole-unit retention, convergence failure, both `compact/end` outcomes, head anchoring, open-tail refusal, inert crash orphans, forced below-threshold overflow, generation proof, caps, and original-error preservation. +- **Unit:** Real Loader and invariant plugins cover whole-unit retention, pruning configuration and replay, rich-block ordering, metadata preservation, convergence, both `compact/end` outcomes, open-tail refusal, pruning-only and summarized overflow recovery, generation proof, caps, and original-error preservation. - **Loop:** Tests pin post-step after durable tool results and before `step/end`, actual `agent/request` routing, closed failed steps, fresh retry numbering, and complete thrown/in-band overflow → compaction → reconstructed retry composition. - **With-key e2e:** A real model and bash session with lowered limits triggers compaction, records a complete `compact/start…end` pair, shrinks the surface, and finishes the task. - **Snapshot gap:** Runaway-turn compaction cannot yet replay because the summarization call records no `assistant/chunk` events or `sessionId`; interleaved summarization-call replay remains follow-up work. diff --git a/.agents/notes/implemented/feature/2026-06-24-workspace-context.md b/.agents/notes/implemented/feature/2026-06-24-workspace-context.md index dc21fb919d..a3eec63b8f 100644 --- a/.agents/notes/implemented/feature/2026-06-24-workspace-context.md +++ b/.agents/notes/implemented/feature/2026-06-24-workspace-context.md @@ -40,7 +40,7 @@ After a successful first-party `read`, `write`, or `edit` call, the `tools/post- A content edit appends `Updated instructions from: `, states that the new content replaces the previous content, and includes the complete current file. If precedence changes from one candidate to another, the message also names the previous path and says it no longer applies. If no candidate remains, the plugin appends `Instructions removed: ` and states that the previously loaded instructions no longer apply. -Dynamic messages use a raw `context/message` envelope because the plugin owns the complete system-reminder framing. Core context injection therefore supports `envelope: 'raw'`; callers that omit it retain the canonical `` wrapper. `context/message.meta` carries opaque JSON state that is persisted but never rendered to the model. +Dynamic messages carry their complete system-reminder framing in `content`, and every `context/message` reaches the model verbatim as a user-role message (there is no core wrapper to opt out of). `context/message.meta` carries opaque JSON state that is persisted but never rendered to the model. Shell commands are not discovery triggers. Local bash calls start fresh shells, and inferring reached paths from arbitrary command strings would require shell semantics the prompt plugin does not own. @@ -76,7 +76,7 @@ There is intentionally no watcher. Detection occurs at the next successful struc ## Consequences -Workspace guidance is isolated per session and shared by both product front doors and every tool presentation mode. Initial instructions benefit from stable prefix caching, while nested and changed content remains durable and replayable. The generic session/agent context contract includes optional raw framing and JSON metadata, both propagated through prompt-submit and post-tool `additionalContexts` arrays without flattening entries. +Workspace guidance is isolated per session and shared by both product front doors and every tool presentation mode. Initial instructions benefit from stable prefix caching, while nested and changed content remains durable and replayable. The generic session/agent context contract carries JSON metadata propagated through prompt-submit and post-tool `additionalContexts` arrays without flattening entries. Repository text remains untrusted input. Lower-authority user-role framing, explicit precedence language, delimiter escaping, and symlink rejection reduce risk but do not eliminate prompt injection. Permission and sandbox layers treat workspace files as data rather than authority. diff --git a/.agents/notes/implemented/feature/2026-06-30-interception-seams.md b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md index c0a25081af..2535c1c564 100644 --- a/.agents/notes/implemented/feature/2026-06-30-interception-seams.md +++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md @@ -14,9 +14,9 @@ The canonical surface separates transformable policy, around-dispatch control, a **Agent events** (`dsh-agent`): - `agent/session-start(agent, source)` — emit, once before turn 1, carrying a `SessionStartSource` (`startup` for a fresh/forked create, `resume` for a reloaded persisted session; `clear`/`compact` reserved). A pure notification — it CANNOT block startup (a deliberate gap: a bridge logs/injects, it does not gate startup). A listener seeds context via `agent.inject()`. -- `agent/prompt-submit(agent, content, source, next) → PromptDecision` — waterfall, fired per drained queued message inside the open turn, before the `user/message` append. `allow` (optionally rewriting the prompt `content` or attaching separately sourced `additionalContexts[]`) or `block` (dropping the prompt; the loop appends a durable `prompt/blocked` in its place — see the dispatch note below). +- `agent/prompt-submit(agent, content, source, next) → PromptDecision` — waterfall, fired for the turn's single claimed queued message before the `user/message` append. `allow` optionally rewrites the prompt `content` or attaches separately sourced `additionalContexts[]`; `block` appends a durable `prompt/blocked` and rejects that zero-step turn. -**`agent/turn-continuation`** receives and returns a `ContinuationDecision`. A `{action:'continue', reason?}` may carry model-facing content and source recorded as next-step steering in the same turn — the typed twin of the `/goal` step-end-steer pattern. It is not a `context/message`, so its type does not offer a context envelope or durable context metadata. +**`agent/turn-continuation`** receives and returns a `ContinuationDecision`. A `{action:'continue', reason?}` may carry model-facing content and source recorded as next-step steering in the same turn — the typed twin of the `/goal` step-end-steer pattern. It is not a `context/message`, so its type does not offer durable context metadata. ### The tool pipeline gives each phase one kind of authority @@ -30,11 +30,11 @@ Every call follows `tools/pre-execute` → guards → `tools/execute` → dispat Core dispatch and the tool body sit inside normalization boundaries, so tool, listener, malformed-result, non-JSON result, and identity-shape failures resolve as JSON-safe `isError` results rather than escaping the turn. A post-execute listener can therefore inspect a thrown tool, and a final observer sees exactly what the caller receives and the session log can persist. -**`TurnEndReason.rejected`** (`dsh-session`): a turn whose entire prompt batch was blocked by `prompt-submit`. +**`TurnEndReason.rejected`** (`dsh-session`): a zero-step turn whose claimed prompt was blocked by `prompt-submit`. ### Three load-bearing loop decisions -1. **Open the turn before prompt policy.** A fully blocked batch becomes a zero-step `rejected` turn, preserving enclosure and giving ACP a durable terminal event. Every veto also records `prompt/blocked` with the original prompt and reason, so mixed batches retain blocked inputs. Every allowed `additionalContexts` entry is injected into the open turn. +1. **Open the turn before prompt policy.** A blocked prompt becomes a zero-step `rejected` turn, preserving enclosure and giving ACP a durable terminal event. The veto records `prompt/blocked` with the original prompt and reason, while every allowed `additionalContexts` entry is injected into the open turn. Each claimed ordinary-send item is the sole message in its turn under the [one-send-one-turn simplification](../simplification/2026-07-17-one-send-one-turn.md); a pre-start drop creates no turn. 2. **Post-tool `additionalContexts` and asynchronous injections enter the active-batch FIFO and append when that batch settles.** `content`/`feedback` shape the result `execute()` returns, but each context is a separate `context/message`, and a single step or composite tool can produce many. Appending context immediately would interleave `result(c1) → context → result(c2)` or place nested context before its outer result, breaking tool-call/result adjacency. `ToolRunContext.deferContext()` therefore collects nested-dispatch context through failures, `execute()` surfaces the ordered array on `ToolExecutionResult`, and the loop accepts it into the same FIFO as `agent.inject()` calls made during execution. The FIFO appends after every recorded result when the batch settles, including before an interrupted turn closes. An accepted outer call preserves deferred contexts before decision contexts; an outer block discards deferred contexts and exposes only contexts explicitly supplied by the blocking decision. diff --git a/.agents/notes/implemented/feature/2026-07-06-sandbox.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.md index 0f87885952..2cc6516523 100644 --- a/.agents/notes/implemented/feature/2026-07-06-sandbox.md +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.md @@ -96,7 +96,7 @@ The default is composition config (`cordis.yml`) — operator-owned, process-wid ```ts interface SessionEventMap { - 'bash/sandbox-mode': { mode: 'read-only' | 'workspace-write' | 'danger-full-access' } + 'sandbox/mode': { mode: 'read-only' | 'workspace-write' | 'danger-full-access' } 'approval/policy': { policy: 'ask' | 'never' } } ``` @@ -111,9 +111,7 @@ Sandbox mode is not narrated in the prompt; denial results report the mode when #### In-process tools -fs/web/todo execute in-process, so their sandbox semantics are policy at their seams: the fs intent gates deciding by the shared mode vocabulary (§ Deferred phases, cross-family) make `read-only` a real boundary instead of a bash-only approximation — until then the contract says so honestly. No generic per-tool sandbox runtime: a host-mediated tool leaves the process only by returning declarative effects the host validates, which is a rewrite, not a wrapper. - -FIXME: Revisit this tool-local boundary. The follow-up design needs to determine whether sandboxing becomes a global harness capability that applies uniformly to every tool, instead of expressing in-process enforcement independently at each tool seam. +fs/web/todo execute in-process, so their sandbox semantics are policy at their seams. The fs seam now enforces the shared mode vocabulary through a sandboxed provider (`dsh-fs-sandbox` fences write/edit by mode; see [the cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)), so `read-only`/`workspace-write` are real boundaries for the filesystem tools, not a bash-only approximation. web/todo remain unfenced (web's only effect is network, outside the file-effect mode vocabulary). No generic per-tool sandbox runtime: a host-mediated tool leaves the process only by returning declarative effects the host validates, which is a rewrite, not a wrapper — the follow-up settled on one shared policy home (`ctx.sandboxPolicy`) with per-seam enforcement, not a uniform wrapper. ### Testing @@ -126,8 +124,7 @@ FIXME: Revisit this tool-local boundary. The follow-up design needs to determine Each phase gets its full design when picked up, validated against the code at that time, and lands with unit, real-API e2e, and snapshot coverage at the tiers it touches. -- **Per-session workspace root** — the executor's write boundary stays config-fixed for its lifetime while each ACP session has its own cwd; a per-session root rides the same per-call policy carrier once designed. -- **Cross-family boundary** — the fs intent gates decide by the shared mode, making `read-only`/`workspace-write` real boundaries beyond bash. +- **Per-session workspace root** — the executor's write boundary stays config-fixed for its lifetime while each ACP session has its own cwd; a per-session root rides the same per-call policy carrier once designed. Centralizing the root on `ctx.sandboxPolicy` (the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)) is the groundwork. - **Second consumer** — `subagent-acp` optionally confines child agents (per-call policy; unconfined default — a child agent must write its own persistence). - **More environments** — an environment-coherent capability group example (e.g. bash+fs against one container). - **Windows chain** — `PLATFORM_CHAINS.win32` is reserved and empty (fail-closed); filling it means a confinement runner from the AppContainer/restricted-token family, shipped from its own repository on the `node-addon-landlock-run` template, plus its profile dialect and denial/runner-failure signatures. @@ -171,7 +168,7 @@ What shipped pins — the tiers in Testing hold each: Costs and accepted limits: - **The one-wrapper illusion is given up knowingly.** A `tools/pre-execute` wrapper plus prompt conventions does not solve sandbox approval — the correct design costs structured denials, native runner probes, per-call policy carriage, and consistent cross-family enforcement, and this design pays it. -- **`read-only` is not yet a cross-family boundary.** Until the fs intent gates decide by the shared mode, the claim holds for bash only; the contract says so honestly (§ In-process tools). +- **`read-only` became a cross-family boundary through a follow-up.** This RFC shipped bash-only enforcement; the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md) extends the same mode vocabulary to the filesystem tools through a sandboxed `ctx.fs` provider and relocates the mode/root config and the `sandbox/mode` override to `ctx.sandboxPolicy` (§ In-process tools). - **Windows has no backend.** Its chain slot is reserved empty — fail-closed, never a fallthrough; filling it is a deferred phase. - **The Seatbelt rung leans on Apple's deprecated-but-shipped `sandbox-exec` CLI.** As darwin's sole candidate it is selected without probing, so a future removal surfaces at execution as the runner-failure classification — re-thrown `SANDBOX_UNAVAILABLE`, the command never runs; fail closed, never open. - **Landlock confinement is only as complete as the running kernel's ABI.** Reported as `enforcement: 'partial'` rather than refused — the deliberate trade that keeps the fallback available on older-kernel hosts. @@ -191,9 +188,9 @@ Costs and accepted limits: - **What happens on a platform with no backend — Windows today?** `confine()` throws the fail-closed `SANDBOX_UNAVAILABLE` and the command never spawns; `win32` is a reserved EMPTY chain, pinned by test to fail closed identically until a Windows runner fills it (§ Deferred phases). - **`bwrap` is installed on my host but unusable (disabled unprivileged userns, an LSM denying `mount`) — what happens?** The chain probe is functional — it builds and enforces a real profile rather than checking `--version` — so a present-but-unusable `bwrap` fails its probe, selection falls to the registry-installed Landlock launcher, and the verdict is cached for the provider's lifetime. - **Does the sandbox restrict network or process visibility?** No — `SandboxMode` claims FILE effects only; the bwrap profile deliberately does not unshare pid, and no backend claims network. Whether network restriction becomes its own knob is left open in § The seam. -- **Which tools actually run confined?** OS subprocesses through `ctx.bash` — the bash tools, and hook commands transitively. fs/web/todo execute in-process, where an `execve` wrapper is mechanically meaningless; their `read-only` semantics arrive with the cross-family deferred phase, and until then the contract says bash-only honestly. +- **Which tools actually run confined?** OS subprocesses through `ctx.bash` — the bash tools, and hook commands transitively — plus the filesystem tools (`read`/`write`/`edit`) through the sandboxed `ctx.fs` provider (the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)): bash confines via the OS runner, fs via an in-process path fence, both keying off the same `ctx.sandboxPolicy` mode. web/todo stay in-process and unfenced (web's only effect is network, outside the file-effect mode vocabulary). - **Does a granted escalation persist?** No. The grant is consumed by the exact foreground or background call that asked; every neighboring call keeps its own effective mode. A later background denial surfaces through `task_output` and may ground a new exact-command retry. -- **When does an editor's mode switch take effect?** Mid-turn: appended immediately, honored by the very next call's stamp. Idle: held on the bridge's session record, anchored at the next turn's `agent/prompt-submit`, with N flips coalescing to at most one event (none if net-zero); a crash before anchoring reverts it and `session/load` reports the truth. The model is not told — its next command simply behaves under the new mode. +- **When does an editor's mode switch take effect?** Mid-turn: appended immediately, honored by the very next call's stamp. Idle: held on the bridge's session record, anchored at the next `agent/prompt-submit` inside its open turn, with N flips coalescing to at most one event (none if net-zero); a crash before anchoring reverts it and `session/load` reports the truth. The model is not told — its next command simply behaves under the new mode. - **What survives a restart — and what if the operator changed the config default while the process was down?** Overrides replay from the session log (`effective = fold ?? config`), so a resumed session keeps its modes with zero catch-up machinery; a default that drifted offline changes behavior the same way a switch does (the approval policy, being stated, is additionally narrated with operator/config attribution). - **What does `enforcement: 'partial'` on a result mean?** The selected backend enforces the subset its kernel ABI governs — e.g. Landlock before ABI v3 does not govern path truncate — and says so structurally instead of refusing the host; the probe's report line distinguishes the cases. The bwrap and Seatbelt profiles govern every promised file effect by construction, so they always report `full`. diff --git a/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md b/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md index 29f77364fa..0f42a547cf 100644 --- a/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md +++ b/.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md @@ -49,9 +49,11 @@ The global registry remains live. A deny-only filter admits a later global name The depth limit bounds recursive delegation independently of tool visibility. A top-level agent has depth zero; an in-process child has its parent's validated depth plus one. `maxDepth` is an absolute non-negative safe integer, and a start rejects before child ownership begins when the derived child depth is greater than the cap. -Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. Omitting the cap leaves depth unbounded by this mechanism. +The effective parent depth is the greater of durable `SessionHeader.delegationDepth` and runtime `AgentOptions.subagentDepth`. An in-process child records its derived depth in the session header, and resume restores that header, so a restart cannot lower the recursion count. -A deployment can combine depth and filtering. For example, it may keep the delegation tool visible at depth one but set `maxDepth: 1`, or deny the delegation tool entirely in children. Neither choice changes the provider's conversation-history behavior. +Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. A direct `SubagentStartRequest` may omit the cap to leave depth unbounded; loader-resolved `dsh-tool-subagent` configuration instead defaults to `3`, accepts a numeric override, and uses explicit `'provider-managed'` to omit the cap for an out-of-process provider whose deployment owns its recursion budget. Three is a small finite default that still permits a root plus three descendant generations: the [SDK helper's generated subagent entries](../../../../packages/sdk/helper/src/features/builtin/index.ts) and [JSON-RPC example](../../../../examples/jsonrpc-agent/cordis.yml) use that general policy, while the shipped interactive ACP, headless, and REPL examples pin one. A numeric tool cap fails at provider mount when the provider lacks `depthLimit`. + +A deployment can combine depth and filtering, but the numeric cap does not synthesize a filter. The delegation tool stays visible at the cap because authorization may depend on runtime state; every attempted start checks the calling agent's current durable and runtime depth, and a rejected start returns an errored tool result without publishing a child. A deployment may separately deny delegation tools in children when its visibility policy is static. Neither choice changes the provider's conversation-history behavior. ### Capability gating keeps providers honest @@ -83,10 +85,10 @@ A security design would need a separate authority representation, propagation ru **Hide only tool schemas.** Presentation-only filtering lets the model execute a tool that the prompt says does not exist through Code Mode or a forged call. One resolver governs both presentation and execution instead. -**Use only tool filtering to stop recursion.** Removing the delegation tool is useful but provider-specific and does not protect direct service callers or alternate delegation tools. Absolute depth is an independent structural bound. +**Encode the depth cap as an automatic tool filter.** A creation-time filter snapshots a decision that may depend on runtime state, affects only one configured tool name, and does not protect direct service callers or alternate delegation tools. The provider instead enforces the absolute cap at every start. ## Consequences Contributors can configure child role, visible global tools, and recursion without defining new providers. Capability checks fail before ownership starts, unpublished setup makes the first request consistent, and one tool resolver prevents presentation/execution drift. -The cost is that deployments must understand live allow/deny behavior and the distinction between visibility and authority. Provider authors must advertise each supported control accurately, and in-process providers must install every requested contribution before publication. The controls deliberately do not solve security confinement or parent-to-child non-escalation. +The cost is that deployments must understand live allow/deny behavior and the distinction between visibility and authority. A model may call a visible delegation tool after the current depth policy forbids another child and receive an error. Provider authors must advertise each supported control accurately, and in-process providers must install every requested contribution before publication. The controls deliberately do not solve security confinement or parent-to-child non-escalation. diff --git a/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.i18n.yaml b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.i18n.yaml new file mode 100644 index 0000000000..8e44e3a6bc --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.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-14-cross-family-fs-sandbox.md: 9b6312e5994469606bd1645902fc798f70258580 +2026-07-14-cross-family-fs-sandbox.zh.md: d4816e03d94bdf12b2db875d71dccb7db3a2c0d7 diff --git a/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md new file mode 100644 index 0000000000..9b6312e599 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md @@ -0,0 +1,94 @@ +# Agent Note: Cross-family file sandbox — one policy home, a sandboxed fs provider, and fs escalation parity + +Status: implemented + +English | [中文](2026-07-14-cross-family-fs-sandbox.zh.md) + +## Problem + +`SandboxMode` claims file effects, but originally only `ctx.bash` enforced it. The fs tools (`write`/`edit`) mutate the host filesystem in-process through `ctx.fs`, where an OS argv wrapper is mechanically meaningless — [the sandbox Agent Note](2026-07-06-sandbox.md) § In-process tools records this and left cross-family enforcement as a deferred phase with an open question: whether in-process enforcement stays per-seam or becomes a uniform harness capability. This Agent Note is that phase, and answers it: one shared policy home, per-seam enforcement at each family's correct altitude. + +The gap was not read-only-shaped. A confined coding agent's product mode is `workspace-write`: bash may already write under the workspace root while everything outside is denied, so an fs enforcement that could only deny-all would be strictly worse than disabling the fs tools — the model would attempt an in-workspace `write`, be denied, and learn to detour through `bash` heredocs. Cross-family enforcement therefore speaks the full mode ladder, including the path-containment judgment `workspace-write` requires (canonical targets; `..`/symlink/absolute-path escapes) and the same escalation lever bash carries. + +A second enforcing family also exposed an ownership problem in the original layout. The deployment default (`mode` + `workspaceRoot`) was configured on `dsh-bash-sandbox`, and the per-session override event was `bash/sandbox-mode`, folded and written by `dsh-bash`'s session-mode kit. With fs enforcing the same policy, either fs reads bash's config and events (a capability family depending on a sibling's plugin config) or each family carries its own copy — and two copies of `workspaceRoot` drift into exactly the split world the sandbox RFC warns about: bash confined to one root while fs fences another. + +## Decision + +Three coordinated pieces, all composed from the leaf `cordis.yml`, none touching `agent-loop`. + +### `ctx.sandboxPolicy` — one home for mode and workspace root + +`packages/sandbox/sandbox-policy/` (`@deepseek-ai/dsh-sandbox-policy`) registers `ctx.sandboxPolicy`, the single owner of the deployment's sandbox policy: + +- `Config`: `mode` (the closed `SandboxMode` union, default `read-only`) and `workspaceRoot` (default the process cwd, resolved absolute). Misconfiguration fails loud at load. +- The per-session override event `sandbox/mode`, with its pure fold (`effectiveSandboxMode(events)`), its write path (`setSandboxMode(session, mode)`), and `SANDBOX_MODES`. The event is policy state — consumed by two families — so it lives here, not in either capability's seam. Its shape and log-only semantics match the `approval/*` precedent. +- `defaultMode` / `workspaceRoot` accessors the enforcing implementations read for their resolve fallback and boundary. + +`dsh-bash-sandbox` carries no sandbox config of its own — it injects `sandboxPolicy` and reads the default from it; its `resolve()` precedence is unchanged (escalation grant > per-call stamp > default). `dsh-tool-bash` and `dsh-tool-fs` fold the session's `sandbox/mode` with `effectiveSandboxMode` to stamp each call; `dsh-permission` presets and the ACP bridge write through the relocated setter. The seam that owns bash execution no longer depends on `dsh-session` at all — the session dependency moved to the policy package with the fold. + +### `dsh-fs-sandbox` — enforcement inside the provider + +`packages/fs/fs-sandbox/` (`@deepseek-ai/dsh-fs-sandbox`) mirrors the `bash-local`/`bash-sandbox` split: `SandboxedFileSystem extends LocalFileSystem`, registered as `ctx.fs`, injecting `sandboxPolicy`. Reads (`resolve`/`stat`/`readText`/`streamText`/`listDir`) pass through untouched — every mode permits reading. The two mutations enforce by mode before delegating to the inherited atomic write: + +- `read-only` denies `writeText`/`editText` outright. +- `workspace-write` fences the canonicalized target against the writable-root set — `writableRoots(policy)` in `dsh-sandbox`: the workspace root plus the platform temp areas (`/tmp`, `os.tmpdir()`), each realpathed — the SAME set the Seatbelt profile grants, so the fs fence is the fourth dialect of one mode meaning alongside the bwrap/Landlock/Seatbelt profiles, and "the write tool cannot write `/tmp` but bash can" asymmetries cannot arise. Containment is prefix-inclusion on real paths; the target is re-canonicalized (`resolve` realpaths the deepest existing ancestor) immediately before delegating, so an ancestor symlink swapped since the tool resolved it is caught. +- `danger-full-access` delegates unfenced. + +A denial is the structured `FS_SANDBOX_DENIED` carrying the effective mode — distinct from `FS_PERMISSION_DENIED` (a host EACCES is the world refusing; this is policy refusing). No text inference: an in-process fence knows exactly what it denied. The per-call carrier is a trailing optional `sandboxMode` on `writeText`/`editText` (the filesystem twin of `BashExecRequest.sandboxMode`); the seam stays session-free (the caller stamps, exactly as `resolve` takes a cwd), and the bare local backend carries-and-ignores it. `FileSystem.sandboxMode` is the capability fact (`undefined` on the base and `fs-local`, the default on `SandboxedFileSystem`), so the tool layer advertises escalation from composition truth. + +The threat model is stated in the package README: a policy fence in trusted code over model-controlled paths, not a kernel boundary — the operations are the seam's own, only the target path is untrusted, so canonicalize-then-contain is the complete answer to this surface (the `code-runtime` "containment, not a security boundary" precedent). Kernel-grade isolation of untrusted CODE stays `ctx.bash`'s job. The residual resolve-to-syscall race is narrowed by the in-place re-canonicalization and eliminated only by platform primitives (`openat2` `RESOLVE_BENEATH`) not worth their portability cost here. + +### Tool parity — one denial marker, one escalation flow + +`dsh-tool-fs` stamps the effective mode onto each mutation and maps `FS_SANDBOX_DENIED` to the marker the model already knows from bash: `[sandbox: file access denied under mode]`. When `ctx.fs.sandboxMode` reports a confining mode at registration, `write` and `edit` advertise the same `sandbox_permissions` + `justification` fields, teach the same same-turn retry, and resolve the same `ctx.approval` request before executing — the four outcomes and their verbatim fail-closed texts carried over from [the sandbox Agent Note](2026-07-06-sandbox.md) § Escalation (strict widening checked at execution against the call's effective mode; a grant consumed by the one call that asked; no new session events). + +The shared pieces live in `dsh-sandbox`, which owns the mode types: `WIDER_MODES`, the escalation-target enum, the argument-pairing validation, the denial/hint marker builders, and `approveEscalation` — the ordered fail-closed choreography. `approveEscalation` takes a minimal STRUCTURAL approver (`EscalationApprover`, generic over the agent and call-id types), not the approval service type, so `dsh-sandbox` gains no dependency on the approval or agent packages: each tool passes its own `ctx.approval`, agent, call id, and tool name as ingredients. `dsh-tool-bash` and `dsh-tool-fs` both use these; the cross-file duplication gate holds the single-sourcing honest. + +The [`examples/acp-agent`](../../../../examples/acp-agent/cordis.yml) composition loads `dsh-sandbox-policy` and `dsh-fs-sandbox`, moves the `mode`/`workspaceRoot` config to the policy entry, and drops the old gating that disabled the fs stack under confined modes; `fs-policy` (read-before-edit) composes orthogonally on top. The system prompt still states no sandbox mode — the marker teaches the boundary at the moment it matters, per the sandbox Agent Note's live evidence. + +### The enforcement point: provider, not intent gate + +The sandbox Agent Note's original cross-family sketch put fs enforcement on the `fs/write-intent`/`fs/edit-intent` events. This Agent Note enforces in the provider instead, on two mechanical facts: the intent slots are single-decision first-wins (occupied by `dsh-fs-policy`, whose contract names a second decider a misconfiguration), and the intent events are dispatched only by `dsh-tool-fs` — a direct `ctx.fs` caller (a cordis-mounted plugin, a custom tool) bypasses them, where provider-level enforcement covers every caller by construction. The sandbox Agent Note's deferred-phase wording is updated to match in the same change. + +### Out of scope + +- **Network policy for `ctx.web`** — `SandboxMode` claims file effects only; a web-only network knob while bash `curl` runs free would be a false boundary. Revisit when a bash backend enforces network (bwrap `--unshare-net`, Landlock ABI v4+). +- **The `subagent-acp` consumer** and **per-session workspace root** — unchanged deferred phases of the sandbox RFC; centralizing the root in `ctx.sandboxPolicy` is groundwork for the latter, not its design. +- **A uniform per-tool sandbox runtime** — remains rejected for the reasons in the sandbox RFC. + +## Alternatives considered + +- **Enforce on the `fs/*` intent events (the sandbox Agent Note's original sketch)** — rejected on the two mechanical facts in § The enforcement point: single-slot first-wins already occupied, and a bypass for direct `ctx.fs` callers. Provider-level enforcement covers every caller and mirrors bash's swap-the-implementation shape. +- **Enforce in `tools/pre-execute`** — rejected: the listener sees the model's raw path string before `resolve()`, so it would re-implement cwd defaulting and symlink canonicalization and still race the real resolve. Disqualifying for `workspace-write`, a judgment over canonical paths. +- **Inline checks in `dsh-tool-fs`** — rejected: covers only the tool path (same bypass as the intent events) and duplicates resolve knowledge one layer above where the canonical target already exists. +- **A `mode` flag on `dsh-fs-local` instead of a sibling backend** — rejected: the capability fact must be composition truth the way `dsh-bash-local` vs `dsh-bash-sandbox` is; a config flag makes the tool's advertisement conditional on configuration, and the bash family already establishes the sibling-package shape. +- **Kernel-enforced fs mutations via a confined helper subprocess** — rejected: a process per write; `editText`'s read-match-write critical section would have to move wholesale into the child to stay atomic; and the threat surface (trusted operations, untrusted path argument) does not need a kernel — the fence in trusted code is the complete answer, while untrusted-code isolation stays on `ctx.bash`. +- **Per-family policy config with a load-time consistency check** — rejected: two homes for one fact, patched by a check that must enumerate every future enforcing family; the policy service makes drift inexpressible instead of detected. +- **Keep the override event in `dsh-bash` as `bash/sandbox-mode`** — rejected: the event is policy state consumed by two families; leaving it bash-named forces `dsh-fs-sandbox` to depend on bash vocabulary. Pre-release, the rename is a same-change move with snapshot re-records, no shims. +- **Escalation choreography imported from the approval/agent packages into `dsh-sandbox`** — rejected: it would invert the layering (a base vocabulary package depending on UI/agent packages). The structural approver keeps the logic single-sourced in `dsh-sandbox` while the dependencies stay in the tool layer that already holds them. +- **A consolidated mutation-options object on the fs seam** (the shape first sketched for the per-call carrier) — rejected on friction: it churns every `writeText`/`editText` caller and splits `signal` across an options bag for mutations while reads keep it positional. A trailing optional `sandboxMode` matches bash's carry-and-ignore pattern and keeps `signal` symmetric across the seam. +- **Extra writable-root grants on `SandboxPolicy` now** — deferred unchanged: `writableRoots()` derives from the mode meaning today; ad-hoc grants are an escalation-scope question the sandbox RFC left open. + +## Consequences + +What shipped — the tiers in § Testing hold each: + +- Under `read-only`, `write`/`edit` return the `[sandbox: file access denied under read-only mode]` marker and the disk is untouched; `read`/`listDir` behave identically to `dsh-fs-local`. +- Under `workspace-write`, mutations land under the workspace root and the temp areas and are denied outside; the containment matrix — `..` traversal, absolute paths outside, a pre-existing symlinked directory inside pointing out, and a new file created under such a symlink — denies every escape on real disks. +- A denied fs mutation retried once with `sandbox_permissions` + `justification` prompts through the composed approval chain; a grant runs exactly that call under the wider mode and the write lands; rejected/cancelled/unavailable each produce their verbatim fail-closed text and mutate nothing. +- One `permission` preset switch governs both families: after a session switches modes, the next bash call and the next fs mutation both honor the new mode from the same `sandbox/mode` fold. +- A direct `ctx.fs.writeText` with no per-call stamp is confined at the deployment default. +- The escalation fields on `write`/`edit` exist exactly when the mounted `ctx.fs` confines, absent under `dsh-fs-local`. +- `agent-loop` is untouched — everything rides `ctx.sandboxPolicy`, the `ctx.fs` seam, `SessionEventMap` merging, and the tool-execution pipeline. + +Costs and accepted limits: + +- **The fs fence is a policy boundary, not a kernel one.** Its threat surface is model-chosen paths, not adversarial host processes; the residual resolve-to-syscall TOCTOU is narrowed, not eliminated, and the README says so. Kernel boundaries remain bash's. +- **`dsh-bash-sandbox` gains a hard dependency on `ctx.sandboxPolicy`.** Every sandboxed composition adds one `cordis.yml` entry or fails loud at load — the intended pre-release foundation move; the examples update in the same change. +- **Fence-vs-runner parity is derived, not asserted.** The fs fence and the Seatbelt profile both take their writable set from `writableRoots`, and a parity unit test pins the sets; a runner profile changing its writable set without that function would drift. +- **The marker and escalation teaching now serve two families.** A wording change is a coordinated edit behind one builder in `dsh-sandbox`; the duplication gate and pinned snapshots hold it single-sourced, at the cost that fs and bash cannot deliberately diverge in phrasing without splitting the builder. + +## Testing + +- Unit: `dsh-sandbox` pins the escalation ladder, the marker builders, the argument-pairing validation, and `approveEscalation`'s ordered fail-closed sequence (non-widening, no-approval, no-agent, each outcome), plus `writableRoots`/`canonicalPath`. `dsh-sandbox-policy` pins the default accessors, the fold/setter, the load-time mode rejection, and HMR safety. `dsh-fs-sandbox` pins the per-mode fence and the containment matrix (inside, temp area, absolute-outside, `..`, symlinked-out directory, new file under one, path-equals-root, root-ending-in-separator) on a real filesystem, plus the per-call override and HMR safety. `dsh-tool-fs` pins advertisement gating, the mode stamp, the fold, denial-marker mapping, and the full escalation matrix (grant, reject, no-service, no-agent, pairing, non-confining guard). `dsh-tool-bash`, `dsh-bash-sandbox`, and `dsh-permission` migrate to the relocated policy/kit. +- Snapshot: the acp-agent example composes `dsh-sandbox-policy` + `dsh-fs-sandbox`; the pinned header carries the fs escalation fields and the `sandbox/mode` event name, re-recorded once. diff --git a/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md new file mode 100644 index 0000000000..d4816e03d9 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md @@ -0,0 +1,94 @@ +# Agent Note: 跨家族文件沙箱——统一策略归属、沙箱化 fs 提供方、fs 升级对等 + +Status: implemented + +[English](2026-07-14-cross-family-fs-sandbox.md) | 中文 + +## 问题 + +`SandboxMode` 声明的是文件效果,但最初只有 `ctx.bash` 执行它。fs 工具(`write`/`edit`)在进程内经由 `ctx.fs` 变更宿主文件系统,那里的 OS argv 包装在机制上毫无意义——[沙箱 RFC](2026-07-06-sandbox.md) § In-process tools 记录了这一点,并把跨家族执行留作一个延后阶段,附带一个未决问题:进程内执行是各 seam 各自表达,还是变成一个统一的 harness 能力。本 Agent Note 就是那个阶段,并给出答案:一个共享的策略归属,在每个家族各自正确的高度上做 per-seam 执行。 + +这个缺口不是 read-only 形状的。一个受限编码 agent 的产品模式是 `workspace-write`:bash 已经可以在工作区根目录下写入,而其外的一切都被拒绝,所以一个只能全部拒绝的 fs 执行会严格劣于禁用 fs 工具——模型会尝试在工作区内 `write`,被拒,然后学会绕道 `bash` heredoc。因此跨家族执行必须讲完整的模式阶梯,包括 `workspace-write` 要求的路径包含判定(规范化目标;`..`/符号链接/绝对路径逃逸),以及与 bash 相同的升级杠杆。 + +第二个执行家族还暴露了原布局中的一个归属问题。部署默认值(`mode` + `workspaceRoot`)配置在 `dsh-bash-sandbox` 上,而 per-session 覆盖事件是 `bash/sandbox-mode`,由 `dsh-bash` 的 session-mode 工具集折叠与写入。当 fs 执行同一套策略时,要么 fs 读取 bash 的配置与事件(一个能力家族依赖同级插件的配置),要么各家族各持一份副本——两份 `workspaceRoot` 会漂移进沙箱 RFC 警告过的那个割裂世界:bash 受限于一个根,而 fs 围栏另一个根。 + +## Decision + +三个相互协调的部分,全部在叶子 `cordis.yml` 中组合,均不触及 `agent-loop`。 + +### `ctx.sandboxPolicy`——mode 与工作区根的统一归属 + +`packages/sandbox/sandbox-policy/`(`@deepseek-ai/dsh-sandbox-policy`)注册 `ctx.sandboxPolicy`,即部署沙箱策略的唯一所有者: + +- `Config`:`mode`(封闭的 `SandboxMode` 联合,默认 `read-only`)与 `workspaceRoot`(默认进程 cwd,解析为绝对路径)。配置错误在加载时高声失败。 +- per-session 覆盖事件 `sandbox/mode`,连同它的纯折叠(`effectiveSandboxMode(events)`)、写入路径(`setSandboxMode(session, mode)`)与 `SANDBOX_MODES`。该事件是策略状态——被两个家族消费——所以它住在这里,而不在任一能力的 seam 里。它的形状与仅日志(log-only)语义遵循 `approval/*` 的先例。 +- `defaultMode` / `workspaceRoot` 访问器,供执行实现读取其 resolve 回退值与边界。 + +`dsh-bash-sandbox` 自身不再携带任何沙箱配置——它注入 `sandboxPolicy` 并从中读取默认值;其 `resolve()` 优先级不变(升级授权 > per-call 盖章 > 默认)。`dsh-tool-bash` 与 `dsh-tool-fs` 用 `effectiveSandboxMode` 折叠会话的 `sandbox/mode` 以对每次调用盖章;`dsh-permission` 预设与 ACP bridge 经由迁移后的 setter 写入。拥有 bash 执行的那个 seam 不再依赖 `dsh-session`——会话依赖随折叠一起迁到了策略包。 + +### `dsh-fs-sandbox`——在提供方内部执行 + +`packages/fs/fs-sandbox/`(`@deepseek-ai/dsh-fs-sandbox`)镜像 `bash-local`/`bash-sandbox` 的拆分:`SandboxedFileSystem extends LocalFileSystem`,注册为 `ctx.fs`,注入 `sandboxPolicy`。读取(`resolve`/`stat`/`readText`/`streamText`/`listDir`)原样透传——每种模式都允许读。两个变更操作在委托给继承来的原子写之前按模式执行: + +- `read-only` 直接拒绝 `writeText`/`editText`。 +- `workspace-write` 把规范化后的目标围栏于可写根集合——`dsh-sandbox` 中的 `writableRoots(policy)`:工作区根加上平台临时目录(`/tmp`、`os.tmpdir()`),各自 realpath——与 Seatbelt profile 授予的是同一个集合,所以 fs 围栏是这一个模式含义在 bwrap/Landlock/Seatbelt profile 之外的第四种方言,因此不会出现「write 工具不能写 `/tmp` 而 bash 能」的不对称。包含判定是对真实路径的前缀包含;目标在委托前被立即重新规范化(`resolve` 对最深的既有祖先做 realpath),因此自工具解析该目标以来被换出的祖先符号链接会被捕获。 +- `danger-full-access` 不加围栏地委托。 + +拒绝是结构化的 `FS_SANDBOX_DENIED`,携带生效模式——区别于 `FS_PERMISSION_DENIED`(宿主 EACCES 是世界在拒绝;这里是策略在拒绝)。无文本推断:进程内围栏确切知道它拒绝了什么。per-call 载体是 `writeText`/`editText` 上一个末尾可选的 `sandboxMode`(文件系统侧对应 `BashExecRequest.sandboxMode`);该 seam 保持无会话依赖(由调用方盖章,正如 `resolve` 接收一个 cwd),而裸的本地后端携带并忽略它。`FileSystem.sandboxMode` 是能力事实(在基类与 `fs-local` 上为 `undefined`,在 `SandboxedFileSystem` 上为默认值),所以工具层按组合真相来宣告升级。 + +威胁模型写在包 README 里:一道位于可信代码中、针对模型可控路径的策略围栏,而非内核边界——操作是 seam 自身的,只有目标路径不可信,所以「先规范化再判包含」是对这个面的完整答案(`code-runtime` 的「containment, not a security boundary」先例)。对不可信代码的内核级隔离仍是 `ctx.bash` 的职责。resolve 到系统调用之间残留的竞态被就地重新规范化收窄,只有平台原语(`openat2` `RESOLVE_BENEATH`)能彻底消除它,而那在此不值其可移植性代价。 + +### 工具对等——一个拒绝标记、一条升级流程 + +`dsh-tool-fs` 把生效模式盖章到每次变更上,并将 `FS_SANDBOX_DENIED` 映射为模型已从 bash 认识的标记:`[sandbox: file access denied under mode]`。当 `ctx.fs.sandboxMode` 在注册时报告一个受限模式,`write` 与 `edit` 宣告相同的 `sandbox_permissions` + `justification` 字段,教授相同的同回合重试,并在执行前解析相同的 `ctx.approval` 请求——四种结果及其逐字的 fail-closed 文案沿用自[沙箱 RFC](2026-07-06-sandbox.md) § Escalation(严格加宽在执行时针对调用的生效模式检查;授权由发起它的那一次调用消费;无任何新会话事件)。 + +共享部分住在 `dsh-sandbox`,它拥有模式类型:`WIDER_MODES`、升级目标枚举、参数配对校验、拒绝/提示标记构造器,以及 `approveEscalation`——有序的 fail-closed 编排。`approveEscalation` 接收一个最小的结构化 approver(`EscalationApprover`,对 agent 与 call-id 类型泛型化),而非审批服务类型,所以 `dsh-sandbox` 不获得对 approval 或 agent 包的依赖:每个工具把自己的 `ctx.approval`、agent、call id 与工具名作为原料传入。`dsh-tool-bash` 与 `dsh-tool-fs` 都使用它们;跨文件重复检测门禁确保单一来源不走样。 + +[`examples/acp-agent`](../../../../examples/acp-agent/cordis.yml) 组合加载 `dsh-sandbox-policy` 与 `dsh-fs-sandbox`,把 `mode`/`workspaceRoot` 配置移到策略条目,并去掉在受限模式下禁用整个 fs 栈的旧门控;`fs-policy`(read-before-edit)正交地叠加其上。系统提示仍然不陈述沙箱模式——标记会在真正重要的那一刻教会模型边界,依据沙箱 RFC 的线上证据。 + +### 执行点:提供方,而非 intent gate + +沙箱 RFC 最初的跨家族草图把 fs 执行放在 `fs/write-intent`/`fs/edit-intent` 事件上。本 Agent Note 改为在提供方中执行,基于两个机制性事实:intent 槽是单决策、先到先得(已被 `dsh-fs-policy` 占据,其契约称第二个决策者为配置错误),且 intent 事件只由 `dsh-tool-fs` 派发——一个直连 `ctx.fs` 的调用方(一个 cordis 挂载插件、一个自定义工具)会绕过它们,而提供方级执行按构造覆盖每一个调用方。沙箱 RFC 的延后阶段措辞在同一变更中被更新以匹配。 + +### 范围之外 + +- **`ctx.web` 的网络策略**——`SandboxMode` 只声明文件效果;在 bash `curl` 畅通时给一个仅限 web 的网络旋钮会是一道假边界。待某个 bash 后端能执行网络(bwrap `--unshare-net`、Landlock ABI v4+)时再议。 +- **`subagent-acp` 消费者** 与 **per-session 工作区根**——沙箱 RFC 未变的延后阶段;把根集中到 `ctx.sandboxPolicy` 是后者的铺垫,而非其设计。 +- **统一的 per-tool 沙箱运行时**——因沙箱 RFC 中的理由继续否决。 + +## Alternatives considered + +- **在 `fs/*` intent 事件上执行(沙箱 RFC 的原始草图)**——因 § 执行点 中的两个机制性事实被否决:单槽先到先得且已被占据,以及对直连 `ctx.fs` 调用方的绕过。提供方级执行覆盖每一个调用方,并镜像 bash 的换实现形态。 +- **在 `tools/pre-execute` 中执行**——否决:监听器在 `resolve()` 之前看到模型的原始路径字符串,因此它会重新实现 cwd 默认化与符号链接规范化,并且仍与真正的 resolve 竞态。对 `workspace-write`(一个对规范路径的判定)而言是取消资格级的。 +- **在 `dsh-tool-fs` 中做内联检查**——否决:只覆盖工具路径(与 intent 事件同样的绕过),并在规范目标已存在之上重复了一层 resolve 知识。 +- **在 `dsh-fs-local` 上加一个 `mode` 标志而非同级后端**——否决:能力事实必须是组合真相,正如 `dsh-bash-local` 对 `dsh-bash-sandbox`;一个配置标志会让工具的宣告取决于配置,而 bash 家族已经确立了同级包形态。 +- **经受限 helper 子进程做内核级 fs 变更**——否决:每次写一个进程;`editText` 的读-匹配-写临界区不得不整体搬进子进程才能保持原子;而威胁面(可信操作、不可信路径参数)不需要内核——可信代码中的围栏就是完整答案,而不可信代码隔离仍在 `ctx.bash`。 +- **带加载期一致性校验的 per-family 策略配置**——否决:一个事实两个归属,靠一个必须枚举每个未来执行家族的校验来打补丁;策略服务让漂移不可表达,而非被检测到。 +- **把覆盖事件留在 `dsh-bash` 里作 `bash/sandbox-mode`**——否决:该事件是被两个家族消费的策略状态;保留 bash 命名会迫使 `dsh-fs-sandbox` 依赖 bash 词汇。预发布阶段,该改名是同一变更内的迁移,附带快照重录,无任何 shim。 +- **把升级编排从 approval/agent 包导入 `dsh-sandbox`**——否决:那会倒置分层(一个基础词汇包依赖 UI/agent 包)。结构化 approver 让逻辑单一来源于 `dsh-sandbox`,而依赖留在本就持有它们的工具层。 +- **fs seam 上一个合并的 mutation-options 对象**(per-call 载体最初草拟的形状)——因摩擦被否决:它会搅动每一个 `writeText`/`editText` 调用方,并把 `signal` 拆进变更专用的选项包,而读取仍保持位置参数。一个末尾可选的 `sandboxMode` 匹配 bash 的携带并忽略模式,并使 `signal` 在整个 seam 上保持对称。 +- **现在就在 `SandboxPolicy` 上加额外的可写根授权**——照旧延后:`writableRoots()` 如今由模式含义推导;临时授权是沙箱 RFC 留下的升级作用域问题。 + +## Consequences + +已交付的部分——§ Testing 的各层各自钉住: + +- 在 `read-only` 下,`write`/`edit` 返回 `[sandbox: file access denied under read-only mode]` 标记,磁盘不受触动;`read`/`listDir` 与 `dsh-fs-local` 行为一致。 +- 在 `workspace-write` 下,变更落在工作区根与临时目录下,其外被拒;包含矩阵——`..` 穿越、指向外部的绝对路径、一个既有的、指向外部的工作区内符号链接目录,以及在这样一个符号链接下新建的文件——在真实磁盘上拒绝每一种逃逸。 +- 一个被拒的 fs 变更,携带 `sandbox_permissions` + `justification` 重试一次,会经组合的审批链提示;一次授权让恰好那一次调用在更宽的模式下运行且写入落盘;rejected/cancelled/unavailable 各自产生其逐字的 fail-closed 文案且不做任何变更。 +- 一次 `permission` 预设切换同时管辖两个家族:会话切换模式后,下一次 bash 调用与下一次 fs 变更都从同一个 `sandbox/mode` 折叠遵循新模式。 +- 一次无 per-call 盖章的直连 `ctx.fs.writeText` 会被围栏于部署默认值。 +- `write`/`edit` 上的升级字段恰好在被挂载的 `ctx.fs` 受限时存在,在 `dsh-fs-local` 下不存在。 +- `agent-loop` 未被触动——一切都骑在 `ctx.sandboxPolicy`、`ctx.fs` seam、`SessionEventMap` 合并,以及工具执行管线之上。 + +代价与接受的限制: + +- **fs 围栏是策略边界,而非内核边界。** 它的威胁面是模型选定的路径,而非对抗性宿主进程;resolve 到系统调用之间残留的 TOCTOU 被收窄而非消除,README 已如实声明。内核边界仍属 bash。 +- **`dsh-bash-sandbox` 获得对 `ctx.sandboxPolicy` 的硬依赖。** 每个沙箱化组合要么加一个 `cordis.yml` 条目,要么在加载时高声失败——这是有意的预发布奠基之举;示例在同一变更内更新。 +- **围栏与 runner 的对等是推导出来的,而非断言的。** fs 围栏与 Seatbelt profile 都从 `writableRoots` 取其可写集合,一个对等单元测试钉住这些集合;一个 runner profile 若在不经该函数的情况下改变其可写集合便会漂移。 +- **标记与升级教学如今服务于两个家族。** 措辞改动是 `dsh-sandbox` 中一个构造器背后的协调编辑;重复检测门禁与钉住的快照维持单一来源,代价是 fs 与 bash 无法在不拆分该构造器的情况下有意地在措辞上分道。 + +## Testing + +- 单元:`dsh-sandbox` 钉住升级阶梯、标记构造器、参数配对校验,以及 `approveEscalation` 的有序 fail-closed 序列(非加宽、无 approval、无 agent、各结果),外加 `writableRoots`/`canonicalPath`。`dsh-sandbox-policy` 钉住默认访问器、折叠/setter、加载期模式拒绝,以及 HMR 安全。`dsh-fs-sandbox` 在真实文件系统上钉住 per-mode 围栏与包含矩阵(内部、临时目录、绝对路径-外部、`..`、指向外部的符号链接目录、其下的新建文件、路径等于根、以分隔符结尾的根),外加 per-call 覆盖与 HMR 安全。`dsh-tool-fs` 钉住宣告门控、模式盖章、折叠、拒绝标记映射,以及完整的升级矩阵(授权、拒绝、无服务、无 agent、配对、非受限守卫)。`dsh-tool-bash`、`dsh-bash-sandbox` 与 `dsh-permission` 迁移到迁移后的策略/工具集。 +- 快照:acp-agent 示例组合 `dsh-sandbox-policy` + `dsh-fs-sandbox`;被钉住的 header 携带 fs 升级字段与 `sandbox/mode` 事件名,一次性重录。 diff --git a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md index 75355aa8d9..b0149016ab 100644 --- a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md +++ b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md @@ -18,7 +18,7 @@ The durable phases are `active`, `paused`, `blocked`, and `complete`. A blocked ### Durable record and replay -Every non-clear mutation uses `Agent.inject()` to append a raw, model-visible `context/message` containing a versioned full snapshot. Clear appends a revisioned tombstone. The context source is `{ kind: 'goal', goalId, revision, round: 0 }`; metadata and rendered `...` content must agree exactly. This descriptive delimiter follows the repository's existing `` convention and [Anthropic's published guidance to structure mixed prompt content with consistent descriptive XML tags](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#structure-prompts-with-xml-tags). That is public model-experience prior art, not evidence about any provider's proprietary training corpus. The session log is the only durable source of truth, so persistence and fork inherit goal records without another database or header field. +Every non-clear mutation uses `Agent.inject()` to append a model-visible `context/message` containing a versioned full snapshot; the session projects that content verbatim. Clear appends a revisioned tombstone. The context source is `{ kind: 'goal', goalId, revision, round: 0 }`; metadata and rendered `...` content must agree exactly. This descriptive delimiter follows the repository's existing `` convention and [Anthropic's published guidance to structure mixed prompt content with consistent descriptive XML tags](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#structure-prompts-with-xml-tags). That is public model-experience prior art, not evidence about any provider's proprietary training corpus. The session log is the only durable source of truth, so persistence and fork inherit goal records without another database or header field. The replay fold validates JSON shape, source attribution, rendered content, fresh ids, revision continuity, lifecycle transitions, counters, and monotonic per-goal timestamps. Goal rounds are positive sequential `user/message` source numbers for the current active revision and cannot exceed `maxGoalRounds`; ordinary session turns do not affect the counter. A malformed current-format record fails replay rather than being ignored or repaired. diff --git a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md index fd7e31f4b6..33f44136da 100644 --- a/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md +++ b/.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.zh.md @@ -18,7 +18,7 @@ Status: implemented ### 持久记录与回放 -每次非清除变更都通过 `Agent.inject()` 追加一条原始且模型可见的 `context/message`,其中包含带版本的完整快照。清除操作追加带修订号的墓碑。上下文来源为 `{ kind: 'goal', goalId, revision, round: 0 }`;元数据必须与渲染后的 `...` 内容完全一致。这个描述性分隔符沿用了仓库已有的 `` 约定,也符合 [Anthropic 关于用一致且描述明确的 XML 标签组织混合提示词内容的公开指南](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#structure-prompts-with-xml-tags)。这是公开的模型体验先例,并非对任何提供方专有训练语料的推断。会话日志是唯一的持久事实来源,因此持久化和 fork 会继承目标记录,而无需另设数据库或头字段。 +每次非清除变更都通过 `Agent.inject()` 追加一条模型可见的 `context/message`,其中包含带版本的完整快照;会话会将其内容原样投射给模型。清除操作追加带修订号的墓碑。上下文来源为 `{ kind: 'goal', goalId, revision, round: 0 }`;元数据必须与渲染后的 `...` 内容完全一致。这个描述性分隔符沿用了仓库已有的 `` 约定,也符合 [Anthropic 关于用一致且描述明确的 XML 标签组织混合提示词内容的公开指南](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#structure-prompts-with-xml-tags)。这是公开的模型体验先例,并非对任何提供方专有训练语料的推断。会话日志是唯一的持久事实来源,因此持久化和 fork 会继承目标记录,而无需另设数据库或头字段。 回放折叠会校验 JSON 形状、来源归属、渲染内容、新 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。目标回合是当前活跃修订上带正数且连续编号的 `user/message` 来源,且不能超过 `maxGoalRounds`;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。 diff --git a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml index 2e152a0072..a1d901ad15 100644 --- a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.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-02-bilingual-docs-and-pairing-gate.md: 45c6edff41a7bc21c76aeeaf14d16af824c601de -2026-07-02-bilingual-docs-and-pairing-gate.zh.md: 91ba7523705d1500150efe0eac9085ea980e80d6 +2026-07-02-bilingual-docs-and-pairing-gate.md: 3be1d5d8fd9dba20cfca34c79cb01d89fad8097a +2026-07-02-bilingual-docs-and-pairing-gate.zh.md: a8aa8812934e755fe0175c8f3f20d194e4d24b4a diff --git a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md index 45c6edff41..3be1d5d8fd 100644 --- a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md +++ b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md @@ -13,7 +13,7 @@ This repo's README and docs tree are read by people and agents inside and outsid - **Paired sibling files with equal authority.** A documentation pair is three sibling files: English `foo.md`, Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`. Neither language is canonical — a document may be authored and reviewed Chinese-first and translated to English afterwards, or the reverse; what binds the pair is that both sides must say the same thing, and pairs merge whole (both languages plus the record, never one alone). Policy: [docs/i18n/README.md](../../../../docs/i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md). - **A sidecar record of both blob hashes makes consistency checkable.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last confirmed-consistent state. An edit to either side without re-confirming the pair is then mechanically detectable as a pure content comparison — no history lookup — and the hashes are computable for files edited in the same PR, which a commit-hash record is not. Re-recording (`verify-translation-pairing --write`) produces a reviewable yaml diff: confirming consistency is an explicit, visible act in the PR. - **`verify-translation-pairing` joins `doc-sync`.** The gate ([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts)) enforces: required pairs exist, every existing pair is complete (all three files) and consistent (both hashes match, switcher links both ways, structural signatures identical), excluded (generated or bilingual-by-construction) files stay unpaired, and date-named documents on or after the manifest's `requiredSince` cutoff have complete pairs. The `required` list in [scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) is a ratchet: each merged translation batch adds its files, so coverage only grows. -- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth. +- **Translation is agent work with human review.** The committed workflow is [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../skills/dsh-code-review/SKILL.md): the skill carries the workflow and defers to the docs as sources of truth. The skill directs the orchestrating agent to delegate translation writing to a subagent. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md index 91ba752370..a8aa881293 100644 --- a/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md +++ b/.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md @@ -13,7 +13,7 @@ Status: implemented - **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../../docs/i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../../docs/i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../../docs/i18n/terminology.md)。 - **伴随记录保存两侧 blob hash,使一致性可检查。** `foo.i18n.yaml` 保存两侧文件在上一次确认一致时各自的完整 git blob hash。此后修改了任一侧而未重新确认配对,都能被机械检测出来(纯内容比较,无需查询历史),而且同一个 PR(Pull Request)内改动的文件也能计算出 hash,commit hash 式的记录做不到这一点。重新记录(`verify-translation-pairing --write`)会产生一份可评审的 yaml diff:确认一致在 PR 中是一个显式、可见的动作。 - **`verify-translation-pairing` 加入 `doc-sync`。** 门禁([scripts/verify-translation-pairing.ts](../../../../scripts/verify-translation-pairing.ts))强制执行以下规则:required 的配对必须存在;任何已存在的配对必须完整(三个文件齐全)且一致(两个 hash 匹配、切换行双向互链、结构签名一致);被排除的文件(生成物或本身即双语的)不得配对;凡文件名以日期开头且日期不早于 manifest(元数据清单)中 `requiredSince` 分界日期的文档,也必须有完整配对。[scripts/translation-pairing.manifest.json](../../../../scripts/translation-pairing.manifest.json) 中的 `required` 清单只进不退:每个合并的翻译批次将自己的文件加入其中,覆盖面只增不减。 -- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../skills/dsh-code-review/SKILL.md) 模式相同:skill(技能)承载工作流,并将文档作为真源。 +- **翻译是 agent 的工作,由人评审。** 仓库内置的工作流是 [.agents/skills/dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../skills/dsh-code-review/SKILL.md) 模式相同:skill(技能)承载工作流,并将文档作为真源。该 skill 要求编排 agent 把翻译写作委派给 subagent。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md new file mode 100644 index 0000000000..b3bdbb0c1b --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md @@ -0,0 +1,41 @@ +# Agent Note: Project canonical documentation into the website + +Status: implemented + +## Problem + +The repository needs a navigable documentation website without turning the website directory into a second documentation source. Copying package guides, architecture pages, or generated catalogs into a site-specific tree allows the two copies to drift, while pointing VitePress directly at the repository root couples public URLs and navigation to the internal file layout. Repository-relative links also need different destinations on the website: published pages stay inside the site, but source files and unpublished contributor documents belong on GitHub. + +## Decision + +Canonical Markdown remains in the repository tier that owns it. Product-facing guides live under `docs/user/`, generated reference remains in the existing generated catalogs, and architectural and cookbook pages remain at their existing `docs/` paths. + +`website/docs.ts` is an explicit publication manifest. Each entry maps one canonical source file to a stable public route, sidebar, section, and order. Adding or removing a published page is therefore a reviewable manifest change rather than an implicit directory crawl. + +`scripts/project-doc-site.ts` projects the manifest into the ignored `website/.generated/` directory before VitePress starts or builds. The generated tree follows public routes so VitePress navigation, locale detection, and local search share the same route vocabulary. Each page receives an `editSource` frontmatter field pointing to its canonical repository file; the edit-link callback reads only that page data, so public URLs remain independent of the source layout. + +Locale home projections retain only the canonical YAML frontmatter. The repository-facing body can keep its H1 and bilingual source links, while the VitePress home theme owns the rendered hero and features and the site navigation owns locale switching. + +The projector parses Markdown links without reserializing the document. A link to another published source becomes a site-relative route; a link to an unpublished repository file becomes a GitHub source link; a repository image becomes a raw GitHub URL. Missing relative targets fail projection. Unit tests pin these transformations, and `docs:check` runs the projector tests plus a production VitePress build as part of `doc-sync` and the parallel documentation gates. + +Mermaid renders the canonical diagrams. The website workspace explicitly declares the five packages that `vitepress-plugin-mermaid` asks Vite to prebundle because pnpm's strict dependency isolation otherwise makes those transitive packages unavailable to the local development server; Knip records this runtime-only use as an intentional dependency exception. + +Site publication is separate from site construction. The repository contains local development and build commands, but no hosting or deployment workflow until a public destination is chosen. + +## Alternatives considered + +**Commit copied Markdown under `website/`.** This makes VitePress setup direct, but every copied guide or API table gains two owners and requires a synchronization convention that cannot identify which copy is authoritative. + +**Make `website/` the canonical home for every published page.** This keeps one copy but moves architecture, generated reference, and contributor-facing material away from their repository ownership tiers merely to satisfy a renderer. + +**Discover every Markdown file automatically.** This minimizes manifest maintenance but publishes internal documents accidentally, exposes source moves as URL changes, and produces navigation from incidental directory order. + +**Use filesystem symlinks.** Symlinks preserve a single source but do not solve public routing or repository-relative links, and their behavior is less predictable across local development, package tooling, and hosted CI environments. + +**Build only in a deployment workflow.** A deployment job can reveal rendering failures after merge. Keeping the production build in `doc-sync` makes the same failure visible locally and in ordinary CI even when no public deployment exists. + +## Consequences + +Documentation facts have one editable home, public routes remain stable across source moves, and the site can include generated references without committing another generated copy. Local development watches canonical inputs and regenerates the disposable projection. + +The publication manifest is a maintained allowlist, and link projection adds a small repository-specific build adapter. A new kind of Markdown link behavior needs a projector test. Mermaid support also increases the client bundle size, but preserves diagrams already used by the canonical documentation. diff --git a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.i18n.yaml b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.i18n.yaml new file mode 100644 index 0000000000..6bae3b4d87 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.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-20-generated-cordis-core-api.md: 848dec2dba6f432c706798c40abe98e8937da651 +2026-07-20-generated-cordis-core-api.zh.md: c40a480224f4e1387b71ade9264458cd84403584 diff --git a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.md b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.md new file mode 100644 index 0000000000..848dec2dba --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.md @@ -0,0 +1,31 @@ +# Agent Note: Generate the Cordis core API reference + +Status: implemented + +English | [中文](2026-07-20-generated-cordis-core-api.zh.md) + +## Problem + +Plugin authors need the detailed Cordis APIs behind `ctx`, event dispatch, fibers, plugin registration, and services. The generated [Harness event and service catalogs](2026-06-20-generated-cordis-catalog.md) intentionally summarize inherited Cordis members, so they do not replace a method-level Cordis reference. Keeping a second hand-written copy under the website would drift from the vendored source and make the renderer an additional documentation owner. + +## Decision + +`scripts/cordis-core-api.ts` reads the public declarations and original JSDoc from `vendor/cordis/src` with the TypeScript compiler API. An explicit page manifest generates five files under [`docs/cordis-catalog/core/`](../../../../docs/cordis-catalog/core/context.md): Context, Events, Fiber, Registry, and Service. `scripts/gen-cordis-catalog.ts` writes these pages together with the Harness event and service catalogs, and `verify-cordis-catalog` rejects stale output. + +The generator validates that documented classes and methods retain descriptive JSDoc, including parameter and non-void return contracts. It emits declaration-only `ts cordis-catalog` fences with the original JSDoc, then renders the same description, parameters, and return contract as readable Markdown. Source links point to the vendored files, and the five pages cross-link to one another. The Harness catalogs remain the exhaustive inventory of repository-declared events and `ctx.*` services; the core pages document how the inherited Cordis APIs operate. + +`website/docs.ts` publishes the five canonical files under matching `/reference/cordis-api/` and `/en/reference/cordis-api/` routes. Both locales use the English generated source until the generator emits translated pages, so changing language preserves navigation structure and route identity. + +## Alternatives considered + +**Restore the old website files as canonical Markdown.** This would recover the pages quickly, but their signatures and prose could drift from the vendored implementation and the website would regain a second documentation source. + +**Expand the inherited tier of the Harness catalogs in place.** Those catalogs answer which Harness events and services exist. Mixing full framework class references into the same pages would obscure that inventory and reverse their deliberate terse inherited tier. + +**Publish vendored source declarations directly.** Source files are authoritative but do not provide stable topic pages, curated public ordering, or website navigation, and they expose implementation bodies that are not part of the reference contract. + +## Consequences + +The five Cordis API pages follow vendor updates through one deterministic generator and share the repository's documentation freshness gate. The website gains a dedicated Cordis API section without copied site content, while root and English navigation remain structurally identical. + +The page manifest is curated, so a newly public Cordis core type needs an explicit generator entry. Generated prose is English-only, and source JSDoc quality directly limits reference quality; Chinese output requires generator-level translation rather than hand-editing the generated files. diff --git a/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.zh.md b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.zh.md new file mode 100644 index 0000000000..c40a480224 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-20-generated-cordis-core-api.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 生成 Cordis 核心 API 参考文档 + +Status: implemented + +[English](2026-07-20-generated-cordis-core-api.md) | 中文 + +## 问题 + +插件作者需要了解 `ctx`、事件派发、Fiber、插件注册和 Service 背后的详细 Cordis API。已有的 [Harness 事件与服务目录](2026-06-20-generated-cordis-catalog.md)有意只简要概括继承自 Cordis 的成员,因此无法替代方法级 Cordis 参考文档。如果在网站下维护另一份手写副本,它会与 vendored 源码产生漂移,也会让渲染器成为额外的文档所有者。 + +## 决策 + +`scripts/cordis-core-api.ts` 使用 TypeScript Compiler API,从 `vendor/cordis/src` 读取公开声明和原始 JSDoc。一个显式页面清单在 [`docs/cordis-catalog/core/`](../../../../docs/cordis-catalog/core/context.md) 下生成五个文件:Context、Events、Fiber、Registry 和 Service。`scripts/gen-cordis-catalog.ts` 将这些页面与 Harness 事件和服务目录一同写入,`verify-cordis-catalog` 会拒绝过期产物。 + +生成器会验证所记录的类和方法保留描述性 JSDoc,包括参数和非 void 返回值契约。它生成包含原始 JSDoc 且仅含声明的 `ts cordis-catalog` 代码围栏,再将同一份说明、参数和返回值契约渲染为便于阅读的 Markdown。源码链接指向 vendored 文件,五个页面之间相互交叉链接。Harness 目录仍是仓库声明的事件与 `ctx.*` 服务的完整清单;核心页面负责说明继承自 Cordis 的 API 如何工作。 + +`website/docs.ts` 将五个规范源文件发布到结构对应的 `/reference/cordis-api/` 和 `/en/reference/cordis-api/` 路由。在生成器产出翻译页面之前,两个 locale 都使用英文生成源,因此切换语言时导航结构和路由标识保持不变。 + +## 考虑过的替代方案 + +**将旧网站文件恢复为规范 Markdown。** 这能快速恢复页面,但其签名和说明可能与 vendored 实现漂移,网站也会重新成为第二个文档来源。 + +**直接扩充 Harness 目录中的继承层。** 这些目录回答有哪些 Harness 事件与服务。将完整的框架类参考混入同一页面会模糊这份清单的定位,并推翻继承层保持精简的既有决定。 + +**直接发布 vendored 源码声明。** 源文件具有权威性,但不能提供稳定的主题页面、经过筛选的公开顺序或网站导航,还会暴露不属于参考契约的实现体。 + +## 影响 + +五个 Cordis API 页面通过同一个确定性生成器跟随 vendor 更新,并复用仓库的文档新鲜度检查。网站无需复制内容即可获得独立的 Cordis API 章节,中文入口和英文入口的导航结构保持一致。 + +页面清单需要人工维护,因此新增公开 Cordis 核心类型时必须显式添加生成器条目。当前生成说明只有英文,且源码 JSDoc 的质量直接决定参考文档质量;中文产物需要在生成器层实现翻译,不能手工编辑生成文件。 diff --git a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.i18n.yaml new file mode 100644 index 0000000000..e441d0bfb1 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.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-17-one-send-one-turn.md: 86c056b53700d0e0c02e04a99cf044fb311f5840 +2026-07-17-one-send-one-turn.zh.md: 3ef9973480481d11d1183760c9fc1f3c247629f4 diff --git a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md new file mode 100644 index 0000000000..86c056b537 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md @@ -0,0 +1,45 @@ +# Agent Note: Remove implicit batching from ordinary sends + +Status: implemented + +English | [中文](2026-07-17-one-send-one-turn.zh.md) + +## Problem + +Suppose a caller submits message A and then message B with two `Agent.send()` calls. Implicit batching can put A and B in one turn simply because both are waiting when the driver reads its queue. The caller made two calls, but the loop silently turns them into one unit of work. + +That grouping depends on timing rather than caller intent. Calls from one synchronous stack, neighboring microtasks, event listeners, and model callbacks could be grouped differently even though every caller used the same API. + +This grouping changes behavior, not just the number of model calls. One ordinary turn owns prompt admission, `turn/start`, `turn/end`, and a durability checkpoint. If message B shares message A's turn, B can enter A's model request instead of first seeing A's closed result in the session log. Allowing one message while blocking another also requires a mixed state that no caller requested. + +## Decision + +The rule is simple: each successful `send()` creates one independent FIFO queue item. If that item runs, it is the only ordinary message in its turn. An item can be dropped before it starts, so the precise guarantee is at most one turn rather than exactly one; two sends are never silently combined. + +Before enqueueing an item, `send()` checks the agent state and makes a detached, deeply frozen snapshot of the content and resolved source. After enqueueing it, `send()` publishes `agent/queued`. + +If messages A and B are both processed, B's turn starts only after A records `turn/end` and A's durability checkpoint settles. B's request therefore sees whatever closed result A left in the same session log. A checkpoint error is reported, but settlement only releases this ordering barrier; it does not make a failed write durable. Broad `cancel()`, disposal, or a failure before `turn/start` can instead discard an unstarted item without opening an empty turn. + +Prompt admission decides one message at a time. An allowed prompt becomes that turn's `user/message`; a blocked prompt records one durable `prompt/blocked` and closes its one-message turn as `rejected`. Mixed-batch and all-blocked-batch branches do not exist. + +The no-batching rule applies only to ordinary `send()`. Running `steer()` puts input in a separate steering FIFO. While a turn remains open, the loop records that input at the next steering checkpoint, which comes before either a model request or the decision whether to continue. Steering makes another step the default, but continuation or terminal policy can still stop before the step starts. Steering left after the turn closes and its durability checkpoint settles becomes later queued input; terminal `agent/turn-stop`, cancellation, or disposal can discard it. When the agent is idle, `steer()` delegates to `send()`, so it creates an independent ordinary queue item. + +`inject()` continues to add model-facing context without submitting an ordinary message; its existing turn-enclosure and flush behavior stays unchanged. `cancel()` remains a whole-agent operation that can clear all unstarted ordinary and steering input and abort the current step. `status` and `whenIdle()` also describe the whole agent, not one message. Several one-message turns can share one `running` interval, including turn close and its checkpoint, so `running` does not prove that a turn is open. + +## Alternatives considered + +**Keep automatic ordinary-send batching to reduce model calls.** This can improve throughput when producers outpace the driver, but it makes turn boundaries depend on scheduling and lets a later message run before the preceding turn closes and reaches its checkpoint. The decision keeps the predictable boundary and accepts the extra calls. Any future batching feature needs an explicit caller-visible contract backed by measurements. + +## Verification + +- Unit and property tests submit sends from the same stack, neighboring microtasks, different producers, and reentrant callbacks; every message gets its own FIFO-ordered turn. +- A built-stdio test submits two lines and observes two model requests and two turn boundaries. +- Delayed and rejected first-turn checkpoints keep the next turn waiting and prove that its request sees the preceding assistant result. +- Failure-path tests cover prompt veto, listener failure, broad cancellation, disposal, and failure before `turn/start`; recorded turns stay balanced, messages do not merge, and surviving queued work still drains. +- Separate tests cover open-turn, post-turn-close, and idle `steer()`, plus `inject()`, whole-agent status, and `whenIdle()`. + +## Consequences + +Ordinary turn boundaries are predictable: messages A and B stay separate, and B runs only after A has closed and reached its checkpoint. Callers still do not receive a per-send completion or cancellation handle; broad cancellation can discard the entire unstarted tail, while status and quiescence remain agent-wide observations. + +The trade-off is more model requests and more checkpoints. A busy queue can take longer to drain and can grow under sustained producers. Ordinary-send batching returns only through an explicit, measured contract. diff --git a/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md new file mode 100644 index 0000000000..3ef9973480 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.zh.md @@ -0,0 +1,45 @@ +# Agent Note: 删除普通 send 的隐式批处理 + +Status: implemented + +[English](2026-07-17-one-send-one-turn.md) | 中文 + +## 问题 + +假设调用方连续两次调用 `Agent.send()`,先提交消息 A,再提交消息 B。隐式批处理可能只因为驱动器读取队列时两条消息都在等待,就把 A、B 放进同一个轮次。调用方明明调用了两次,agent loop(智能体循环)却悄悄把它们变成一个工作单元。 + +这种分组取决于运行时机,而不是调用方的意图。因此,即使所有调用方使用相同 API,来自同一个同步调用栈、相邻微任务、事件监听器和模型回调的调用也可能产生不同分组。 + +这种分组改变的不只是模型调用次数。一个普通轮次包含提示词准入、`turn/start`、`turn/end` 和持久性检查点。如果消息 B 与消息 A 共用轮次,B 可能直接进入 A 的模型请求,而不是先看到 A 在会话日志中已经关闭的结果。若系统允许一条消息、阻止另一条消息,还需要引入调用方没有请求的混合状态。 + +## 决策 + +规则很简单:一次成功的 `send()` 创建一个独立的 FIFO 队列项。该队列项如果运行,就是所在轮次中唯一的普通消息。队列项可能在启动前被丢弃,因此精确保证是最多一个轮次,而不是必定一个轮次;两次 send 绝不会被悄悄合并。 + +队列项入队之前,`send()` 会检查 agent 状态,并为内容和解析后的来源创建一份脱离调用方对象、经过深度冻结的快照。队列项入队之后,`send()` 发布 `agent/queued`。 + +如果消息 A、B 都进入处理,B 的轮次只能在 A 记录 `turn/end` 且 A 的持久性检查点处理结束后开始。因此,B 的请求能看到 A 在同一会话日志中留下的已关闭结果。检查点错误会照常报告,但处理结束只表示解除这道顺序屏障,不表示失败的写入已经持久化。广义 `cancel()`、dispose(资源释放)或 `turn/start` 之前的失败也可能丢弃尚未启动的队列项,而不打开一个空轮次。 + +提示词准入每次只决定一条消息。获准提示词成为该轮次的 `user/message`;被阻止的提示词记录一条持久的 `prompt/blocked`,并让自己的单消息轮次以 `rejected` 关闭。实现中不存在混合批次或全阻止批次分支。 + +上述不合批规则只适用于普通 `send()`。agent 运行时,`steer()` 会把输入放入独立的 steering(中途引导)FIFO。只要当前轮次仍然打开,agent loop 就会在下一个 steering 检查点记录该输入;该检查点位于模型请求或继续轮次的决策之前。收到 steering 会把再执行一步作为默认选择,但继续轮次的策略或终止策略仍可在该步骤开始前停止。轮次关闭且其持久性检查点处理结束后,剩余的 steering 会成为后续排队输入;终止性的 `agent/turn-stop`、取消或 dispose 可以将其丢弃。agent 空闲时,`steer()` 委托给 `send()`,因此会创建一个独立的普通队列项。 + +`inject()` 继续添加面向模型的上下文,而不提交普通消息;其现有的轮次封闭与持久化刷新行为保持不变。`cancel()` 仍是面向整个 agent 的操作,可以清空所有尚未启动的普通输入和 steering,并中止当前步骤。`status` 和 `whenIdle()` 描述的也是整个 agent,而不是某一条消息。多个单消息轮次可以共用一个 `running` 区间,该区间还可能覆盖轮次关闭及其检查点,因此 `running` 不表示轮次一定处于打开状态。 + +## 曾考虑的替代方案 + +**保留普通 send 的自动批处理,以减少模型调用。** 当消息进入队列的速度超过驱动器的处理速度时,这种做法可以提高吞吐量,但会让轮次边界取决于调度,并让后一条消息在前一轮关闭且到达检查点之前运行。本决策保留可预测的边界,并接受额外调用。未来若要加入批处理功能,必须提供调用方可见的显式契约,并有测量结果作为依据。 + +## 验证 + +- 单元测试和性质测试从同一调用栈、相邻微任务、不同生产方和重入回调提交 send;每条消息都会得到一个按 FIFO 排序的独立轮次。 +- stdio 构建产物测试提交两行输入,并观察到两个模型请求和两个轮次边界。 +- 延迟和拒绝第一个轮次的检查点,都能让下一个轮次保持等待,并证明其请求可以看到前一条助手结果。 +- 失败路径测试覆盖提示词否决、监听器失败、广义取消、dispose 和 `turn/start` 之前的失败;已记录的轮次保持边界平衡,消息不会合并,仍需处理的排队工作也能继续清空。 +- 其他测试分别覆盖轮次打开时、轮次关闭后和空闲时的 `steer()`,以及 `inject()`、面向整个 agent 的状态和 `whenIdle()`。 + +## 后果 + +普通轮次的边界可预测:消息 A、B 始终分开,B 只能在 A 关闭并到达检查点后运行。调用方仍然拿不到逐次 send 的完成或取消句柄;广义取消可以丢弃整个尚未启动的队尾,状态和静止性也仍是面向整个 agent 的观察。 + +代价是模型请求和检查点都会增加。繁忙队列可能需要更长时间才能清空;如果生产方持续提交消息,队列也可能增长。只有建立显式且经过测量的契约后,才能重新引入普通 send 批处理。 diff --git a/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.i18n.yaml new file mode 100644 index 0000000000..edb7429454 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.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-20-unwrap-injected-content-envelopes.md: 32642660f7bcea748c349933b99552b1974922c5 +2026-07-20-unwrap-injected-content-envelopes.zh.md: a01a51e12cecca5bc46526ccca61dbe90eb3136f diff --git a/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md new file mode 100644 index 0000000000..32642660f7 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md @@ -0,0 +1,41 @@ +# Agent Note: Project injected content verbatim, dropping the XML envelopes + +Status: implemented + +English | [中文](2026-07-20-unwrap-injected-content-envelopes.zh.md) + +## Problem + +Two families of injected session content rendered into the model transcript wrapped in XML envelopes: `steering/message` as `` and `context/message` as `` (the latter with a `'raw'` opt-out that skipped the wrapper). The envelopes aimed to tell the model "this is injected, not the user speaking." + +Two problems: + +- **No model is trained on these tags.** `` and `` are arbitrary markup no model was taught to read, so the framing adds tokens without a reliable effect and can actively mislead — recorded transcripts show a model treating a `` instruction as third-party metadata and refusing it while answering only the original prompt. +- **The session surface is the wrong layer for framing.** The surface projects the durable log into the model transcript; deciding how content is worded is not its job. A caller that wants a particular frame formats its own content before injecting it — which the one heavy producer (`workspace-context`) already does, owning its complete `` frame and opting out of the `` wrapper with `envelope: 'raw'`. The remaining tag machinery (`ContextEnvelope`, an `envelope` field threaded through `InjectOptions`, `HookContext`, the `context/message` event, and the loop) served a distinction that belongs to the caller. + +## Decision + +Injected session content projects verbatim; the caller owns any framing. `deriveEventMessage` renders `user/message`, `context/message`, and `steering/message` through one shared case returning `{ role: 'user', content: event.data.content }`; their content blocks reach the model unchanged. `context/message`'s `source`/`meta` and `steering/message`'s `turn` stay in the durable event log but do not render. + +The `ContextEnvelope` type and every `envelope` field are removed — `context/message` in `SessionEventMap`, `InjectOptions`, `HookContext`, and the `inject()`/`additionalContexts` plumbing in `dsh-agent-loop`. `workspace-context` no longer requests `'raw'`; its self-framed content renders as before. The `renderTagged`/`renderContextEnvelope` helpers are deleted. `context/message.meta` still carries durable, model-hidden JSON state. + +The `source` attribution the envelopes carried is not lost — it remains on the durable events; it simply no longer renders into the transcript. + +## Alternatives considered + +- **Keep the `` envelope, unwrap only steering** — leaves the `ContextEnvelope`/`envelope` machinery alive for a framing bit no model reads, and keeps the inconsistency that the main producer already opts out of. +- **Keep the envelope field for plugin-sourced content only** — splits one projection into two on `source.kind` for no observed benefit; a plugin steering the agent (hook-bridge continuation reasons) also wants the instruction followed, not labeled. +- **Move the unwrapping into adapters** — the canonical projection is the model-visible contract ("model-visible ⟺ logged"); per-adapter divergence on framing would make the derived transcript adapter-dependent. Framing that a caller genuinely wants belongs in the caller's content, not in an adapter. + +## Consequences + +- Mid-turn steering and injected context reach the model with the same weight as an ordinary user prompt. +- The transcript no longer distinguishes injected content from a user message; consumers that need the distinction read the durable event log, which keeps the event types, `source`, and `meta` intact. +- The `hook-{cc,codex}-stop-continue` ACP snapshots were re-recorded: the old recordings captured the model refusing steering as third-party metadata, the fix's exact failure mode. +- The [content-block-vocabulary Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)'s tagged-envelope clause is amended to point here. + +## Deferred + +`workspace-context` already frames its own content: it emits a complete `` block as the message content instead of leaning on a surface-level wrapper. That caller-owned pattern is the one to keep — the surface passes content through verbatim, and any framing lives in the producer's own content. + +Two framing paths existed — caller-baked framing (`workspace-context`'s ``) and surface-level wrapping (``/`` added by `deriveEventMessage`). This change removes the second, leaving only caller-owned framing. If labeled framing is wanted again, unify it through the event's `meta` map — the producer-attached, model-hidden metadata field — consumed by a dedicated renderer or adapter, rather than re-hardcoding a tag in `deriveEventMessage`. A producer declares the frame it wants in `meta`; one renderer applies it; the session-surface projection stays a verbatim pass-through. diff --git a/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.zh.md b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.zh.md new file mode 100644 index 0000000000..a01a51e12c --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.zh.md @@ -0,0 +1,41 @@ +# Agent Note: 注入内容逐字投影,去除 XML 封套 + +Status: implemented + +[English](2026-07-20-unwrap-injected-content-envelopes.md) | 中文 + +## 问题 + +两类注入的会话内容在渲染进模型 transcript(文本记录)时被包在 XML 封套里:`steering/message` 包成 ``,`context/message` 包成 ``(后者有一个 `'raw'` 退出选项可跳过封套)。这些封套意在告诉模型「这是注入内容,不是用户在说话」。 + +两个问题: + +- **没有模型在这些标签上训练过。** `` 和 `` 是任何模型都未被教会去读的任意标记,因此这层框架只是徒增 token 而没有可靠效果,还可能起反作用——已录制的 transcript 显示,模型会把 `` 指令当成第三方元数据而拒绝服从,只回答原始提示。 +- **session 表层是承载框架的错误层次。** 表层的职责是把持久日志投影为模型 transcript;决定内容如何措辞并不是它的事。想要特定框架的调用方可以在注入前自行格式化内容——唯一的重度生产方(`workspace-context`)本就这样做,它自带完整的 `` 框架,并用 `envelope: 'raw'` 退出 `` 封套。剩下的标签机制(`ContextEnvelope` 类型,以及贯穿 `InjectOptions`、`HookContext`、`context/message` 事件和 agent loop 的 `envelope` 字段)所服务的区分,本应归属调用方。 + +## 决策 + +注入的会话内容逐字投影,框架由调用方自行负责。`deriveEventMessage` 通过一个共享分支渲染 `user/message`、`context/message` 和 `steering/message`,都返回 `{ role: 'user', content: event.data.content }`;它们的内容块原样到达模型。`context/message` 的 `source`/`meta` 和 `steering/message` 的 `turn` 保留在持久事件日志中,但不渲染。 + +`ContextEnvelope` 类型和所有 `envelope` 字段都被移除——包括 `SessionEventMap` 中的 `context/message`、`InjectOptions`、`HookContext`,以及 `dsh-agent-loop` 中 `inject()`/`additionalContexts` 的相关管线。`workspace-context` 不再请求 `'raw'`;它自带框架的内容渲染方式不变。`renderTagged`/`renderContextEnvelope` 辅助函数被删除。`context/message.meta` 仍携带持久的、对模型隐藏的 JSON 状态。 + +封套曾携带的 `source` 归属并未丢失——它仍保留在持久事件上;只是不再渲染进 transcript。 + +## 权衡的替代方案 + +- **保留 `` 封套,只对 steering 去封套** —— 会为一个没有模型会读的框架位保留 `ContextEnvelope`/`envelope` 机制,并保留主要生产方本就退出的那种不一致。 +- **仅对插件来源的内容保留 envelope 字段** —— 会按 `source.kind` 把一条投影拆成两条,却没有观察到任何收益;插件引导 agent(智能体)时(钩子桥接器的轮次续行原因)同样希望指令被遵从,而不是被贴标签。 +- **把去封套的逻辑移入适配器** —— 规范投影就是模型可见契约(「模型可见 ⟺ 已记录」);让各适配器在框架上各行其是,会使派生的 transcript 依赖于适配器。调用方确实想要的框架应放进调用方自己的内容里,而不是适配器。 + +## 结果 + +- 中途引导与注入的 context 以与普通用户提示相同的权重到达模型。 +- transcript 不再区分注入内容与用户消息;需要这一区分的消费方读取持久事件日志,其中事件类型、`source` 和 `meta` 完整保留。 +- `hook-{cc,codex}-stop-continue` ACP 快照已重新录制:旧录制捕获的是模型把 steering 当作第三方元数据而拒绝服从,正是本次修复针对的失败模式。 +- [内容块词汇表 Agent Note](../architecture/2026-06-11-content-block-vocabulary.md) 中关于带标签封套的条款已修订为指向本文。 + +## 推迟事项 + +`workspace-context` 已经自行为内容加框架:它把一个完整的 `` 块作为消息内容发出,而不依赖表层封套。这种调用方自有的模式才是应保留的——表层逐字透传内容,任何框架都住在生产方自己的内容里。 + +曾经存在两条框架路径——调用方自行加框架(`workspace-context` 的 ``),以及表层封套(`deriveEventMessage` 加上的 ``/``)。本次变更移除了后者,只留下调用方自有的框架。如果未来又需要带标签的框架,应由事件的 `meta` map(生产方附加、对模型隐藏的元数据字段)来统一它,交给专门的渲染器或适配器消费,而不是在 `deriveEventMessage` 中重新硬编码标签。生产方在 `meta` 中声明所需的框架,由一个渲染器统一施加;session 表层的投影始终保持逐字透传。 diff --git a/.agents/skills/dsh-doc-site-sync/SKILL.md b/.agents/skills/dsh-doc-site-sync/SKILL.md new file mode 100644 index 0000000000..bee1b0dfe8 --- /dev/null +++ b/.agents/skills/dsh-doc-site-sync/SKILL.md @@ -0,0 +1,82 @@ +--- +name: dsh-doc-site-sync +description: Use when publishing, updating, moving, or removing DeepSeek Harness documentation website pages; editing website/docs.ts mappings or navigation; diagnosing a page missing from the VitePress site; fixing projected documentation links; or running the docs:dev, docs:check, and doc-sync workflow after website-content changes. +--- + +# Synchronizing the DeepSeek Harness Documentation Site + +Keep repository Markdown as the only editable content source. Treat the website as a tested projection: [website/docs.ts](../../../website/docs.ts) selects public pages, [scripts/project-doc-site.ts](../../../scripts/project-doc-site.ts) rewrites them into the disposable `website/.generated/` tree, and VitePress builds that tree. + +Repository translations follow the sibling pairing contract: English `foo.md`, Chinese `foo.zh.md`, and `foo.i18n.yaml` live together. Never create `zh-CN/` or other locale directories for website content. The site route trees are independent of that source layout: `foo.zh.md` projects to the root route and `foo.md` projects to the matching `/en/` route. + +## Read the owning contracts + +- Read [docs/AGENTS.md](../../../docs/AGENTS.md) and use [dsh-doc-standards](../dsh-doc-standards/SKILL.md) when deciding where content belongs or changing product documentation prose. +- Use [dsh-translate-docs](../dsh-translate-docs/SKILL.md) whenever an edited source has a bilingual counterpart. +- Read the current `DocsPage` type and entries in [website/docs.ts](../../../website/docs.ts) before changing the manifest; do not rely on a remembered field set. +- Read [website/.vitepress/config.ts](../../../website/.vitepress/config.ts) before adding a new section, sidebar collection, locale, or top-level navigation item. + +## Classify the change + +- **Edit an already published page:** change only its canonical Markdown source. Do not touch the manifest unless its route or navigation metadata changes. +- **Publish a new page:** create it in its owning `docs/` tier, then add one manifest entry. +- **Rename, move, or remove a page:** update the canonical file, manifest entry, and inbound repository links atomically. Remove stale manifest entries; `docs:check` rejects missing sources. +- **Publish a generated catalog:** map the generated `docs/` file, but change its generator or source metadata rather than editing the catalog by hand. +- **Change site structure:** update the manifest for ordinary pages; update VitePress configuration only when the existing sidebar, section, or locale model cannot express the change. + +Never edit or commit `website/.generated/`, `website/.cache/`, or `website/.dist/`. Never copy a maintained `docs/` page into `website/`. + +## Add or update a manifest entry + +Set every `DocsPage` field deliberately: + +- `source`: repository-relative canonical Markdown path. For a complete bilingual pair, add the English `.md` path through `pairedPages()`; it derives the sibling `.zh.md`, the content locales, and counterpart aliases. +- `route`: public VitePress path including the `.md` suffix. +- `label`: sidebar label, not necessarily the document H1. +- `sidebar`: reuse `zh-guide`, `zh-develop`, or `en-docs` unless the information architecture genuinely needs another collection. +- `section`: reuse an existing section when possible. If adding one, also place it in `sectionOrder` in the VitePress config. +- `order`: stable order within the section. +- `sourceAliases`: optional additional repository paths that should resolve to this page when links are projected. It does not create another public route. + +Use `mirroredPages()` only for a source that intentionally falls back to the same available language in both route trees. Convert that entry to `pairedPages()` when its counterpart is added. Keep the manifest an explicit public allowlist. Do not publish RFCs, postmortems, testing guides, `AGENTS.md`, or maintainer workflows merely because they exist under `docs/`; add internal material only when the user explicitly changes the publication boundary. + +## Preserve link behavior + +Write normal repository-relative Markdown links in canonical docs. The projector applies these rules: + +- A target present in the manifest becomes a site-relative route. +- An existing target outside the manifest becomes a GitHub source link, including supported line suffixes. +- External URLs, site-absolute URLs, email links, and fragment-only links remain unchanged. +- A missing repository-relative target fails projection instead of silently producing a broken link. + +Do not write website-specific routes into canonical Markdown just to satisfy VitePress. Use `sourceAliases` for directory-style repository links that should resolve to a mapped index page. + +## Preview and validate + +Run local preview while editing: + +```sh +pnpm docs:dev +``` + +The dev server watches mapped source files and reprojects them. Restart it after changing the manifest if the new source is not picked up automatically. + +Run the focused website gate before treating the mapping as valid: + +```sh +pnpm docs:check +``` + +Before committing a documentation-site change, run: + +```sh +pnpm run doc-sync +pnpm run lint +git diff --check +``` + +Use [dsh-pre-push-checks](../dsh-pre-push-checks/SKILL.md) before pushing. Report the canonical files changed, manifest entries added or removed, public routes affected, and the exact checks run. + +## Keep deployment separate + +Synchronizing content into the VitePress build does not publish it to the internet. Do not add GitHub Pages permissions, deployment workflows, custom domains, or public hosting unless the user explicitly requests deployment and confirms the hosting policy. diff --git a/.agents/skills/dsh-doc-site-sync/agents/openai.yaml b/.agents/skills/dsh-doc-site-sync/agents/openai.yaml new file mode 100644 index 0000000000..9f4909f258 --- /dev/null +++ b/.agents/skills/dsh-doc-site-sync/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "DSH Documentation Site Sync" + short_description: "Publish repository docs through the DSH website manifest" + default_prompt: "Use $dsh-doc-site-sync to publish or update a DeepSeek Harness documentation page on the website." diff --git a/.agents/skills/dsh-translate-docs/SKILL.md b/.agents/skills/dsh-translate-docs/SKILL.md index d935c5c4b7..8786eb5e6b 100644 --- a/.agents/skills/dsh-translate-docs/SKILL.md +++ b/.agents/skills/dsh-translate-docs/SKILL.md @@ -1,10 +1,16 @@ --- name: dsh-translate-docs -description: Use when creating or updating the bilingual counterpart of a doc in this repo (English ↔ Chinese pairs) — orients the translator to the pairing contract, the terminology source of truth, the translation rules, and the consistency gate that verifies the result +description: Use when creating or updating the bilingual counterpart of a doc in this repo (English ↔ Chinese pairs) — tells the orchestrating agent when to delegate translation to a subagent, and orients the translator to the pairing contract, the terminology source of truth, the translation rules, and the consistency gate that verifies the result --- # Translating DeepSeek-Harness docs +## Delegate to a subagent + +When this skill fires and translations need to be written, do not translate yourself: spawn a subagent to do the translation work. If you are that delegated subagent, skip this section; the sections from here on address the agent actually writing the translation. + +## What this skill is + **This skill is guidance, not a translation memory.** It is the workflow map for keeping `foo.md ↔ foo.zh.md` pairs consistent and natural in both languages. Both languages carry equal authority — a change is authored in either one, and that side is the source for that update. You are the translator: the rules below say what must hold, not how to phrase any particular sentence — phrasing judgment is yours, terminology is not. ## Sources of truth (read, don't re-summarize) diff --git a/AGENTS.md b/AGENTS.md index 87f46c88a0..24a138e396 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ examples/ Runnable cordis.yml leaves over packages/examples bundles (see exam .agents/ Agent workflows and Agent Notes (`notes/`) docs/ architecture, generated catalogs, postmortems, cookbook (see docs/AGENTS.md) scripts/ repo gates and generators -website/ VitePress docs site (zh-CN); api/ pages generated from source +website/ VitePress projection of selected bilingual docs/ sources ``` Package groups: [packages/README.md](packages/README.md). diff --git a/docs/AGENTS.md b/docs/AGENTS.md index a33e9738fc..da0c03b088 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -15,9 +15,10 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home; | [Agent Notes](../.agents/notes/README.md) | Decision records: the why, what-was-given-up, and concise verification contract; `implemented/` notes describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped | | [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — | | [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the Agent Note each guide links) | +| [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history | | Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns | | [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), gate-by-gate enumerations that drift from `package.json` scripts | -| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind | +| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [Cordis core API](cordis-catalog/core/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind | | Skills (`.agents/skills/`) | Reusable workflows and specialized decision standards | Product and runtime contracts (→ docs or source) | Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type shapes → core data; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link. diff --git a/docs/agent-lifecycle.md b/docs/agent-lifecycle.md index 888b24043a..2134c8b355 100644 --- a/docs/agent-lifecycle.md +++ b/docs/agent-lifecycle.md @@ -64,7 +64,7 @@ sequenceDiagram The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set. -`dsh-compact-basic` uses `agent/post-step` for pressure after those durable facts and `agent/request-error` only for canonical context overflow. Recovery compacts between the closed failed step and a fresh retry step, and returns retry only when the surface replacement generation advances; otherwise the original request error remains authoritative. +`dsh-compact-basic` uses `agent/post-step` for pressure after those durable facts and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and a fresh retry step, and returns retry only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative. SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors. diff --git a/docs/architecture.md b/docs/architecture.md index 6ef426b28d..9a83346924 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -27,11 +27,12 @@ A harness is one [Cordis](cordis-primer.md) context. Packages add services (`ctx | `ctx.tokenMeter` | [`llm/token-meter`](../packages/llm/token-meter/README.md) | singleton replay-aware request/surface pressure | | `ctx.bash` | [`bash/`](../packages/bash/README.md) | foreground/background command execution | | `ctx.sandbox` | [`sandbox/`](../packages/sandbox/README.md) | same-world process confinement (argv wrapping, per-call policy) | +| `ctx.sandboxPolicy` | [`sandbox/`](../packages/sandbox/README.md) | shared sandbox policy home | | `ctx.codeRuntime` | [`code-runtime/`](../packages/code-runtime/README.md) | model-written program execution | | `ctx.fs` | [`fs/`](../packages/fs/README.md) | filesystem provider primitives and policy events | | `ctx.skills` | [`skill/`](../packages/skill/README.md) | skill provider registry and progressive disclosure | | `ctx.web` | [`web/`](../packages/web/README.md) | search/fetch provider registries | -| `ctx.compact` | [`compact/`](../packages/compact/README.md) | session-log compaction | +| `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.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 | @@ -55,11 +56,11 @@ Waterfall events behave like around-middleware: a listener delegates by calling ## Default Loop Lifecycle -The shipped loop drains prompts through checkpoints; every pause is exposed to plugins through services or events. +The shipped loop drains prompt-to-checkpoint work through plugin-visible services and events. -A **turn** drains queued input in an append-only session until the model requests no more tools or plugin continuation. A **step** is one model request plus its tool executions. In the flow below ([sequence companion](agent-lifecycle.md)), quoted names are durable session events and event names are extension points. +A **session** is an append-only log. Each ordinary **turn** claims one queued `send()` item; injection claims none. A claimed `send()` successor awaits the preceding claimed ordinary turn's checkpoint but may share its `running` interval ([decision](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md)). A turn ends when model and plugins stop it. A **step** is one model request plus tools. Below ([sequence companion](agent-lifecycle.md)), quotes mark durable events; other names are extension points. -Startup resolves identity. No id mints `-session-`; `sessionId` resumes or creates; `resumeSessionId` requires history. Active failures emit `agent-loop/config-start-failed(sessionId, error)`, so front doors reject work; teardown stays silent. +No id mints `-session-`; `sessionId` resumes/creates; `resumeSessionId` needs history. Resume restores lineage, seeds, and delegation depth pre-publication. Failures emit `agent-loop/config-start-failed(sessionId, error)`; front doors reject; teardown stays silent. ### Turn Flow @@ -69,13 +70,13 @@ choose declarative identity and fresh/resume path -> enter session + agent -> session/created -> agent/created -> enable driving -> agent/session-start(source) -> start driver forever: - wait for queued messages + wait for a queued message emit agent/status(running) TURN: 'turn/start' - each queued message -> agent/prompt-submit + claimed message -> agent/prompt-submit allowed prompt -> 'user/message' plus injected context - every prompt blocked -> 'turn/end'(rejected) + blocked prompt -> 'prompt/blocked' -> 'turn/end'(rejected) STEP loop: drain steering assemble system prompt and tool schemas @@ -111,7 +112,7 @@ Each step assembles ordered prompt sections, tool schemas, and `{{name}}` variab Tool-time context—including async `agent.inject()` notices and post-tool `additionalContexts`—settles after recorded results. Steering drains before `agent/post-step`, which observes durable output, results, context, and steering before signal closure. Leftovers become queued input. Terminal `agent/turn-stop` runs after continuation and steering folding, remains authoritative through turn close and flush, and discards later steering while preserving queued prompts. -`dsh-compact-basic` handles pressure and canonical overflow at checkpoints; retry requires a balanced surface replacement ([decision](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)). +Optional pruning precedes summaries; retry requires durable surface progress; cancellation wins ([decision](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)). ### Failure Boundaries diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 64ddcc40c5..f6ab9eec3c 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -16,6 +16,8 @@ flowchart LR pkg_compact_basic["compact-basic"] pkg_token_meter["token-meter"] svc_tokenMeter["ctx.tokenMeter
Replay token measurement"] + pkg_compact_tool_result_prune["compact-tool-result-prune"] + svc_toolResultPrune["ctx.toolResultPrune
Model-free tool-result pruning"] pkg_session["session"] svc_sessions["ctx.sessions
In-memory session store"] pkg_agent["agent"] @@ -62,6 +64,9 @@ flowchart LR pkg_sandbox["sandbox"] svc_sandbox["ctx.sandbox
Process-sandbox seam"] pkg_sandbox_local["sandbox-local"] + pkg_sandbox_policy["sandbox-policy"] + svc_sandboxPolicy["ctx.sandboxPolicy
Sandbox policy home"] + pkg_fs_sandbox["fs-sandbox"] pkg_approval["approval"] svc_approval["ctx.approval
Approval seam"] pkg_permission["permission"] @@ -109,8 +114,10 @@ flowchart LR pkg_code_runtime_worker --> svc_codeRuntime pkg_compact --> svc_compact pkg_compact_basic --> svc_compact + pkg_compact_tool_result_prune --> svc_toolResultPrune pkg_fs --> svc_fs pkg_fs_local --> svc_fs + pkg_fs_sandbox --> svc_fs pkg_goal --> svc_goals pkg_llm --> svc_llm pkg_llm_deepseek --> svc_llm @@ -119,6 +126,7 @@ flowchart LR pkg_permission --> svc_permission pkg_sandbox --> svc_sandbox pkg_sandbox_local --> svc_sandbox + pkg_sandbox_policy --> svc_sandboxPolicy pkg_session --> svc_sessions pkg_session_persistence --> svc_sessionPersistence pkg_session_persistence_jsonl --> svc_sessionPersistence @@ -165,6 +173,8 @@ flowchart LR svc_llm --> pkg_compact_basic svc_permission --> pkg_acp svc_sandbox --> pkg_bash_sandbox + svc_sandboxPolicy --> pkg_bash_sandbox + svc_sandboxPolicy --> pkg_fs_sandbox svc_sessionPersistence --> pkg_acp svc_sessionPersistence --> pkg_agent_loop svc_sessionPersistence --> pkg_hooks_claude @@ -189,6 +199,7 @@ flowchart LR svc_tasks --> pkg_tool_subagent svc_tasks --> pkg_tool_tasks svc_tokenMeter --> pkg_compact_basic + svc_toolResultPrune --> pkg_compact_basic svc_tools --> pkg_acp svc_tools --> pkg_agent_loop svc_tools --> pkg_tool_ask_user @@ -211,6 +222,7 @@ flowchart LR | --- | --- | --- | --- | --- | --- | --- | | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compact-basic`](../packages/compact/compact-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. | | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. | +| `ctx.toolResultPrune` | `core` | [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | - | [`compact-basic`](../packages/compact/compact-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. | | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`cli-demo`](../packages/examples/cli-demo), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. | | `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session-persistence/session-persistence) | [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl), [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`acp`](../packages/ui/acp), [`session-query`](../packages/session-query/session-query) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. | | `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | - | - | - | Resolves live and optional persisted logs into one logical corpus for exact reads and relationship traces. | @@ -224,10 +236,11 @@ flowchart LR | `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them. | | `ctx.bashEnv` | `core` | [`tool-bash`](../packages/bash/tool-bash) | - | - | - | Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace. | | `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. | +| `ctx.sandboxPolicy` | `core` | [`sandbox-policy`](../packages/sandbox/sandbox-policy) | - | [`bash-sandbox`](../packages/bash/bash-sandbox), [`fs-sandbox`](../packages/fs/fs-sandbox) | - | The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots. | | `ctx.approval` | `seam` | `approval` | [`acp`](../packages/ui/acp) | [`tools`](../packages/core/tools), [`tool-bash`](../packages/bash/tool-bash) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. | | `ctx.permission` | `core` | [`permission`](../packages/ui/permission) | - | [`acp`](../packages/ui/acp) | - | User-facing preset table (`workspace-write`/`danger-full-access`) bundling the sandbox-mode and approval-policy knobs; a switch writes one `permission/preset` event through to both knob events. | | `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | [`tools`](../packages/core/tools) | - | Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the tool registry consumes it for Code Mode). | -| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-policy contributes observed-state checks through the fs/* event gate. | +| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate. | | `ctx.compact` | `seam` | [`compact`](../packages/compact/compact) | [`compact-basic`](../packages/compact/compact-basic) | [`compact-basic`](../packages/compact/compact-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; a model-facing compact tool remains deferred. | | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn`](../packages/subagent/subagent-spawn), [`subagent-fork`](../packages/subagent/subagent-fork), [`subagent-acp`](../packages/subagent/subagent-acp) | [`tool-subagent`](../packages/subagent/tool-subagent) | - | Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name. | | `ctx.tasks` | `core` | [`tasks`](../packages/tasks/tasks) | - | [`tool-bash`](../packages/bash/tool-bash), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (tool-bash background commands, tool-subagent background delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it. | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index e824c12150..3ff6158354 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -185,28 +185,21 @@ Source: [`packages/bash/bash-local/src/index.ts:17`](../packages/bash/bash-local ## `@deepseek-ai/dsh-bash-sandbox` -Requires: `sandbox` +Requires: `sandbox` · `sandboxPolicy` ```ts config-catalog /** - * Plugin config: the local executor's knobs plus the sandbox policy. All - * optional — `static Config` supplies the defaults (`mode: 'read-only'` is the - * fail-safe default; an example that wants a workspace-writable agent opts in - * explicitly). The runner choice is not configured here: which platform - * backend confines the command is the `ctx.sandbox` provider's config. + * Plugin config: the local executor's knobs, verbatim. The sandbox policy — + * the default mode and the `workspace-write` boundary root — is NOT here: it + * lives on `ctx.sandboxPolicy` (`@deepseek-ai/dsh-sandbox-policy`), the one + * home both enforcing families read, so bash and fs can never confine to + * different roots. The runner choice is likewise the `ctx.sandbox` provider's + * config, not this executor's. */ -export interface Config extends LocalConfig { - /** File-sandbox mode commands run under (default: `read-only`). */ - mode?: SandboxMode - /** - * Root directory `workspace-write` mode may write under (default: the - * executor's default working directory — `cwd`, else `process.cwd()`). - */ - workspaceRoot?: string -} +export type Config = LocalConfig ``` -Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local) · [`SandboxMode`](core-data-structures/sandbox.md) +Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local) Source: [`packages/bash/bash-sandbox/src/index.ts:27`](../packages/bash/bash-sandbox/src/index.ts) @@ -310,6 +303,22 @@ export interface BasicCompactConfig { Source: [`packages/compact/compact-basic/src/types.ts:8`](../packages/compact/compact-basic/src/types.ts) +## `@deepseek-ai/dsh-compact-tool-result-prune` + +```ts config-catalog +/** Character-budget policy for deterministic tool-result pruning. */ +export interface ToolResultPruneConfig { + /** Prune when total text exceeds this many Unicode code points. Defaults to `8192`. */ + thresholdChars?: number + /** Maximum leading Unicode code points retained. Defaults to `4096`. */ + headChars?: number + /** Maximum trailing Unicode code points retained. Defaults to `1024`. */ + tailChars?: number +} +``` + +Source: [`packages/compact/compact-tool-result-prune/src/types.ts:4`](../packages/compact/compact-tool-result-prune/src/types.ts) + ## `@deepseek-ai/dsh-fs-local` ```ts config-catalog @@ -322,6 +331,24 @@ export interface Config { Source: [`packages/fs/fs-local/src/index.ts:38`](../packages/fs/fs-local/src/index.ts) +## `@deepseek-ai/dsh-fs-sandbox` + +Requires: `sandboxPolicy` + +```ts config-catalog +/** + * Plugin config: the local backend's knobs, verbatim (only `cwd`, the resolve + * base for relative paths). The sandbox default (mode + `workspace-write` + * boundary root) is NOT here — it lives on `ctx.sandboxPolicy`, the one home + * both enforcing families share. + */ +export type Config = LocalConfig +``` + +Depends on: [`LocalConfig`](#deepseek-aidsh-fs-local) + +Source: [`packages/fs/fs-sandbox/src/index.ts:49`](../packages/fs/fs-sandbox/src/index.ts) + ## `@deepseek-ai/dsh-goal` Requires: `agents` @@ -612,7 +639,7 @@ export interface Config { /** One preset's sandbox/approval bundle and optional client presentation. */ export interface PresetSpec { - /** The `bash/sandbox-mode` value the preset writes through. */ + /** The `sandbox/mode` value the preset writes through. */ sandbox: SandboxMode /** The `approval/policy` value the preset writes through. */ approval: ApprovalPolicy @@ -625,7 +652,7 @@ export interface PresetSpec { Depends on: [`ApprovalPolicy`](core-data-structures/approval.md) · [`SandboxMode`](core-data-structures/sandbox.md) -Source: [`packages/ui/permission/src/index.ts:80`](../packages/ui/permission/src/index.ts) +Source: [`packages/ui/permission/src/index.ts:83`](../packages/ui/permission/src/index.ts) ## `@deepseek-ai/dsh-repeat-tool-guard` @@ -687,6 +714,31 @@ export interface Config { Source: [`packages/sandbox/sandbox-local/src/index.ts:19`](../packages/sandbox/sandbox-local/src/index.ts) +## `@deepseek-ai/dsh-sandbox-policy` + +```ts config-catalog +/** + * Plugin config: the deployment's sandbox default. All optional — `Config` + * supplies the defaults (`mode: 'read-only'` is the fail-safe default; a + * deployment that wants a workspace-writable agent opts in explicitly). The + * runner choice is NOT here (it is the `ctx.sandbox` provider's config), nor + * is any per-family knob: this is the one shared policy home. + */ +export interface Config { + /** File-sandbox mode a session starts from (default: `read-only`). */ + mode?: SandboxMode + /** + * Absolute root directory `workspace-write` may write under (default: + * `process.cwd()`). Both enforcing families fence against this SAME root. + */ + workspaceRoot?: string +} +``` + +Depends on: [`SandboxMode`](core-data-structures/sandbox.md) + +Source: [`packages/sandbox/sandbox-policy/src/index.ts:44`](../packages/sandbox/sandbox-policy/src/index.ts) + ## `@deepseek-ai/dsh-session-persistence-jsonl` Requires: `sessions` @@ -1042,7 +1094,7 @@ export interface Config { } ``` -Source: [`packages/bash/tool-bash/src/index.ts:39`](../packages/bash/tool-bash/src/index.ts) +Source: [`packages/bash/tool-bash/src/index.ts:40`](../packages/bash/tool-bash/src/index.ts) ## `@deepseek-ai/dsh-tool-cordis` @@ -1080,7 +1132,7 @@ export interface Config { } ``` -Source: [`packages/fs/tool-fs/src/index.ts:22`](../packages/fs/tool-fs/src/index.ts) +Source: [`packages/fs/tool-fs/src/index.ts:24`](../packages/fs/tool-fs/src/index.ts) ## `@deepseek-ai/dsh-tool-fs-search` @@ -1149,8 +1201,7 @@ export interface Config { /** * Tool filter applied to every child. Filtered tools disappear from its * prompt and reject execution. Requires the provider's `toolFilter` - * capability; unknown names fail startup. Children otherwise see this tool, - * so deny it or set `maxDepth` to bound recursion. + * capability; unknown names fail startup. */ toolFilter?: { /** Global tool names the child keeps; everything else is removed. */ @@ -1159,10 +1210,15 @@ export interface Config { deny?: string[] } /** - * Maximum child depth. Requires the provider's `depthLimit` capability and a - * non-negative safe integer. Omission is unbounded. + * Maximum child depth: a non-negative safe integer (default `3`; `0` forbids + * delegation entirely), or `'provider-managed'` to send no cap. A numeric cap + * requires the provider's `depthLimit` capability (mount fails loud + * otherwise). The provider checks the calling agent's current depth at every + * start; the tool remains model-visible so runtime policy owns rejection. + * `'provider-managed'` is for an out-of-process provider (ACP) whose + * recursion budget belongs to the child harness's own deployment. */ - maxDepth?: number + maxDepth?: number | 'provider-managed' } ``` diff --git a/website/zh-CN/api/cordis/context.md b/docs/cordis-catalog/core/context.md similarity index 78% rename from website/zh-CN/api/cordis/context.md rename to docs/cordis-catalog/core/context.md index 0fbb2fcc70..f6b249c738 100644 --- a/website/zh-CN/api/cordis/context.md +++ b/docs/cordis-catalog/core/context.md @@ -1,17 +1,19 @@ - + # Context -The context is the core cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods (`ctx.on`, `ctx.emit`, …) are documented on [Events](./events.md); `ctx.effect` and `ctx.fiber` on [Fiber](./fiber.md); `ctx.plugin` and `ctx.inject` on [Registry](./registry.md). +The context is the core Cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods are documented on [Events](events.md), effects and the current fiber on [Fiber](fiber.md), and plugin loading on [Registry](registry.md). Root and child dependency containers for Cordis plugins. + A context is a proxy: normal property reads go through the service resolver, while `extend()`, `isolate()`, and `intercept()` create scoped child contexts without mutating their parent. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L42) +[Source](../../../vendor/cordis/src/context.ts#L42) ### ctx.extend(meta?) -```ts website-api +```ts cordis-catalog /** * Create a child context with extra metadata on top of the current scope. * @@ -25,17 +27,18 @@ extend(meta = {}): this ``` Create a child context with extra metadata on top of the current scope. + The child prototypally inherits every property of this context; own properties of `meta` shadow the inherited ones. The parent is not mutated. - `meta` — own properties (including symbol keys) to define on the child. **Returns** a child context inheriting from this one. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L99) +[Source](../../../vendor/cordis/src/context.ts#L99) ### ctx.isolate(name, label?) -```ts website-api +```ts cordis-catalog /** * Create a child context with an independent service scope for `name`. * @@ -52,6 +55,7 @@ isolate(name: string, label?: symbol) ``` Create a child context with an independent service scope for `name`. + Below the returned context, reads and writes of the service `name` resolve against the new label instead of the parent's, so a different implementation can be provided without affecting the parent scope. Passing the same `label` to two `isolate()` calls joins their scopes. - `name` — the service name to isolate. @@ -59,11 +63,11 @@ Below the returned context, reads and writes of the service `name` resolve again **Returns** a child context whose `name` service resolves in the new scope. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L121) +[Source](../../../vendor/cordis/src/context.ts#L121) ### ctx.intercept(name, config) -```ts website-api +```ts cordis-catalog /** * Add service-specific intercept config for plugins started below this * context. @@ -81,6 +85,7 @@ intercept(name: string, config: any): this ``` Add service-specific intercept config for plugins started below this context. + Plugins loaded under the returned context see `config` merged into the service's resolved config (ancestor entries first; see `Service[symbols.resolveConfig]`). The parent context is not affected. - `name` — the service name whose config to intercept. @@ -88,123 +93,123 @@ Plugins loaded under the returned context see `config` merged into the service's **Returns** a child context carrying the additional intercept entry. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L139) +[Source](../../../vendor/cordis/src/context.ts#L139) ### ctx.root -```ts website-api +```ts cordis-catalog /** The root context of the application (every child context shares it). @experimental */ root: this ``` The root context of the application (every child context shares it). @experimental -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L22) +[Source](../../../vendor/cordis/src/context.ts#L22) ### ctx.baseUrl -```ts website-api +```ts cordis-catalog /** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */ baseUrl?: string ``` Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L24) +[Source](../../../vendor/cordis/src/context.ts#L24) ### ctx.events -```ts website-api +```ts cordis-catalog /** The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). */ events: EventsService ``` The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L26) +[Source](../../../vendor/cordis/src/context.ts#L26) ### ctx.logger -```ts website-api +```ts cordis-catalog /** The logging service. Call `ctx.logger(name)` for a named logger. */ logger: LoggerService ``` The logging service. Call `ctx.logger(name)` for a named logger. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L28) +[Source](../../../vendor/cordis/src/context.ts#L28) ### ctx.reflect -```ts website-api +```ts cordis-catalog /** The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). */ reflect: ReflectService ``` The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L30) +[Source](../../../vendor/cordis/src/context.ts#L30) ### ctx.registry -```ts website-api +```ts cordis-catalog /** The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). */ registry: RegistryService ``` The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L32) +[Source](../../../vendor/cordis/src/context.ts#L32) ## Static members ### Context.effect -```ts website-api +```ts cordis-catalog /** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */ static readonly effect: unique symbol ``` Symbol key under which a disposer exposes its EffectMeta diagnostics tree. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L44) +[Source](../../../vendor/cordis/src/context.ts#L44) ### Context.filter -```ts website-api +```ts cordis-catalog /** Symbol key for a context's listener filter, consulted on every event dispatch. */ static readonly filter: unique symbol ``` Symbol key for a context's listener filter, consulted on every event dispatch. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L46) +[Source](../../../vendor/cordis/src/context.ts#L46) ### Context.isolate -```ts website-api +```ts cordis-catalog /** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */ static readonly isolate: unique symbol ``` Symbol key of the isolation map (see the `Context[symbols.isolate]` property). -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L48) +[Source](../../../vendor/cordis/src/context.ts#L48) ### Context.intercept -```ts website-api +```ts cordis-catalog /** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */ static readonly intercept: unique symbol ``` Symbol key of the intercept map (see the `Context[symbols.intercept]` property). -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L50) +[Source](../../../vendor/cordis/src/context.ts#L50) ### Context.is(value) -```ts website-api +```ts cordis-catalog /** * Returns true for Cordis context proxies and context prototypes. * @@ -218,19 +223,20 @@ static is(value: any): value is Context ``` Returns true for Cordis context proxies and context prototypes. + Works across realms and across multiple copies of cordis, because the brand is keyed by a global symbol rather than by `instanceof`. - `value` — the value to test. **Returns** `true` if `value` is a Cordis context, narrowing its type. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/context.ts#L61) +[Source](../../../vendor/cordis/src/context.ts#L61) ## Service store and mixins ### ctx.get(name, strict?) -```ts website-api +```ts cordis-catalog /** * Read a service from the store without the inject requirement. * @@ -250,11 +256,11 @@ Read a service from the store without the inject requirement. **Returns** the service value, or `undefined` when not (yet) provided. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L16) +[Source](../../../vendor/cordis/src/reflect.ts#L16) ### ctx.set(name, value) -```ts website-api +```ts cordis-catalog /** * Overwrite a provided service's value. * @@ -269,16 +275,17 @@ set(name: string, value: any): void ``` Overwrite a provided service's value. + Only the fiber that provided the service may set it; setting an unprovided name throws. - `name` — the service name. - `value` — the new service value. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L28) +[Source](../../../vendor/cordis/src/reflect.ts#L28) ### ctx.provide(name, value) -```ts website-api +```ts cordis-catalog /** * Register a service implementation owned by the current fiber. * @@ -296,6 +303,7 @@ provide(name: string, value?: any): () => void ``` Register a service implementation owned by the current fiber. + The service becomes visible to dependents in the same isolation scope once the fiber is active; it is unregistered (waking dependents) when the returned disposer runs or the fiber unloads. Throws if the name is already provided in this scope or declared as an accessor. - `name` — the service name. @@ -303,11 +311,11 @@ The service becomes visible to dependents in the same isolation scope once the f **Returns** a disposer that unregisters the service. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L43) +[Source](../../../vendor/cordis/src/reflect.ts#L43) ### ctx.accessor(name, options) -```ts website-api +```ts cordis-catalog /** * Define a computed context property backed by get/set hooks. * @@ -321,16 +329,17 @@ accessor(name: string, options: Omit): void ``` Define a computed context property backed by get/set hooks. + The accessor is removed when the current fiber unloads. Throws if the name is already declared. - `name` — the context property name. - `options` — the `get` hook and optional `set` hook. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L55) +[Source](../../../vendor/cordis/src/reflect.ts#L55) ### ctx.mixin(name, mixins) -```ts website-api +```ts cordis-catalog /** * Expose selected members of a service directly on `ctx`. * @@ -346,9 +355,10 @@ mixin(source: T, mixins: (keyof this & keyof T)[] | Dict): ``` Expose selected members of a service directly on `ctx`. + Each mixed-in key becomes an accessor that forwards to the service (binding methods to it), so e.g. `ctx.on` forwards to `ctx.events.on`. Mixins are removed when the current fiber unloads. - `name` — the context property holding the source service. - `mixins` — keys to forward, or a source-key → ctx-key map. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/reflect.ts#L66) +[Source](../../../vendor/cordis/src/reflect.ts#L66) diff --git a/website/zh-CN/api/cordis/events.md b/docs/cordis-catalog/core/events.md similarity index 82% rename from website/zh-CN/api/cordis/events.md rename to docs/cordis-catalog/core/events.md index 77488b5d24..2fb64e78a2 100644 --- a/website/zh-CN/api/cordis/events.md +++ b/docs/cordis-catalog/core/events.md @@ -1,12 +1,13 @@ - + # Events -The event system mixed into every context. Harness-defined events are cataloged on [Harness events](../harness/events.md). +The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated separately in the [Cordis events catalog](../events.md). ### ctx.parallel(name, ...args) -```ts website-api +```ts cordis-catalog /** * Dispatch an event, running all listeners concurrently. * @@ -25,11 +26,11 @@ Dispatch an event, running all listeners concurrently. **Returns** a promise resolving once every listener has settled. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L43) +[Source](../../../vendor/cordis/src/events.ts#L43) ### ctx.emit(name, ...args) -```ts website-api +```ts cordis-catalog /** * Dispatch an event synchronously, ignoring listener return values. * @@ -45,11 +46,11 @@ Dispatch an event synchronously, ignoring listener return values. - `name` — the event name. - `args` — arguments passed to every listener. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L52) +[Source](../../../vendor/cordis/src/events.ts#L52) ### ctx.serial(name, ...args) -```ts website-api +```ts cordis-catalog /** * Dispatch an event, awaiting listeners in order until one bails. * @@ -68,11 +69,11 @@ Dispatch an event, awaiting listeners in order until one bails. **Returns** the first bail value (non-null, non-false, non-undefined), if any. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L62) +[Source](../../../vendor/cordis/src/events.ts#L62) ### ctx.bail(name, ...args) -```ts website-api +```ts cordis-catalog /** * Dispatch an event, calling listeners in order until one bails. * @@ -91,11 +92,11 @@ Dispatch an event, calling listeners in order until one bails. **Returns** the first bail value (non-null, non-false, non-undefined), if any. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L72) +[Source](../../../vendor/cordis/src/events.ts#L72) ### ctx.waterfall(name, ...args) -```ts website-api +```ts cordis-catalog /** * Dispatch an event whose last argument is a `next` continuation. * @@ -111,6 +112,7 @@ waterfall(thisArg: NoInfer>, name: K ``` Dispatch an event whose last argument is a `next` continuation. + Each listener wraps the rest of the chain: calling `next()` invokes the next listener (finally the built-in behavior); not calling it vetoes. - `name` — the event name. @@ -118,11 +120,11 @@ Each listener wraps the rest of the chain: calling `next()` invokes the next lis **Returns** the outermost listener's return value. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L85) +[Source](../../../vendor/cordis/src/events.ts#L85) ### ctx.on(name, listener, options?) -```ts website-api +```ts cordis-catalog /** * Register an event listener owned by the current fiber. * @@ -142,11 +144,11 @@ Register an event listener owned by the current fiber. **Returns** a disposer removing the listener; `true` if it was still registered. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L96) +[Source](../../../vendor/cordis/src/events.ts#L96) ### ctx.once(name, listener, options?) -```ts website-api +```ts cordis-catalog /** * Same as `on()`, but the listener disposes itself after its first call. * @@ -166,13 +168,13 @@ Same as `on()`, but the listener disposes itself after its first call. **Returns** a disposer removing the listener; `true` if it was still registered. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L105) +[Source](../../../vendor/cordis/src/events.ts#L105) ## EventOptions Options accepted by `ctx.on()` and `ctx.once()`. -```ts website-api +```ts cordis-catalog /** Options accepted by `ctx.on()` and `ctx.once()`. */ interface EventOptions { /** Add the listener before existing listeners for the same event. */ @@ -182,14 +184,15 @@ interface EventOptions { } ``` -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L111) +[Source](../../../vendor/cordis/src/events.ts#L111) ## DispatchMode Event dispatch strategy used by the event service. + `emit` runs synchronous listeners without awaiting them, `parallel` awaits all listeners together, `serial` awaits them in order until one bails, `bail` stops on the first synchronous bail value, and `waterfall` composes listeners around a final `next` callback. -```ts website-api +```ts cordis-catalog /** * Event dispatch strategy used by the event service. * @@ -201,4 +204,4 @@ Event dispatch strategy used by the event service. type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall' ``` -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/events.ts#L31) +[Source](../../../vendor/cordis/src/events.ts#L31) diff --git a/website/zh-CN/api/cordis/fiber.md b/docs/cordis-catalog/core/fiber.md similarity index 77% rename from website/zh-CN/api/cordis/fiber.md rename to docs/cordis-catalog/core/fiber.md index f79adbaf20..d865ce01fc 100644 --- a/website/zh-CN/api/cordis/fiber.md +++ b/docs/cordis-catalog/core/fiber.md @@ -1,12 +1,13 @@ - + # Fiber -A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber; `ctx.effect()` delegates to it. +A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber, and `ctx.effect()` delegates to it. ### ctx.effect(execute, label?) -```ts website-api +```ts cordis-catalog /** * Register a cleanup-aware effect on this fiber. * @@ -25,6 +26,7 @@ effect(execute: () => Effect, label?: string): AsyncDisposable> ``` Register a cleanup-aware effect on this fiber. + `execute` runs immediately; the disposers it produces are collected and run (in reverse order) either when the returned disposer is called or when the fiber unloads, whichever comes first. Calling the disposer twice is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed, and `TypeError` if `execute` returns an invalid shape. - `execute` — the effect body; see `Effect` for accepted shapes. @@ -32,117 +34,118 @@ Register a cleanup-aware effect on this fiber. **Returns** a disposer that tears the effect down and settles once done. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L419) +[Source](../../../vendor/cordis/src/fiber.ts#L419) ### ctx.fiber -```ts website-api +```ts cordis-catalog /** The fiber (plugin runtime instance) that owns this context. */ fiber: Fiber ``` The fiber (plugin runtime instance) that owns this context. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L11) +[Source](../../../vendor/cordis/src/fiber.ts#L11) ## The Fiber class Runtime instance of one plugin application. + A fiber tracks dependency state, validated config, lifecycle effects, and cleanup for the plugin context returned by `ctx.plugin()`. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L183) +[Source](../../../vendor/cordis/src/fiber.ts#L183) ### fiber.uid -```ts website-api +```ts cordis-catalog /** Unique id within the registry; 0 for the root fiber, `null` once disposed. */ public uid: number | null ``` Unique id within the registry; 0 for the root fiber, `null` once disposed. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L185) +[Source](../../../vendor/cordis/src/fiber.ts#L185) ### fiber.ctx -```ts website-api +```ts cordis-catalog /** The context this fiber's plugin runs in (extends the parent context). */ public readonly ctx: Context ``` The context this fiber's plugin runs in (extends the parent context). -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L187) +[Source](../../../vendor/cordis/src/fiber.ts#L187) ### fiber.config -```ts website-api +```ts cordis-catalog /** The validated plugin config (updated by `update()`). */ public config: any ``` The validated plugin config (updated by `update()`). -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L189) +[Source](../../../vendor/cordis/src/fiber.ts#L189) ### fiber.state -```ts website-api +```ts cordis-catalog /** Current lifecycle state; transitions emit `internal/status`. */ public state ``` Current lifecycle state; transitions emit `internal/status`. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L191) +[Source](../../../vendor/cordis/src/fiber.ts#L191) ### fiber.dispose -```ts website-api +```ts cordis-catalog /** Dispose this fiber: unload the plugin, then settle once cleanup finished. */ public readonly dispose: () => Promise ``` Dispose this fiber: unload the plugin, then settle once cleanup finished. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L193) +[Source](../../../vendor/cordis/src/fiber.ts#L193) ### fiber.store -```ts website-api +```ts cordis-catalog /** Snapshot of required service implementations while loaded; `undefined` otherwise. */ public store: Dict | undefined ``` Snapshot of required service implementations while loaded; `undefined` otherwise. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L195) +[Source](../../../vendor/cordis/src/fiber.ts#L195) ### fiber.inertia -```ts website-api +```ts cordis-catalog /** The in-flight load/unload transition, if one is currently running. */ public inertia: Promise | undefined ``` The in-flight load/unload transition, if one is currently running. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L197) +[Source](../../../vendor/cordis/src/fiber.ts#L197) ### fiber.name -```ts website-api +```ts cordis-catalog /** The plugin's display name, inherited from the nearest named ancestor, else `'root'`. */ get name() ``` The plugin's display name, inherited from the nearest named ancestor, else `'root'`. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L340) +[Source](../../../vendor/cordis/src/fiber.ts#L340) ### fiber.assertActive() -```ts website-api +```ts cordis-catalog /** * Throw if the fiber has already been disposed. * @@ -156,11 +159,11 @@ Throw if the fiber has already been disposed. **Returns** nothing when the fiber is still active. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L355) +[Source](../../../vendor/cordis/src/fiber.ts#L355) ### fiber.effect(execute, label?) -```ts website-api +```ts cordis-catalog /** * Register a cleanup-aware effect on this fiber. * @@ -179,6 +182,7 @@ effect(execute: () => Effect, label?: string): AsyncDisposable> ``` Register a cleanup-aware effect on this fiber. + `execute` runs immediately; the disposers it produces are collected and run (in reverse order) either when the returned disposer is called or when the fiber unloads, whichever comes first. Calling the disposer twice is a no-op. Throws `CordisError('INACTIVE_EFFECT')` if the fiber is already disposed, and `TypeError` if `execute` returns an invalid shape. - `execute` — the effect body; see `Effect` for accepted shapes. @@ -186,11 +190,11 @@ Register a cleanup-aware effect on this fiber. **Returns** a disposer that tears the effect down and settles once done. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L419) +[Source](../../../vendor/cordis/src/fiber.ts#L419) ### fiber.getEffects() -```ts website-api +```ts cordis-catalog /** * Return metadata for currently registered effects. * @@ -203,11 +207,11 @@ Return metadata for currently registered effects. **Returns** one `EffectMeta` tree per labeled live effect. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L572) +[Source](../../../vendor/cordis/src/fiber.ts#L572) ### fiber.await() -```ts website-api +```ts cordis-catalog /** * Wait for current lifecycle work and rethrow startup errors. * @@ -221,11 +225,11 @@ Wait for current lifecycle work and rethrow startup errors. **Returns** this fiber, once it has settled into a stable state. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L701) +[Source](../../../vendor/cordis/src/fiber.ts#L701) ### fiber.restart() -```ts website-api +```ts cordis-catalog /** * Dispose and immediately reload this plugin with its current config. * @@ -239,11 +243,11 @@ Dispose and immediately reload this plugin with its current config. **Returns** a promise resolving once the reload settled. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L715) +[Source](../../../vendor/cordis/src/fiber.ts#L715) ### fiber.update(config, noSave?) -```ts website-api +```ts cordis-catalog /** * Validate and apply new config, then restart the plugin. * @@ -259,6 +263,7 @@ update(config: any, noSave = false) ``` Validate and apply new config, then restart the plugin. + Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto or replace the restart. - `config` — the new raw config; validated before anything restarts. @@ -266,14 +271,15 @@ Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto o **Returns** nothing; the restart runs behind the `internal/update` waterfall. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L733) +[Source](../../../vendor/cordis/src/fiber.ts#L733) ## Effect Effect body result accepted by `ctx.effect()` and plugin startup. + Either a single disposer, a promise of one, or a (possibly async) iterable yielding several — generator effects register each yielded disposer as it is produced. -```ts website-api +```ts cordis-catalog /** * Effect body result accepted by `ctx.effect()` and plugin startup. * @@ -286,14 +292,15 @@ type Effect = | AsyncEffect ``` -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L82) +[Source](../../../vendor/cordis/src/fiber.ts#L82) ## Disposable Function returned by an effect to release resources during disposal. + Disposers run in reverse registration order when the owning fiber unloads; they may be async, in which case unloading awaits them. -```ts website-api +```ts cordis-catalog /** * Function returned by an effect to release resources during disposal. * @@ -303,13 +310,13 @@ Disposers run in reverse registration order when the owning fiber unloads; they type Disposable = () => T ``` -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L73) +[Source](../../../vendor/cordis/src/fiber.ts#L73) ## EffectMeta Tree node used to expose nested effect labels for diagnostics. -```ts website-api +```ts cordis-catalog /** Tree node used to expose nested effect labels for diagnostics. */ interface EffectMeta { /** Human-readable effect label, e.g. `ctx.on("event")` or `ctx.provide("name")`. */ @@ -319,13 +326,13 @@ interface EffectMeta { } ``` -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L95) +[Source](../../../vendor/cordis/src/fiber.ts#L95) ## CordisError Framework error with a stable machine-readable code. -```ts website-api +```ts cordis-catalog /** Framework error with a stable machine-readable code. */ class CordisError extends Error { /** @@ -345,13 +352,13 @@ namespace CordisError { } ``` -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L156) +[Source](../../../vendor/cordis/src/fiber.ts#L156) ## ValidationError Error raised when plugin configuration fails standard-schema validation. -```ts website-api +```ts cordis-catalog /** Error raised when plugin configuration fails standard-schema validation. */ class ValidationError extends TypeError { name = 'ValidationError' @@ -365,4 +372,4 @@ class ValidationError extends TypeError { } ``` -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/fiber.ts#L18) +[Source](../../../vendor/cordis/src/fiber.ts#L18) diff --git a/website/zh-CN/api/cordis/registry.md b/docs/cordis-catalog/core/registry.md similarity index 88% rename from website/zh-CN/api/cordis/registry.md rename to docs/cordis-catalog/core/registry.md index f91f5a72af..2772dca723 100644 --- a/website/zh-CN/api/cordis/registry.md +++ b/docs/cordis-catalog/core/registry.md @@ -1,4 +1,5 @@ - + # Registry @@ -6,7 +7,7 @@ Plugin loading and dependency injection. ### ctx.inject(deps, callback) -```ts website-api +```ts cordis-catalog /** * Run a callback once the requested services are available. * @@ -21,6 +22,7 @@ inject(deps: Inject, callback: Plugin.Function): Fiber & PromiseLike = | Plugin.Function @@ -116,14 +118,15 @@ namespace Plugin { } ``` -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L91) +[Source](../../../vendor/cordis/src/registry.ts#L91) ## Inject Service dependency declaration accepted by plugins and the `@Inject` decorator. + Array form requests services without intercept config. Object form maps each service name to optional intercept config for the plugin context. -```ts website-api +```ts cordis-catalog /** * Service dependency declaration accepted by plugins and the `@Inject` * decorator. @@ -146,4 +149,4 @@ namespace Inject { } ``` -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L18) +[Source](../../../vendor/cordis/src/registry.ts#L18) diff --git a/website/zh-CN/api/cordis/service.md b/docs/cordis-catalog/core/service.md similarity index 57% rename from website/zh-CN/api/cordis/service.md rename to docs/cordis-catalog/core/service.md index 13aa82a2ca..84b74f98df 100644 --- a/website/zh-CN/api/cordis/service.md +++ b/docs/cordis-catalog/core/service.md @@ -1,100 +1,102 @@ - + # Service -Base class for context services: subclass it and load the subclass as a plugin to register `ctx.`. +The base class for context services. A subclass loaded as a plugin registers itself as `ctx.`. Base class for services that expose a named API on `ctx`. + Subclasses call `super(ctx, name)` from their constructor. The service is registered immediately and is automatically removed with the owning fiber. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L11) +[Source](../../../vendor/cordis/src/service.ts#L11) ### service.name -```ts website-api +```ts cordis-catalog /** The service name this instance is registered under. */ public name!: string ``` The service name this instance is registered under. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L30) +[Source](../../../vendor/cordis/src/service.ts#L30) ## Static members ### Service.init -```ts website-api +```ts cordis-catalog /** Symbol key of an instance method run after construction (class plugins). */ static readonly init: unique symbol ``` Symbol key of an instance method run after construction (class plugins). -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L13) +[Source](../../../vendor/cordis/src/service.ts#L13) ### Service.check -```ts website-api +```ts cordis-catalog /** Symbol key of the availability predicate passed to `ctx.provide()`. */ static readonly check: unique symbol ``` Symbol key of the availability predicate passed to `ctx.provide()`. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L15) +[Source](../../../vendor/cordis/src/service.ts#L15) ### Service.config -```ts website-api +```ts cordis-catalog /** Symbol key of the phantom intercept-config type parameter. */ static readonly config: unique symbol ``` Symbol key of the phantom intercept-config type parameter. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L17) +[Source](../../../vendor/cordis/src/service.ts#L17) ### Service.invoke -```ts website-api +```ts cordis-catalog /** Symbol key of the call body making a service callable (e.g. `ctx.logger()`). */ static readonly invoke: unique symbol ``` Symbol key of the call body making a service callable (e.g. `ctx.logger()`). -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L19) +[Source](../../../vendor/cordis/src/service.ts#L19) ### Service.extend -```ts website-api +```ts cordis-catalog /** Symbol key of the helper deriving an extended service instance. */ static readonly extend: unique symbol ``` Symbol key of the helper deriving an extended service instance. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L21) +[Source](../../../vendor/cordis/src/service.ts#L21) ### Service.tracker -```ts website-api +```ts cordis-catalog /** Symbol key of the tracker metadata used for context tracing. */ static readonly tracker: unique symbol ``` Symbol key of the tracker metadata used for context tracing. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L23) +[Source](../../../vendor/cordis/src/service.ts#L23) ### Service.resolveConfig -```ts website-api +```ts cordis-catalog /** Symbol key of the intercept-config resolution helper below. */ static readonly resolveConfig: unique symbol ``` Symbol key of the intercept-config resolution helper below. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/service.ts#L25) +[Source](../../../vendor/cordis/src/service.ts#L25) diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index 92f3b16240..dfe0fdb502 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -7,7 +7,7 @@ Every cordis event a plugin can listen to: exact signature, dispatch mode, and o This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them. -The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. +The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md). Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`). @@ -33,7 +33,7 @@ A fully configured agent and live session were published. Setup is composition-o Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:147`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:150`](../../packages/core/agent/src/types.ts) ### `agent/disposed` — emit @@ -53,7 +53,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence but bef Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:156`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:159`](../../packages/core/agent/src/types.ts) ### `agent/error` — emit @@ -75,7 +75,7 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:311`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:314`](../../packages/core/agent/src/types.ts) ### `agent/post-step` — serial @@ -98,7 +98,7 @@ Awaited serial checkpoint after the response, real or synthetic tool results, in Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:264`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:267`](../../packages/core/agent/src/types.ts) ### `agent/pre-step` — serial @@ -121,18 +121,18 @@ Awaited serial checkpoint before `step/start`; appends land outside the pending Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:207`](../../packages/core/agent/src/types.ts) ### `agent/prompt-submit` — waterfall -Allow, rewrite, or block one drained prompt before it becomes a user message. Call `next()` for the unchanged default. +Allow, rewrite, or block one claimed prompt before it becomes a user message. Call `next()` for the unchanged default. ```ts cordis-catalog /** - * Allow, rewrite, or block one drained prompt before it becomes a user + * Allow, rewrite, or block one claimed prompt before it becomes a user * message. Call `next()` for the unchanged default. - * @param agent - the agent draining its inbox. - * @param content - the drained message's blocks, as queued. + * @param agent - the agent whose turn claimed the message. + * @param content - the claimed message's blocks, as queued. * @param source - the message's resolved source. * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. * @mode waterfall @@ -142,7 +142,7 @@ Allow, rewrite, or block one drained prompt before it becomes a user message. Ca Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:214`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:217`](../../packages/core/agent/src/types.ts) ### `agent/queued` — emit @@ -163,7 +163,7 @@ Detached, frozen content entered the agent's inbox. Source defaults have already Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:175`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:178`](../../packages/core/agent/src/types.ts) ### `agent/request` — waterfall @@ -186,7 +186,7 @@ Replace the frozen call configuration. Model-visible content must use logged cha Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:226`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:229`](../../packages/core/agent/src/types.ts) ### `agent/request-error` — waterfall @@ -211,7 +211,7 @@ Recover a model-request failure after its failed step has closed. `retry` opens Types: [Agent](../core-data-structures/core.md) · [RequestError](../core-data-structures/core.md) · [RequestErrorDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:278`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:281`](../../packages/core/agent/src/types.ts) ### `agent/session-prefix` — waterfall @@ -237,7 +237,7 @@ Compose request-only messages placed before derived history. The frozen result i Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:241`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:244`](../../packages/core/agent/src/types.ts) ### `agent/session-start` — emit @@ -259,7 +259,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SessionStartSource](../core-data-structures/core.md) -Source: [`packages/core/agent/src/types.ts:188`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:191`](../../packages/core/agent/src/types.ts) ### `agent/status` — emit @@ -279,7 +279,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does no Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:165`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:168`](../../packages/core/agent/src/types.ts) ### `agent/step-result` — waterfall @@ -301,7 +301,7 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:252`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:255`](../../packages/core/agent/src/types.ts) ### `agent/turn-continuation` — waterfall @@ -322,7 +322,7 @@ Override whether the turn continues. The default continues after tool calls or s Types: [Agent](../core-data-structures/core.md) · [ContinuationDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:288`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:291`](../../packages/core/agent/src/types.ts) ### `agent/turn-stop` — serial @@ -343,7 +343,7 @@ Monotonic terminal-stop checkpoint after continuation and steering are folded; a Types: [Agent](../core-data-structures/core.md) · [ContinuationStop](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) -Source: [`packages/core/agent/src/types.ts:298`](../../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:301`](../../packages/core/agent/src/types.ts) ## `agent-loop/*` @@ -408,7 +408,7 @@ Single-slot decision for the next FileSystem.editText. Calling `next()` yields a Types: [FsTarget](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/filesystem.md) -Source: [`packages/fs/fs/src/index.ts:61`](../../packages/fs/fs/src/index.ts) +Source: [`packages/fs/fs/src/index.ts:62`](../../packages/fs/fs/src/index.ts) ### `fs/observed` — emit @@ -428,7 +428,7 @@ Record a successful observation. Listeners must be synchronous recorders: throws Types: [FsTarget](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/filesystem.md) -Source: [`packages/fs/fs/src/index.ts:70`](../../packages/fs/fs/src/index.ts) +Source: [`packages/fs/fs/src/index.ts:71`](../../packages/fs/fs/src/index.ts) ### `fs/write-intent` — waterfall @@ -448,7 +448,7 @@ Single-slot decision for the next FileSystem.writeText. Calling `next()` yields Types: [FsTarget](../core-data-structures/filesystem.md) · [FsWriteIntent](../core-data-structures/filesystem.md) -Source: [`packages/fs/fs/src/index.ts:53`](../../packages/fs/fs/src/index.ts) +Source: [`packages/fs/fs/src/index.ts:54`](../../packages/fs/fs/src/index.ts) ## `goal/*` @@ -608,7 +608,7 @@ A ready child settled. Scope-filtered dispatch uses the same delegating parent c Types: [Scoped](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/subagent.md) -Source: [`packages/subagent/subagent/src/index.ts:112`](../../packages/subagent/subagent/src/index.ts) +Source: [`packages/subagent/subagent/src/index.ts:139`](../../packages/subagent/subagent/src/index.ts) ### `subagent/provider-added` — emit @@ -625,7 +625,7 @@ A provider became resolvable in the registry. Types: [SubagentProvider](../core-data-structures/subagent.md) -Source: [`packages/subagent/subagent/src/index.ts:86`](../../packages/subagent/subagent/src/index.ts) +Source: [`packages/subagent/subagent/src/index.ts:113`](../../packages/subagent/subagent/src/index.ts) ### `subagent/provider-removed` — emit @@ -640,7 +640,7 @@ A provider left the registry. Accepted runs remain holder-owned. 'subagent/provider-removed'(name: string): void ``` -Source: [`packages/subagent/subagent/src/index.ts:92`](../../packages/subagent/subagent/src/index.ts) +Source: [`packages/subagent/subagent/src/index.ts:119`](../../packages/subagent/subagent/src/index.ts) ### `subagent/start` — emit @@ -662,7 +662,7 @@ A provider established a ready child. For in-process providers, `ctx.agents.get( Types: [Scoped](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/subagent.md) -Source: [`packages/subagent/subagent/src/index.ts:103`](../../packages/subagent/subagent/src/index.ts) +Source: [`packages/subagent/subagent/src/index.ts:130`](../../packages/subagent/subagent/src/index.ts) ## `system-prompt/*` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 67884e71c2..7d99f0bf2b 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -7,7 +7,7 @@ Every `ctx.` service a plugin can call: the exact public interface with ori This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them. -The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. +The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. Detailed Context, Fiber, Registry, and Service APIs are generated in the [Cordis core API](core/context.md). ## `ctx.agentLoop` — `AgentLoop` @@ -216,7 +216,7 @@ roots(): Agent[] Types: [Agent](../core-data-structures/core.md) · [SessionId](../core-data-structures/core.md) -Source: [`packages/core/agent/src/index.ts:217`](../../packages/core/agent/src/index.ts) +Source: [`packages/core/agent/src/index.ts:223`](../../packages/core/agent/src/index.ts) ## `ctx.approval` — `ApprovalService` @@ -286,7 +286,7 @@ abstract start(spec: BashExecSpec): BashProcess Types: [BashExecRequest](../core-data-structures/bash.md) · [BashExecSpec](../core-data-structures/bash.md) · [BashProcess](../core-data-structures/bash.md) · [BashRunResult](../core-data-structures/bash.md) -Source: [`packages/bash/bash/src/index.ts:49`](../../packages/bash/bash/src/index.ts) +Source: [`packages/bash/bash/src/index.ts:48`](../../packages/bash/bash/src/index.ts) ## `ctx.bashEnv` — `BashEnvRegistry` @@ -317,7 +317,7 @@ list(): BashEnvVariableInfo[] Types: [DshEnvironment](../core-data-structures/bash.md) · [ToolExecution](../core-data-structures/tools.md) -Source: [`packages/bash/tool-bash/src/index.ts:102`](../../packages/bash/tool-bash/src/index.ts) +Source: [`packages/bash/tool-bash/src/index.ts:103`](../../packages/bash/tool-bash/src/index.ts) ## `ctx.codeRuntime` — `CodeRuntime` (abstract seam) @@ -458,9 +458,12 @@ abstract listDir(target: FsTarget, signal?: AbortSignal): Promise * @param content - the full new file content. * @param expected - the write intent guarding the write; omit for unconditional. * @param signal - aborts before the atomic rename takes effect. + * @param sandboxMode - the per-call sandbox mode this write runs under; a + * sandboxing backend fences the write by it, the bare backend ignores it. + * Omit to leave the backend its own default. * @returns the outcome, including the version the write produced. */ -abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal): Promise +abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxMode?: SandboxMode, ): Promise /** * Atomically edit literal text. When supplied, the version guard is checked @@ -470,14 +473,17 @@ abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, * @param edit - the literal search/replace request. * @param expected - the version guard; omit for an unconditional edit. * @param signal - aborts before the atomic rename takes effect. + * @param sandboxMode - the per-call sandbox mode this edit runs under; a + * sandboxing backend fences the edit by it, the bare backend ignores it. + * Omit to leave the backend its own default. * @returns the outcome, including the version the edit produced. */ -abstract editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal): Promise +abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxMode?: SandboxMode, ): Promise ``` -Types: [FsDirEntry](../core-data-structures/filesystem.md) · [FsEditOutcome](../core-data-structures/filesystem.md) · [FsEditRequest](../core-data-structures/filesystem.md) · [FsInfo](../core-data-structures/filesystem.md) · [FsPathInfo](../core-data-structures/filesystem.md) · [FsTarget](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/filesystem.md) · [FsWriteIntent](../core-data-structures/filesystem.md) · [FsWriteOutcome](../core-data-structures/filesystem.md) +Types: [FsDirEntry](../core-data-structures/filesystem.md) · [FsEditOutcome](../core-data-structures/filesystem.md) · [FsEditRequest](../core-data-structures/filesystem.md) · [FsInfo](../core-data-structures/filesystem.md) · [FsPathInfo](../core-data-structures/filesystem.md) · [FsTarget](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/filesystem.md) · [FsWriteIntent](../core-data-structures/filesystem.md) · [FsWriteOutcome](../core-data-structures/filesystem.md) · [SandboxMode](../core-data-structures/sandbox.md) -Source: [`packages/fs/fs/src/index.ts:80`](../../packages/fs/fs/src/index.ts) +Source: [`packages/fs/fs/src/index.ts:81`](../../packages/fs/fs/src/index.ts) ## `ctx.goals` — `GoalService` @@ -647,7 +653,7 @@ set(session: Session, name: string): void Types: [Session](../core-data-structures/session.md) · [SessionEvent](../core-data-structures/core.md) -Source: [`packages/ui/permission/src/index.ts:94`](../../packages/ui/permission/src/index.ts) +Source: [`packages/ui/permission/src/index.ts:97`](../../packages/ui/permission/src/index.ts) ## `ctx.sandbox` — `SandboxProvider` (abstract seam) @@ -670,7 +676,13 @@ abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv Types: [ConfinedArgv](../core-data-structures/sandbox.md) · [SandboxPolicy](../core-data-structures/sandbox.md) -Source: [`packages/sandbox/sandbox/src/index.ts:111`](../../packages/sandbox/sandbox/src/index.ts) +Source: [`packages/sandbox/sandbox/src/index.ts:122`](../../packages/sandbox/sandbox/src/index.ts) + +## `ctx.sandboxPolicy` — `SandboxPolicyService` + +The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode and workspace root; enforcing implementations read defaultMode and workspaceRoot, and the tool layers fold each session's `sandbox/mode` override with effectiveSandboxMode on top. + +Source: [`packages/sandbox/sandbox-policy/src/index.ts:60`](../../packages/sandbox/sandbox-policy/src/index.ts) ## `ctx.sessionPersistence` — `SessionPersistence` (abstract seam) @@ -784,9 +796,9 @@ Persistence is intentionally not implemented here — persistence plugins subscr * Create a session owned by the calling fiber: disposing that fiber stops * event notification and removes the session from the store. `options.seed` * populates the session with a copy of those events (replay/fork); - * `options.meta` attaches creation metadata (validated absolute `cwd`, - * `parentSession` lineage) as the immutable {@link SessionHeader} (the store - * fills `version`/`id`/`createdAt`). + * `options.meta` attaches creation metadata (validated absolute `cwd`, seed + * and parent lineage, and delegation depth) as the immutable + * {@link SessionHeader} (the store fills `version`/`id`/`createdAt`). * * For an agent whose session must be torn down IN ORDER with its loop (so the * loop's final flush is captured before the store attachment ends), do NOT use this @@ -898,7 +910,7 @@ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Types: [CreateSessionOptions](../core-data-structures/persistence.md) · [Session](../core-data-structures/session.md) · [SessionId](../core-data-structures/core.md) -Source: [`packages/core/session/src/index.ts:577`](../../packages/core/session/src/index.ts) +Source: [`packages/core/session/src/index.ts:553`](../../packages/core/session/src/index.ts) ## `ctx.skills` — `SkillService` @@ -1012,7 +1024,7 @@ async start(name: string, request: SubagentStartRequest): Promise Types: [SubagentProvider](../core-data-structures/subagent.md) · [SubagentRun](../core-data-structures/subagent.md) · [SubagentStartRequest](../core-data-structures/subagent.md) -Source: [`packages/subagent/subagent/src/index.ts:153`](../../packages/subagent/subagent/src/index.ts) +Source: [`packages/subagent/subagent/src/index.ts:180`](../../packages/subagent/subagent/src/index.ts) ## `ctx.systemPrompt` — `SystemPrompt` @@ -1186,6 +1198,43 @@ Types: [EpochHeader](../core-data-structures/session.md) · [Message](../core-da Source: [`packages/llm/token-meter/src/index.ts:106`](../../packages/llm/token-meter/src/index.ts) +## `ctx.toolResultPrune` — `ToolResultPruneService` + +Deterministic head/middle/tail pruning for current tool-result surface nodes. + +```ts cordis-catalog +/** + * Measure text content in Unicode code points; non-text blocks cost zero. + * @param blocks - tool-result content to measure. + * @returns total Unicode code points across text blocks. + */ +measureContent(blocks: readonly ContentBlock[]): number + +/** + * Replace an over-budget text middle while retaining rich-block order. + * Text slicing is by Unicode code point, not UTF-16 code unit, so a retained + * boundary cannot split a surrogate pair. Grapheme clusters may still split. + * @param blocks - original tool-result content. + * @returns pruned content, or `null` when the text is within budget. + */ +pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null + +/** + * Prune every over-budget tool result from one stable current-surface snapshot. + * Each replacement preserves the complete event data except for `content`, + * and points at the shadowed node for durable provenance and replay. + * @param session - session whose current surface is rewritten. + * @returns landed replacements and aggregate Unicode-code-point savings. + * @throws when the session rejects a replacement; replacements committed + * earlier in the pass remain durable. + */ +pruneSession(session: Session): PruneResult +``` + +Types: [ContentBlock](../core-data-structures/core.md) · [PruneResult](../core-data-structures/compaction.md) · [Session](../core-data-structures/session.md) + +Source: [`packages/compact/compact-tool-result-prune/src/index.ts:39`](../../packages/compact/compact-tool-result-prune/src/index.ts) + ## `ctx.tools` — `ToolRegistry` Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch. diff --git a/docs/core-data-structures/bash.md b/docs/core-data-structures/bash.md index c1c7754dd2..2070c464cf 100644 --- a/docs/core-data-structures/bash.md +++ b/docs/core-data-structures/bash.md @@ -159,7 +159,7 @@ interface CollectedOutput { ## File sandbox: `BashSandboxInfo` -A sandbox-consuming executor exposes its configured fallback through `BashExecutor.sandboxMode`. The tool layer folds each session's durable `bash/sandbox-mode` override and may replace it for one user-approved strictly wider call. The mode/enforcement vocabulary is owned by the [`@deepseek-ai/dsh-sandbox` seam](sandbox.md); modes govern file effects only. +A sandbox-consuming executor exposes its configured fallback through `BashExecutor.sandboxMode`. The tool layer folds each session's durable `sandbox/mode` override (owned by [`@deepseek-ai/dsh-sandbox-policy`](../../packages/sandbox/sandbox-policy/README.md)) and may replace it for one user-approved strictly wider call. The mode/enforcement vocabulary is owned by the [`@deepseek-ai/dsh-sandbox` seam](sandbox.md); modes govern file effects only. A sandboxed run reports its mode, conservative denial classification, and enforcement completeness. `runnerFailed` marks a sandbox runner failure before the command ran; foreground execution throws `SANDBOX_UNAVAILABLE`, while a settled background process has only its facts channel. diff --git a/docs/core-data-structures/compaction.md b/docs/core-data-structures/compaction.md index a511b2f93d..bb302ff52b 100644 --- a/docs/core-data-structures/compaction.md +++ b/docs/core-data-structures/compaction.md @@ -6,7 +6,7 @@ Source: [`packages/compact/compact/src/types.ts`](../../packages/compact/compact ## The `compact/*` session events -Compaction extends [`SessionEventMap`](session.md) with three event types via declaration merging. All three are **log-only** — they record the compaction lock and its provenance, and never join the surface. `SurfaceEventType` is deliberately NOT extended (only message-producing events reach the model), so the summary itself rides on a separate `user/message` with `surfaceOp: { op: 'replace', start, end }` — the only surface mutation. See the Agent Note for why reusing `user/message` is honest rather than a workaround. +Compaction extends [`SessionEventMap`](session.md) with three event types via declaration merging. All three are **log-only** — they record the compaction lock and its provenance, and never join the surface. `SurfaceEventType` is deliberately NOT extended (only message-producing events reach the model), so the summary itself rides on a separate `user/message` with `surfaceOp: { op: 'replace', start, end }` — the only surface mutation performed by summary compaction. See the Agent Note for why reusing `user/message` is honest rather than a workaround. | Event | Payload | Role | |---|---|---| @@ -60,6 +60,36 @@ type CompactionTrigger = 'pressure' | 'context-overflow' `CompactService` exposes `compactIfNeeded(agent, trigger, signal)` for automatic `pressure` or `context-overflow` policy, returning `null` when no safe work exists, and `compactRegion(...)` for an explicit inclusive surface range. Implementations must forward the supplied signal to summarization. The seam owns no pricing API: the singleton [`ctx.tokenMeter`](token-meter.md) directly owns estimation and replay, while `dsh-compact-basic` owns retention, event sequencing, routed summarization calls, and their configuration. -Pressure compaction runs at serial `agent/post-step`, after successful assistant output, tool results, buffered context, and steering are durable but before `step/end`. Failed-request recovery runs through `agent/request-error` after the failed step closes, and authorizes a fresh numbered-step retry only when the surface replacement generation advances. Region boundaries preserve tool-call/result pairing but do not preserve whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling. +Pressure compaction runs at serial `agent/post-step`, after successful assistant output, tool results, buffered context, and steering are durable but before `step/end`. Once pressure or canonical overflow qualifies, compact-basic invokes optional [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md) before range selection, remeasures through `ctx.tokenMeter`, and can advance the surface without a summary. Failed-request recovery runs through `agent/request-error` after the failed step closes and authorizes a fresh numbered-step retry only when the surface replacement generation advances, even if later summary work throws after pruning; cancellation still wins. Region boundaries preserve tool-call/result pairing but not whole turns, allowing early closed steps of one oversized turn to compact. `dsh-compact-basic` owns thresholds, retained-tail policy, overflow caps, and failure handling. The seam exports `toolPairingBalancedBefore(session, seq)` and `toolPairingBalancedAfter(session, seq)` for those edge checks. Both validate current surface membership and reject missing seqs and orphan results; the [package contract](../../packages/compact/compact/README.md#tool-pairing-boundaries) owns their cache semantics. + +## Tool-result pruning outcomes + +The optional tool-result pruning service reports each durable content replacement and the aggregate Unicode-code-point reduction. Its public result types live in [`compact-tool-result-prune/src/types.ts`](../../packages/compact/compact-tool-result-prune/src/types.ts). + +```ts type-equiv +/** Provenance and size accounting for one landed surface replacement. */ +interface PrunedEntry { + /** Full-fidelity tool-result event shadowed by the replacement. */ + readonly originalSeq: number + /** Newly appended pruned tool-result event. */ + readonly replacementSeq: number + /** Tool call shared by the original and replacement. */ + readonly callId: CallId + /** Original text size in Unicode code points. */ + readonly charsBefore: number + /** Replacement text size in Unicode code points. */ + readonly charsAfter: number +} +``` + +```ts type-equiv +/** Aggregate outcome of one stable-surface pruning pass. */ +interface PruneResult { + /** Replacements in the snapshotted surface order. */ + readonly pruned: readonly PrunedEntry[] + /** Total Unicode code points removed across replacements. */ + readonly charsRemoved: number +} +``` diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index bb1e08abd5..f32b75a24d 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -339,13 +339,11 @@ The fourteen event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) -`InjectOptions` extends ordinary message attribution with context-only framing and durable model-hidden JSON metadata: +`InjectOptions` extends ordinary message attribution with durable model-hidden JSON metadata: ```ts type-equiv /** Options specific to durable synthetic context injection. */ interface InjectOptions extends SendOptions { - /** Keep the canonical context tag, or send caller-owned framing verbatim. */ - envelope?: ContextEnvelope /** Opaque JSON state retained in the session event but hidden from the model. */ meta?: JsonValue } @@ -363,15 +361,20 @@ interface Agent { readonly ctx: Context /** - * Queue detached, frozen lossless-JSON input; starts a turn when idle. + * Queue one detached, frozen lossless-JSON item. If claimed, it is the sole + * ordinary message in its FIFO-ordered turn; the next claimed item waits for + * that turn's checkpoint. * Invalid input throws synchronously before notification or enqueue. */ send(content: ContentBlock[], options?: SendOptions): void /** - * Steer a running turn: content is injected between steps of the current - * turn. Uses the same owned-value and synchronous-validation boundary as - * {@link send}; when idle, behaves exactly like that method. + * Submit steering while the agent is `running`. An open turn records it at + * the next steering checkpoint before a request or continuation decision; + * policy may stop before another step. After turn close and its checkpoint, + * any remainder is queued for a later turn; terminal `agent/turn-stop`, + * cancellation, or disposal may discard it. Uses the same synchronous + * snapshot-and-validation boundary as {@link send}; when idle, delegates to it. */ steer(content: ContentBlock[], options?: SendOptions): void @@ -385,10 +388,11 @@ interface Agent { inject(content: ContentBlock[], options?: InjectOptions): void /** - * Clear queued and steering work, including work waiting to start, and abort - * the active step. The supplied reason is preserved across pre-step and active - * cancellation windows, and `whenIdle()` resolves after cancellation reaches - * quiescence. Idle cancellation is a no-op and does not arm a later cancel. + * Clear all queued and steering work, including items waiting to start, and + * abort the active step. The supplied reason is preserved across pre-step + * and active cancellation windows, and `whenIdle()` resolves after + * cancellation reaches quiescence. Idle cancellation is a no-op and does not + * arm a later cancel. */ cancel(reason?: string): void @@ -398,7 +402,7 @@ interface Agent { } ``` -`AgentStatus` is `'idle' | 'running' | 'disposed'`, and `SessionId` is branded. `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 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. The [event taxonomy](../architecture.md#event) owns the `agent/*` lifecycle, checkpoint, and waterfall contracts. Turn and step boundaries are durable session events rather than agent emits. @@ -408,7 +412,7 @@ The process-local initiator carried by `ctx.agents` is the exact `Agent` above, ## Interception decisions -Each `agent/*` interception waterfall returns a small, seam-specific typed union — the unified Decision idiom (the tool seams' `PreToolDecision`/`PostToolDecision` in [tools.md](tools.md) follow the same shape). A CC/Codex hook bridge maps its `permissionDecision`/`decision`/`continue`/`additionalContext` fields onto these; a native plugin returns them directly. Prompt and post-tool decisions share one model-facing context shape, `HookContext`, which is `inject()`ed as a `context/message` and therefore carries a REQUIRED `source` (a missing source would default to `{kind:'user'}` and mislabel plugin context as a user prompt). Its optional `envelope` selects the canonical context tag or caller-owned raw framing, while JSON `meta` persists plugin state without exposing it to the model. Both decisions carry `additionalContexts[]` so every entry preserves its own provenance, framing, and metadata. Continuation reasons are steering messages instead and deliberately use the narrower content/source shape. +Each `agent/*` interception waterfall returns a small, seam-specific typed union — the unified Decision idiom (the tool seams' `PreToolDecision`/`PostToolDecision` in [tools.md](tools.md) follow the same shape). A CC/Codex hook bridge maps its `permissionDecision`/`decision`/`continue`/`additionalContext` fields onto these; a native plugin returns them directly. Prompt and post-tool decisions share one model-facing context shape, `HookContext`, which is `inject()`ed as a `context/message` and therefore carries a REQUIRED `source` (a missing source would default to `{kind:'user'}` and mislabel plugin context as a user prompt). Its `content` reaches the model verbatim as a user-role message, while JSON `meta` persists plugin state without exposing it to the model. Both decisions carry `additionalContexts[]` so every entry preserves its own provenance and metadata. Continuation reasons are steering messages instead and deliberately use the narrower content/source shape. Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) @@ -417,27 +421,26 @@ Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types interface HookContext { content: ContentBlock[] source: MessageSource - /** Keep the canonical context tag, or use caller-owned framing verbatim. */ - envelope?: ContextEnvelope /** Opaque JSON state retained in the session event but hidden from the model. */ meta?: JsonValue } ``` -`agent/prompt-submit` returns a `PromptDecision` (allow a drained queued message — optionally rewriting its `content` or attaching `additionalContexts` — or block it; a batch whose every prompt is blocked opens a zero-step turn that ends `rejected`): +`agent/prompt-submit` returns a `PromptDecision` (allow the turn's claimed queued message — optionally rewriting its `content` or attaching `additionalContexts` — or record `prompt/blocked` and end that zero-step turn as `rejected`): ```ts type-equiv /** * Prompt interception result. `allow.content` replaces the prompt and each - * `additionalContexts` entry becomes a separate context message. `block` records a - * durable `prompt/blocked`; an all-blocked batch ends a zero-step rejected turn. + * `additionalContexts` entry becomes a separate context message. `block` + * records a durable `prompt/blocked` and ends the claimed prompt's zero-step + * turn as rejected. */ type PromptDecision = | { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: HookContext[] } | { kind: 'block'; reason: string } ``` -`agent/turn-continuation` returns a `ContinuationDecision` (the loop's default is `continue` when the step had tool calls or steering was injected, else `stop`; a `continue` `reason` is recorded as next-step steering in the same turn and therefore carries no context envelope or metadata — the typed `/goal` pattern): +`agent/turn-continuation` returns a `ContinuationDecision` (the loop's default is `continue` when the step had tool calls or steering was injected, else `stop`; a `continue` `reason` is recorded as next-step steering in the same turn and therefore carries no context metadata — the typed `/goal` pattern): ```ts type-equiv /** Turn continuation override; a continue reason is recorded as next-step steering in the same turn. */ diff --git a/docs/core-data-structures/filesystem.md b/docs/core-data-structures/filesystem.md index f29258857e..84d1ce9c5a 100644 --- a/docs/core-data-structures/filesystem.md +++ b/docs/core-data-structures/filesystem.md @@ -241,6 +241,7 @@ type FsErrorCode = | 'FS_NOT_TEXT' | 'FS_NOT_REGULAR_FILE' | 'FS_PERMISSION_DENIED' + | 'FS_SANDBOX_DENIED' | 'FS_IO_ERROR' | 'FS_STALE_VERSION' | 'FS_NOT_OBSERVED' @@ -249,7 +250,7 @@ type FsErrorCode = | 'FS_ABORTED' ``` -`FS_NOT_DIRECTORY`, `FS_PERMISSION_DENIED`, and `FS_IO_ERROR` are used by directory listing to distinguish an existing non-directory target, a denied listing, and an unexpected backend I/O failure. `FS_NOT_OBSERVED` means the policy plugin has no prior-observation record for this owner (or a `createIfAbsent` hit an existing file). `FS_STALE_VERSION` means the backend version no longer matches the observed one (or an edit hit a missing target). Freshness authorization has no partial/full distinction, so there is no `FS_PARTIAL_OBSERVATION`. +`FS_NOT_DIRECTORY`, `FS_PERMISSION_DENIED`, and `FS_IO_ERROR` are used by directory listing to distinguish an existing non-directory target, a denied listing, and an unexpected backend I/O failure. `FS_SANDBOX_DENIED` is a POLICY refusal from a sandbox-enforcing backend (`dsh-fs-sandbox`) — the mode fence denied a write/edit — distinct from `FS_PERMISSION_DENIED` (the host kernel refusing). `FS_NOT_OBSERVED` means the policy plugin has no prior-observation record for this owner (or a `createIfAbsent` hit an existing file). `FS_STALE_VERSION` means the backend version no longer matches the observed one (or an edit hit a missing target). Freshness authorization has no partial/full distinction, so there is no `FS_PARTIAL_OBSERVATION`. ## The service and the plugin diff --git a/docs/core-data-structures/goal.md b/docs/core-data-structures/goal.md index 6c4ac165f7..d45847ba0f 100644 --- a/docs/core-data-structures/goal.md +++ b/docs/core-data-structures/goal.md @@ -69,7 +69,7 @@ interface GoalView extends GoalSnapshot { ## Durable changes -Every mutation is a `context/message` whose metadata is either a complete snapshot or a clear tombstone. The version, metadata, goal source, raw envelope, and rendered content form one replay invariant. +Every mutation is a `context/message` whose metadata is either a complete snapshot or a clear tombstone. The version, metadata, goal source, and verbatim rendered content form one replay invariant. ```ts type-equiv /** Full-snapshot goal mutation retained in a model-visible context event. */ diff --git a/docs/core-data-structures/persistence.md b/docs/core-data-structures/persistence.md index a80b9bc896..0c6eecd35f 100644 --- a/docs/core-data-structures/persistence.md +++ b/docs/core-data-structures/persistence.md @@ -6,7 +6,7 @@ The seam is a textbook [capability seam](../../.agents/notes/implemented/archite ## The flush checkpoint -`session/event` is a *synchronous* notification; persistence plugins buffer it (write-behind) and drain at the awaited `session/flush` checkpoint the loop fires at every turn end. Flush is `ctx.parallel` (awaited): a turn's events are durably committed before the next turn starts, and the turn boundary is the commit boundary. A rejecting flush is reported via `agent/error` and the logger — never as a session event (it would land past the commit boundary), so the backend keeps its buffered events for the next flush. +`session/event` is a *synchronous* notification; persistence plugins buffer it (write-behind) until `session/flush`. The loop awaits an ordinary turn's checkpoint before claiming the next queue item; synchronous idle `inject()` schedules its checkpoint without blocking `send()`, and disposal still drains it. A successful flush durably commits the closed turn as one unit; a rejecting flush is reported through `agent/error` and the logger — never as a session event past the closed turn — while the backend keeps its buffered events for the next flush. ## Crash recovery preserves an interrupted turn @@ -60,12 +60,18 @@ interface SessionHeader { * boundary lets resume and replay distinguish parent history from child work. */ readonly seedLength?: number + /** + * Delegation depth: absent (zero) for a top-level session, parent depth + 1 + * for a subagent child. Persisted so a recursion budget survives restart and + * resume — a runtime-only depth would reset a resumed child to top-level. + */ + readonly delegationDepth?: number } ``` ## `CreateSessionOptions` — seeding and metadata -Creating a `Session` through the store takes a `seed` (replay/fork an existing event log) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller supplies the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, and — only when reconstructing a persisted session — the original `createdAt` to preserve it. +Creating a `Session` through the store takes a `seed` (replay/fork an existing event log) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller supplies the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the `delegationDepth`, and — only when reconstructing a persisted session — the original `createdAt` to preserve it. ```ts type-equiv /** @@ -85,6 +91,7 @@ interface CreateSessionOptions { readonly parentSession?: SessionId readonly createdAt?: number readonly seedLength?: number + readonly delegationDepth?: number } } ``` diff --git a/docs/core-data-structures/session.md b/docs/core-data-structures/session.md index 263d991f86..d1c97b1fae 100644 --- a/docs/core-data-structures/session.md +++ b/docs/core-data-structures/session.md @@ -4,15 +4,6 @@ The in-memory, event-sourced model of [dsh-session](../../packages/core/session) Source: [`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts) -## Context framing - -`ContextEnvelope` selects the standard tagged projection or preserves a producer-owned complete frame. The latter changes framing only; the event remains a user-role `context/message` in chronological history. - -```ts type-equiv -/** Canonical context-tag framing, or caller-owned framing rendered verbatim. */ -type ContextEnvelope = 'context' | 'raw' -``` - ## `SessionEventMap` — the event vocabulary The append-only event types. Merge-extensible: a plugin declares extra event types via declaration merging — e.g. the [compaction seam](compaction.md) adds `compact/start` / `compact/summary` / `compact/end`, and `@deepseek-ai/dsh-hook-protocol` adds log-only `hook/invoked` / `hook/result` provenance for a hook bridge. Like `compact/*`, these are NOT `SurfaceEventType`s (no `surfaceOp`). The generated [persistence log event catalog](../persistence-catalog.md) enumerates every member — core and merged — with its payload, surface badge, and declaration site. @@ -26,40 +17,44 @@ The append-only event types. Merge-extensible: a plugin declares extra event typ */ interface SessionEventMap { /** - * Opens turn `turn`. `trigger` records what started it — a drained message - * batch or an idle-time injection. The turn is the durability/replay + * Opens turn `turn`. `trigger` records what started it — one claimed queued + * message or an idle-time injection. The turn is the durability/replay * boundary: every event sits between a `turn/start` and its matching * `turn/end` (the turn-enclosure invariant). */ 'turn/start': { turn: number; trigger: TurnTrigger } /** * Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop - * fires the awaited `session/flush` checkpoint at every turn end, so the turn - * boundary is also the durable-commit boundary. + * awaits `session/flush` after an ordinary turn ends before claiming the next + * queued item. Success commits the turn; rejection is reported live and does + * not prevent later work. */ 'turn/end': { turn: number; reason: TurnEndReason } /** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */ 'step/start': { turn: number; step: number } /** Closes step `step` of turn `turn`. */ 'step/end': { turn: number; step: number } - /** A user-visible prompt (queued message drained at turn start). */ + /** A user-visible prompt (the queued message claimed for this turn). */ 'user/message': { content: ContentBlock[]; source: MessageSource } /** * Durable record of a prompt veto and its reason. It is log-only: the blocked - * prompt never enters the model-visible surface, including in a mixed batch. + * prompt never enters the model-visible surface, and its turn runs zero steps. */ 'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string } /** * In-session context injection (file-change notices, subdir AGENTS.md, * skill content, cron notifications, …). Rendered into the derived history - * as synthetic context — NOT a user prompt. `envelope: 'raw'` lets a caller - * own the complete model-facing frame; `meta` is durable JSON state omitted - * from the model projection. + * as a synthetic user-role message carrying `content` verbatim — NOT a + * user prompt. `meta` is durable JSON state omitted from the model + * projection; it is also the intended channel for any future framing + * directive (a producer declares the frame, a dedicated renderer applies it — + * see the deferred note in + * ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md), + * so the surface keeps projecting `content` verbatim rather than wrapping it. */ 'context/message': { content: ContentBlock[] source: MessageSource - envelope?: ContextEnvelope meta?: JsonValue } /** Raw stream chunk — token-level replay fidelity. */ @@ -432,8 +427,8 @@ declare class Session { - `user/message` → a user message. - `assistant/message` → an assistant message with the event's provider/model provenance and optional adapter-private replay state. Raw `assistant/chunk` events are replay/UI data and are **skipped** in derivation (the assembled message is authoritative). An **empty-content** `assistant/message` is also skipped — a max-tokens step cut off with no content still records an `assistant/message` to host its usage/provenance, but a content-less assistant turn must not enter the provider transcript. - `tool/result` → a user message carrying a `tool-result` block. -- `context/message` → a user-role message at its chronological position. The default `envelope` is `context`, which wraps content as ``; `envelope: 'raw'` uses caller-owned framing verbatim. Optional JSON `meta` remains in the event log and is never rendered. -- `steering/message` → a user-role message wrapped in `` at its chronological position. +- `context/message` → a user-role message carrying its `content` verbatim at its chronological position. Optional JSON `meta` remains in the event log and is never rendered. +- `steering/message` → a user-role message carrying its content verbatim at its chronological position. Everything else (`turn/*`, `step/*`) is structural and does not project into a message. Token usage is observed on `assistant/message.usage` (the step that produced it); an operational error's step number is on `turn/end.reason` for `kind: 'error'`. Because this unreleased format intentionally has no compatibility promise, seed/load validation rejects request headers without provider+model and assistant messages without provider/model provenance instead of guessing a route for historical data. @@ -486,8 +481,8 @@ interface TurnEndReasonMap { /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */ 'max-tokens': { kind: 'max-tokens' } /** - * Policy blocked every prompt before the first step. The zero-step turn still - * records a balanced durable boundary and the veto reason. + * Policy blocked the turn's claimed prompt before the first step. The + * zero-step turn still records a balanced durable boundary and veto reason. */ rejected: { kind: 'rejected'; reason: string } /** @@ -498,7 +493,7 @@ interface TurnEndReasonMap { } ``` -`max-tokens` mirrors the model-call `FinishReason` of the same name: any `max-tokens` step in a turn makes the whole turn end `max-tokens` rather than `completed` (the cut-short fact wins over a later continuation), so a consumer can tell a clean stop from a truncated one — but only over `completed`: the `disposed`/`aborted`/`error` outcomes take precedence. `rejected` is a zero-step turn whose whole prompt batch an `agent/prompt-submit` hook blocked (the ACP bridge maps it to `cancelled`). `interrupted` is the one reason no loop emits — it is synthesized by crash recovery (see [persistence.md](persistence.md)). Both maps are merge-extensible. +`max-tokens` mirrors the model-call `FinishReason` of the same name: any `max-tokens` step in a turn makes the whole turn end `max-tokens` rather than `completed` (the cut-short fact wins over a later continuation), so a consumer can tell a clean stop from a truncated one — but only over `completed`: the `disposed`/`aborted`/`error` outcomes take precedence. `rejected` is a zero-step turn whose claimed prompt an `agent/prompt-submit` hook blocked (the ACP bridge maps it to `cancelled`). `interrupted` is the one reason no loop emits — it is synthesized by crash recovery (see [persistence.md](persistence.md)). Both maps are merge-extensible. ## The turn-enclosure invariant diff --git a/docs/core-data-structures/subagent.md b/docs/core-data-structures/subagent.md index 54e79aca68..bd4324e697 100644 --- a/docs/core-data-structures/subagent.md +++ b/docs/core-data-structures/subagent.md @@ -237,5 +237,5 @@ interface SubagentProvider { The spawn and fork backends create an ordinary agent through `parent.ctx`, pass cancellation into core creation, and dispose through `AgentHandle`. Provider removal blocks new starts without revoking accepted runs. Each child gets a new flat scope rather than inheriting parent registrations. Depth and fork seeding reuse existing agent and session vocabulary: -- **Delegation depth** is a merge-extensible `AgentOptions.subagentDepth` field (`0` for a top-level agent, parent + 1 for a child). Only `undefined` means top level; every stored present value must be a non-negative safe integer. The seam owns it — the loop neither sets nor reads it — so a nested spawn validates its parent's stored depth, rejects a derived child depth outside the safe-integer domain, and applies a defined absolute `request.maxDepth` cap to that child. +- **Delegation depth** is durable `SessionHeader.delegationDepth` plus the merge-extensible runtime field `AgentOptions.subagentDepth`; absence means top-level depth zero, and the greater present value is authoritative. The seam owns both fields — the loop neither sets nor reads them — so an in-process child persists parent depth + 1, resume cannot lower it, and every start rejects a derived depth outside the safe-integer domain or above a defined absolute `request.maxDepth` cap. - **Fork seeding** uses `CreateAgentOptions.seed` (a `SessionEvent[]` prefix threaded through `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })`, the same primitive `resume` uses). The fork backend passes a *balanced completed-turn prefix* of the parent's log — the parent's events up to and including its last `turn/end` — so the seed is contiguous-from-0 and the [invariants](../../packages/support/invariants) replay accepts it (the in-flight, unbalanced turn is excluded). diff --git a/docs/core-data-structures/tools.md b/docs/core-data-structures/tools.md index 30ec86213a..9e3d698440 100644 --- a/docs/core-data-structures/tools.md +++ b/docs/core-data-structures/tools.md @@ -180,8 +180,8 @@ A tool body receives the runtime extension. `deferContext()` is the composite-to interface ToolRunContext extends ToolExecution { /** * Defer one nested-dispatch context until this tool's final result reaches - * the agent loop. Contexts retain their individual source, envelope, and - * metadata and are emitted in call order. + * the agent loop. Contexts retain their individual source and metadata and + * are emitted in call order. */ deferContext(context: HookContext): void } diff --git a/docs/defensive-patterns.md b/docs/defensive-patterns.md index cf30072094..fe74a9d19f 100644 --- a/docs/defensive-patterns.md +++ b/docs/defensive-patterns.md @@ -12,7 +12,7 @@ When an interface documents two valid ways to signal something — an adapter ma ## Async state is not synchronous state -`agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) rather than counting actions you assume map 1:1 to turns (the loop batches queued messages). The guard cuts both ways: if the awaited transition can never occur (EOF with no work submitted → never `running`), the wait hangs — handle the "nothing to wait for" branch explicitly. +`agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-send result: several queued sends run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items. The guard cuts both ways: if the awaited transition can never occur (EOF with no work submitted → never `running`), the wait hangs — handle the "nothing to wait for" branch explicitly. ## Dispose must reach quiescence, not just request it diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index e0d5914f6c..8f94417a12 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -8,35 +8,35 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:362`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | -| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:147`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:156`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | -| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:311`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`tui`](../packages/ui/tui) | -| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:264`](../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:204`](../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:214`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | -| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:175`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - | -| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:226`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp) | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:278`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic) | -| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:241`](../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:188`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal`](../packages/goal/goal), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`stdio`](../packages/ui/stdio) | -| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:165`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | -| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:252`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | -| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:288`](../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-stop` | `serial` | [`packages/core/agent/src/types.ts:298`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | +| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:150`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:159`](../packages/core/agent/src/types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | +| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:314`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`tui`](../packages/ui/tui) | +| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:267`](../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:207`](../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:217`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | +| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:178`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | - | +| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:229`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp) | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:281`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic) | +| `agent/session-prefix` | `waterfall` | [`packages/core/agent/src/types.ts:244`](../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:191`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal`](../packages/goal/goal), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`stdio`](../packages/ui/stdio) | +| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:168`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`invariants`](../packages/support/invariants), [`stdio`](../packages/ui/stdio), [`tui`](../packages/ui/tui) | +| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:255`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | - | +| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:291`](../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-stop` | `serial` | [`packages/core/agent/src/types.ts:301`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | `approval/request` | `waterfall` | [`packages/ui/user-approval/src/index.ts:31`](../packages/ui/user-approval/src/index.ts) | [`user-approval`](../packages/ui/user-approval) (`waterfall`) | [`acp`](../packages/ui/acp) | -| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:61`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | -| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:70`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) | -| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:53`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | +| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:62`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | +| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:71`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`) | [`fs-policy`](../packages/fs/fs-policy) | +| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:54`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | | `goal/changed` | `emit` | [`packages/goal/goal/src/types.ts:167`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | - | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:43`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`invariants`](../packages/support/invariants), [`llm-replay`](../packages/support/llm-replay) | | `session/created` | `emit` | [`packages/core/session/src/index.ts:47`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:57`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`session-persistence`](../packages/session-persistence/session-persistence) | | `session/event` | `emit` | [`packages/core/session/src/index.ts:69`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/ui/acp), [`cli-demo`](../packages/examples/cli-demo), [`invariants`](../packages/support/invariants), [`jsonrpc`](../packages/ui/jsonrpc), [`session-persistence`](../packages/session-persistence/session-persistence), [`stdio`](../packages/ui/stdio), [`token-meter`](../packages/llm/token-meter), [`tui`](../packages/ui/tui), [`workspace-context`](../packages/context/workspace-context) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:79`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session-persistence/session-persistence) | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:112`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:86`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:92`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:103`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`jsonrpc`](../packages/ui/jsonrpc) | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:113`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:119`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:130`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:27`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`acp`](../packages/ui/acp) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:33`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:116`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - | diff --git a/docs/glossary.md b/docs/glossary.md index 13b800d9a8..c8ec6f378d 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -14,7 +14,7 @@ FIXME(glossary-completeness): Expand this glossary before the first release so i - **shadowing** — most-specific-wins name resolution: a scoped tool/section/variable replaces its same-named global twin for that scope alone. The per-agent persona and per-agent tool-variant mechanism. - **restriction / scope-local registration** — a restriction (`tools.restrict`) filters the GLOBAL tool surface for one scope (compose by intersection); scope-local registrations are merged after that filter. A filtered-away global tool is absent from the prompt AND refuses execution, indistinguishably from a nonexistent one. - **setup window** — the creation slot where a creator composes an agent's scoped world (`CreateAgentOptions.setup`): after the scope and agent object exist but before the agent or session is published, `agent/session-start` fires, or the first prompt is assembled. Setup registers; it never drives the agent. -- **lineage** — parent/child facts carried as data (`parentSession`, `subagentDepth`); never affects visibility. +- **lineage** — parent/child facts carried as data (`parentSession`, durable `delegationDepth`, runtime `subagentDepth`); never affects visibility. ## goal diff --git a/docs/i18n/style-samples.md b/docs/i18n/style-samples.md index 2d9de63bc6..dd970b7c12 100644 --- a/docs/i18n/style-samples.md +++ b/docs/i18n/style-samples.md @@ -28,9 +28,9 @@ **dispose(资源释放)必须等待所有任务完全停稳,不能仅下发终止指令就返回**:如果清理过程只发出终止或中断信号,却不等任务停止就返回,就会留下孤儿进程。清理应采用异步方式,等待所有子任务彻底退出(先发出终止信号,再等待退出);发出信号前应先关闭监听器与通知注册表,使延迟到达的完成事件不再触发通知。测试要证明 dispose 的确等到清理完成:执行完 `await fiber.dispose()` 后进程 PID 立即消失,不能只检查进程最终会自行消亡。 -> **Async state is not synchronous state** — `agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) rather than counting actions you assume map 1:1 to turns. +> **Async state is not synchronous state** — `agent.send()` does not flip status before returning; a background task's completion races turn boundaries; `reader.close()` fires for both EOF and disposal. Never gate control flow on a status you only just requested — drive lifecycle off the events/promises that actually fire (`agent/status`, `task.done`), and observe the transition (saw `running` THEN `idle`) instead of treating status as a per-send result: several queued sends run as consecutive turns under one `running` interval, while cancellation or disposal can discard unstarted items. -**异步状态不等同于同步瞬时状态**:调用 `agent.send()` 不会在返回前同步更新状态;后台任务的完成时间与轮次边界存在竞态;`reader.close()` 既会在读到文件末尾时触发,也会在资源释放时触发。切勿把刚刚发起的状态变更当成已经生效,据此控制流程;生命周期逻辑应以实际触发的事件和已完成的 promise(`agent/status`、`task.done`)为准,并观察完整的状态变化(先 `running`,再 `idle`),不要根据操作次数推断操作与轮次一一对应。 +**异步状态不等同于同步瞬时状态**:调用 `agent.send()` 不会在返回前同步更新状态;后台任务的完成时间与轮次边界存在竞态;`reader.close()` 既会在读到文件末尾时触发,也会在资源释放时触发。切勿把刚刚发起的状态变更当成已经生效,据此控制流程;生命周期逻辑应以实际触发的事件和已完成的 promise(`agent/status`、`task.done`)为准,并观察完整的状态变化(先 `running`,再 `idle`),不要把状态当作逐次 `send()` 的结果:多次排队的 `send()` 会作为连续轮次运行,但可能共用一个 `running` 区间;取消或资源释放还可能丢弃尚未启动的队列项。 ## ③ 测试政策清单 diff --git a/docs/module-graph.md b/docs/module-graph.md index 4c53a9bfad..d794b526ae 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -41,6 +41,7 @@ flowchart TD pkg_fs["fs"] pkg_fs_local["fs-local"] pkg_fs_policy["fs-policy"] + pkg_fs_sandbox["fs-sandbox"] pkg_tool_fs["tool-fs"] pkg_tool_fs_search["tool-fs-search"] end @@ -52,6 +53,7 @@ flowchart TD subgraph group_compact["packages/compact"] pkg_compact["compact"] pkg_compact_basic["compact-basic"] + pkg_compact_tool_result_prune["compact-tool-result-prune"] end subgraph group_subagent["packages/subagent"] pkg_subagent["subagent"] @@ -139,6 +141,7 @@ flowchart TD subgraph group_sandbox["packages/sandbox"] pkg_sandbox["sandbox"] pkg_sandbox_local["sandbox-local"] + pkg_sandbox_policy["sandbox-policy"] end subgraph group_sdk["packages/sdk"] pkg_helper["helper"] @@ -166,8 +169,6 @@ flowchart TD pkg_session --> pkg_scope pkg_system_prompt --> pkg_llm pkg_system_prompt --> pkg_scope - pkg_fs --> pkg_brand - pkg_fs --> pkg_llm pkg_web --> pkg_llm pkg_sandbox --> pkg_llm pkg_token_meter --> pkg_llm @@ -178,14 +179,13 @@ flowchart TD pkg_agent --> pkg_session pkg_agent --> pkg_system_prompt pkg_bash --> pkg_sandbox - pkg_bash --> pkg_session - pkg_fs_local --> pkg_fs - pkg_fs_policy --> pkg_fs - pkg_skill_local --> pkg_fs - pkg_skill_local --> pkg_home - pkg_skill_local --> pkg_skill + pkg_fs --> pkg_brand + pkg_fs --> pkg_llm + pkg_fs --> pkg_sandbox pkg_compact --> pkg_llm pkg_compact --> pkg_session + pkg_compact_tool_result_prune --> pkg_llm + pkg_compact_tool_result_prune --> pkg_session pkg_web_fetch_local --> pkg_timeout pkg_web_fetch_local --> pkg_web pkg_web_search_deepseek --> pkg_web @@ -199,6 +199,8 @@ flowchart TD pkg_llm_replay --> pkg_session pkg_sandbox_local --> pkg_llm pkg_sandbox_local --> pkg_sandbox + pkg_sandbox_policy --> pkg_sandbox + pkg_sandbox_policy --> pkg_session pkg_goal --> pkg_agent pkg_goal --> pkg_brand pkg_goal --> pkg_llm @@ -206,8 +208,14 @@ flowchart TD pkg_goal --> pkg_session pkg_bash_local --> pkg_bash pkg_bash_local --> pkg_timeout + pkg_fs_local --> pkg_fs + pkg_fs_policy --> pkg_fs + pkg_skill_local --> pkg_fs + pkg_skill_local --> pkg_home + pkg_skill_local --> pkg_skill pkg_compact_basic --> pkg_agent pkg_compact_basic --> pkg_compact + pkg_compact_basic --> pkg_compact_tool_result_prune pkg_compact_basic --> pkg_llm pkg_compact_basic --> pkg_session pkg_compact_basic --> pkg_token_meter @@ -252,8 +260,14 @@ flowchart TD pkg_bash_sandbox --> pkg_bash pkg_bash_sandbox --> pkg_bash_local pkg_bash_sandbox --> pkg_sandbox + pkg_bash_sandbox --> pkg_sandbox_policy + pkg_fs_sandbox --> pkg_fs + pkg_fs_sandbox --> pkg_fs_local + pkg_fs_sandbox --> pkg_sandbox + pkg_fs_sandbox --> pkg_sandbox_policy pkg_permission --> pkg_bash pkg_permission --> pkg_sandbox + pkg_permission --> pkg_sandbox_policy pkg_permission --> pkg_session pkg_permission --> pkg_user_approval pkg_agent_loop --> pkg_agent @@ -268,6 +282,7 @@ flowchart TD pkg_tool_bash --> pkg_home pkg_tool_bash --> pkg_llm pkg_tool_bash --> pkg_sandbox + pkg_tool_bash --> pkg_sandbox_policy pkg_tool_bash --> pkg_session_persistence pkg_tool_bash --> pkg_system_prompt pkg_tool_bash --> pkg_tasks @@ -275,9 +290,12 @@ flowchart TD pkg_tool_bash --> pkg_user_approval pkg_tool_fs --> pkg_fs pkg_tool_fs --> pkg_llm + pkg_tool_fs --> pkg_sandbox + pkg_tool_fs --> pkg_sandbox_policy pkg_tool_fs --> pkg_session pkg_tool_fs --> pkg_system_prompt pkg_tool_fs --> pkg_tools + pkg_tool_fs --> pkg_user_approval pkg_tool_fs_search --> pkg_bash pkg_tool_fs_search --> pkg_llm pkg_tool_fs_search --> pkg_retention @@ -478,16 +496,14 @@ flowchart TD | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`llm`](../packages/llm/llm) | | [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`system-prompt`](../packages/core/system-prompt) | `core` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | -| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) | | [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) | | [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm) | | [`token-meter`](../packages/llm/token-meter) | `llm` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`agent`](../packages/core/agent) | `core` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | -| [`bash`](../packages/bash/bash) | `bash` | [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | -| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) | -| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs) | -| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`home`](../packages/util/home), [`skill`](../packages/skill/skill) | +| [`bash`](../packages/bash/bash) | `bash` | [`sandbox`](../packages/sandbox/sandbox) | +| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | | [`compact`](../packages/compact/compact) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | +| [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) | | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`web`](../packages/web/web) | | [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`web`](../packages/web/web) | @@ -496,9 +512,13 @@ flowchart TD | [`session-persistence`](../packages/session-persistence/session-persistence) | `session-persistence` | [`session`](../packages/core/session) | | [`llm-replay`](../packages/support/llm-replay) | `support` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | +| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | | [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session) | | [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`timeout`](../packages/util/timeout) | -| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | +| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) | +| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs) | +| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`home`](../packages/util/home), [`skill`](../packages/skill/skill) | +| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`compact-tool-result-prune`](../packages/compact/compact-tool-result-prune), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) | | [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`session`](../packages/core/session) | | [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) | @@ -511,11 +531,12 @@ flowchart TD | [`tasks`](../packages/tasks/tasks) | `tasks` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | | [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) | -| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox) | -| [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) | +| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) | +| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) | +| [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) | | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`home`](../packages/util/home), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | -| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`home`](../packages/util/home), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | +| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 688d3b545c..209fb04d1c 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -79,7 +79,7 @@ export type SessionEvent = { }[T] ``` -Sources: [`packages/core/session/src/types.ts:255`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:262`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:292`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:324`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:270`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:300`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts) ## Events @@ -151,7 +151,7 @@ Source: [`packages/ui/user-approval/src/index.ts:68`](../packages/ui/user-approv Types: [StreamChunk](core-data-structures/llm-streaming.md) -Source: [`packages/core/session/src/types.ts:219`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:227`](../packages/core/session/src/types.ts) #### `assistant/message` — surface @@ -167,22 +167,7 @@ Source: [`packages/core/session/src/types.ts:219`](../packages/core/session/src/ Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md) -Source: [`packages/core/session/src/types.ts:226`](../packages/core/session/src/types.ts) - -### `bash/*` - -#### `bash/sandbox-mode` — log-only - -```ts persistence-catalog -/** - * Durable log-only sandbox-mode override; never a surface event or model - * message. Execution and ACP option reporting fold the latest event through - * {@link effectiveSandboxMode} without adding a prompt notice. - */ -'bash/sandbox-mode': { mode: SandboxMode } -``` - -Source: [`packages/bash/bash/src/session-mode.ts:20`](../packages/bash/bash/src/session-mode.ts) +Source: [`packages/core/session/src/types.ts:234`](../packages/core/session/src/types.ts) ### `compact/*` @@ -244,21 +229,24 @@ Source: [`packages/compact/compact/src/types.ts:22`](../packages/compact/compact /** * In-session context injection (file-change notices, subdir AGENTS.md, * skill content, cron notifications, …). Rendered into the derived history - * as synthetic context — NOT a user prompt. `envelope: 'raw'` lets a caller - * own the complete model-facing frame; `meta` is durable JSON state omitted - * from the model projection. + * as a synthetic user-role message carrying `content` verbatim — NOT a + * user prompt. `meta` is durable JSON state omitted from the model + * projection; it is also the intended channel for any future framing + * directive (a producer declares the frame, a dedicated renderer applies it — + * see the deferred note in + * ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md), + * so the surface keeps projecting `content` verbatim rather than wrapping it. */ 'context/message': { content: ContentBlock[] source: MessageSource - envelope?: ContextEnvelope meta?: JsonValue } ``` Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md) -Source: [`packages/core/session/src/types.ts:212`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:221`](../packages/core/session/src/types.ts) ### `hook/*` @@ -320,7 +308,7 @@ Source: [`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook- 'permission/preset': { preset: string } ``` -Source: [`packages/ui/permission/src/index.ts:33`](../packages/ui/permission/src/index.ts) +Source: [`packages/ui/permission/src/index.ts:36`](../packages/ui/permission/src/index.ts) ### `prompt/*` @@ -329,14 +317,14 @@ Source: [`packages/ui/permission/src/index.ts:33`](../packages/ui/permission/src ```ts persistence-catalog /** * Durable record of a prompt veto and its reason. It is log-only: the blocked - * prompt never enters the model-visible surface, including in a mixed batch. + * prompt never enters the model-visible surface, and its turn runs zero steps. */ 'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string } ``` Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md) -Source: [`packages/core/session/src/types.ts:204`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:209`](../packages/core/session/src/types.ts) ### `request/*` @@ -350,7 +338,25 @@ Source: [`packages/core/session/src/types.ts:204`](../packages/core/session/src/ 'request/header': { header: EpochHeader; reason: RequestHeaderReason } ``` -Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts) + +### `sandbox/*` + +#### `sandbox/mode` — log-only + +```ts persistence-catalog +/** + * The session's sandbox mode was switched — log-only (like `approval/*`; + * NOT a surface event, carries no `surfaceOp`): durable and replayable, + * never in the model transcript. The LAST such event is the session's + * override ({@link effectiveSandboxMode}); who asked for it is derivable + * from position (an event after the log's last `request/header*` was a + * runtime switch by the user; see the tool layer's narrator). + */ +'sandbox/mode': { mode: SandboxMode } +``` + +Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:34`](../packages/sandbox/sandbox-policy/src/session-mode.ts) ### `steering/*` @@ -363,7 +369,7 @@ Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/ Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md) -Source: [`packages/core/session/src/types.ts:244`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts) ### `step/*` @@ -374,7 +380,7 @@ Source: [`packages/core/session/src/types.ts:244`](../packages/core/session/src/ 'step/end': { turn: number; step: number } ``` -Source: [`packages/core/session/src/types.ts:197`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:202`](../packages/core/session/src/types.ts) #### `step/start` — log-only @@ -383,7 +389,7 @@ Source: [`packages/core/session/src/types.ts:197`](../packages/core/session/src/ 'step/start': { turn: number; step: number } ``` -Source: [`packages/core/session/src/types.ts:195`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:200`](../packages/core/session/src/types.ts) ### `todo/*` @@ -396,7 +402,7 @@ Source: [`packages/core/session/src/types.ts:195`](../packages/core/session/src/ Types: [TodoItem](core-data-structures/session.md) -Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:254`](../packages/core/session/src/types.ts) ### `tool/*` @@ -413,7 +419,7 @@ Source: [`packages/core/session/src/types.ts:246`](../packages/core/session/src/ Types: [CallId](core-data-structures/core.md) -Source: [`packages/core/session/src/types.ts:232`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:240`](../packages/core/session/src/types.ts) #### `tool/code-dispatch` — log-only @@ -457,7 +463,7 @@ Source: [`packages/core/tools/src/code-mode.ts:34`](../packages/core/tools/src/c Types: [CallId](core-data-structures/core.md) · [ContentBlock](core-data-structures/core.md) -Source: [`packages/core/session/src/types.ts:242`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:250`](../packages/core/session/src/types.ts) ### `turn/*` @@ -466,22 +472,23 @@ Source: [`packages/core/session/src/types.ts:242`](../packages/core/session/src/ ```ts persistence-catalog /** * Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop - * fires the awaited `session/flush` checkpoint at every turn end, so the turn - * boundary is also the durable-commit boundary. + * awaits `session/flush` after an ordinary turn ends before claiming the next + * queued item. Success commits the turn; rejection is reported live and does + * not prevent later work. */ 'turn/end': { turn: number; reason: TurnEndReason } ``` Types: [TurnEndReason](core-data-structures/session.md) -Source: [`packages/core/session/src/types.ts:193`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:198`](../packages/core/session/src/types.ts) #### `turn/start` — log-only ```ts persistence-catalog /** - * Opens turn `turn`. `trigger` records what started it — a drained message - * batch or an idle-time injection. The turn is the durability/replay + * Opens turn `turn`. `trigger` records what started it — one claimed queued + * message or an idle-time injection. The turn is the durability/replay * boundary: every event sits between a `turn/start` and its matching * `turn/end` (the turn-enclosure invariant). */ @@ -490,17 +497,17 @@ Source: [`packages/core/session/src/types.ts:193`](../packages/core/session/src/ Types: [TurnTrigger](core-data-structures/session.md) -Source: [`packages/core/session/src/types.ts:187`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:191`](../packages/core/session/src/types.ts) ### `user/*` #### `user/message` — surface ```ts persistence-catalog -/** A user-visible prompt (queued message drained at turn start). */ +/** A user-visible prompt (the queued message claimed for this turn). */ 'user/message': { content: ContentBlock[]; source: MessageSource } ``` Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md) -Source: [`packages/core/session/src/types.ts:199`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:204`](../packages/core/session/src/types.ts) diff --git a/docs/user/develop/basic/config.i18n.yaml b/docs/user/develop/basic/config.i18n.yaml new file mode 100644 index 0000000000..7740eda954 --- /dev/null +++ b/docs/user/develop/basic/config.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 +config.md: 26d2d48ebede74194fbf306aa97d214bdb99b722 +config.zh.md: 9ed389b16779f25c633d0c8772f8658197ba4322 diff --git a/docs/user/develop/basic/config.md b/docs/user/develop/basic/config.md new file mode 100644 index 0000000000..26d2d48ebe --- /dev/null +++ b/docs/user/develop/basic/config.md @@ -0,0 +1,118 @@ +# Plugin configuration + +English | [中文](config.zh.md) + +Accept configuration supplied through `cordis.yml`. + +## Define the Config type + +Export a `Config` type and a same-named Schemastery schema. Put defaults directly on the schema fields: + +```ts +import type { Context } from 'cordis' +import Schema from 'schemastery' + +export const name = 'my-plugin' + +export interface Config { + greeting: string + maxRetries: number + verbose?: boolean +} + +export const Config: Schema = Schema.object({ + greeting: Schema.string().default('Hello'), + maxRetries: Schema.number().default(3), + verbose: Schema.boolean().default(false), +}) + +export function apply(ctx: Context, config: Config) { + console.log(config.greeting) // User value or schema default. +} +``` + +Configure it in `cordis.yml`: + +```yaml +- name: './src/my-plugin.ts' + config: + greeting: 'Hi there' + maxRetries: 5 +``` + +When loading the plugin, Cordis uses the exported schema to validate configuration and fill defaults. Do not export a plain object as `Config`; it does not implement the Standard Schema interface required by Cordis. + +## Schema validation + +Use Schemastery to express stricter validation: + +```ts +import type { Context } from 'cordis' +import Schema from 'schemastery' + +export const name = 'validated-plugin' + +export interface Config { + apiKey: string + timeout: number + mode: 'fast' | 'accurate' +} + +export const Config = Schema.object({ + apiKey: Schema.string().required(), + timeout: Schema.number().default(30000), + mode: Schema.union(['fast', 'accurate']).default('fast'), +}) + +export function apply(ctx: Context, config: Config) { + // config is validated and type-safe. +} +``` + +The schema runs while the plugin loads. Invalid configuration fails the load with an actionable error. + +## Design principles + +### Do not hardcode tunable values + +Harness requires **anything that two deployments may want to set differently to be a configuration field**. + +```ts +// Wrong: hardcoded timeout. +const TIMEOUT = 30000 + +// Correct: configurable. +export interface Config { + timeoutMs: number // Defaults to 30000. +} +``` + +The test is whether `cordis.yml` can change the value without a code edit. + +### Fail loudly on invalid configuration + +If configuration refers to an unregistered LLM provider route or another nonexistent resource, fail early instead of silently skipping it: + +```ts +import type { Context } from 'cordis' +import type {} from '@deepseek-ai/dsh-llm' + +export interface ModelConfig { + provider: string +} + +export function apply(ctx: Context, config: ModelConfig) { + if (!ctx.llm.listProviders().some(provider => provider.id === config.provider)) { + throw new Error(`LLM provider "${config.provider}" is not registered`) + } +} +``` + +## Work with HMR + +A configuration edit hot-replaces the plugin: the framework unloads the old instance and loads a new one. Because registrations are effects and clean themselves up, replacement does not retain the old instance's registrations. + +## Next steps + +- [Plugins and lifecycle](../framework/) — understand the full plugin lifecycle +- [Services and dependencies](../framework/service.md) — provide a service to other plugins diff --git a/website/zh-CN/develop/basic/config.md b/docs/user/develop/basic/config.zh.md similarity index 53% rename from website/zh-CN/develop/basic/config.md rename to docs/user/develop/basic/config.zh.md index c912f49111..9ed389b167 100644 --- a/website/zh-CN/develop/basic/config.md +++ b/docs/user/develop/basic/config.zh.md @@ -1,24 +1,33 @@ # 插件配置 +[English](config.md) | 中文 + 让你的插件接受用户在 `cordis.yml` 中传入的配置。 ## 定义 Config 类型 -在插件中导出一个 `Config` 类型,`apply` 的第二个参数就是用户配置: +在插件中导出一个 `Config` 类型和同名的 Schemastery schema;默认值直接写在 schema 中: ```ts import type { Context } from 'cordis' +import Schema from 'schemastery' export const name = 'my-plugin' export interface Config { - greeting?: string - maxRetries?: number + greeting: string + maxRetries: number verbose?: boolean } +export const Config: Schema = Schema.object({ + greeting: Schema.string().default('Hello'), + maxRetries: Schema.number().default(3), + verbose: Schema.boolean().default(false), +}) + export function apply(ctx: Context, config: Config) { - console.log(config.greeting ?? 'Hello') // 用户配置或默认值 + console.log(config.greeting) // User value or schema default. } ``` @@ -31,32 +40,32 @@ export function apply(ctx: Context, config: Config) { maxRetries: 5 ``` -只导出类型时,配置原样传入,默认值由代码自己兜底(如上面的 `??`)。想让框架代管默认值和校验,导出一个 schema(见下节)。 +插件加载时,Cordis 会通过导出的 schema 校验配置,并填充未提供字段的默认值。不要导出普通对象作为 `Config`,因为它不满足 Cordis 要求的 Standard Schema 接口。 ## Schema 校验 -对于需要默认值和严格校验的场景,额外导出一个 Schemastery schema(仓库约定以 `z` 引入)。加载时框架先用它校验并填充默认值,再把结果传给 `apply`: +对于需要严格校验的场景,使用 Schemastery 定义 schema: ```ts import type { Context } from 'cordis' -import z from 'schemastery' +import Schema from 'schemastery' export const name = 'validated-plugin' export interface Config { apiKey: string - timeout?: number - mode?: 'fast' | 'accurate' + timeout: number + mode: 'fast' | 'accurate' } -export const Config: z = z.object({ - apiKey: z.string().required(), - timeout: z.number().default(30000), - mode: z.union(['fast', 'accurate'] as const).default('fast'), +export const Config = Schema.object({ + apiKey: Schema.string().required(), + timeout: Schema.number().default(30000), + mode: Schema.union(['fast', 'accurate']).default('fast'), }) export function apply(ctx: Context, config: Config) { - // config 已经过校验,类型安全,默认值已填充 + // config is validated and type-safe. } ``` @@ -69,13 +78,12 @@ Schema 在插件加载时执行校验。如果配置不合法,插件会加载 Harness 的约定:**任何两个部署可能想要不同值的东西,都应该是配置字段**。 ```ts -// 错误 — 硬编码超时时间 +// Wrong: hardcoded timeout. const TIMEOUT = 30000 -// 正确 — 可配置 +// Correct: configurable. export interface Config { - /** 默认 30000 */ - timeoutMs?: number + timeoutMs: number // Defaults to 30000. } ``` @@ -83,26 +91,23 @@ export interface Config { ### 配置错误要响亮 -如果配置引用了不存在的东西(比如一个未注册的 LLM 提供方路由),应该尽早报错,而不是静默跳过: +如果配置引用了未注册的 LLM 提供方路由或其他不存在的资源,应该尽早报错,而不是静默跳过: ```ts import type { Context } from 'cordis' import type {} from '@deepseek-ai/dsh-llm' -export interface Config { +export interface ModelConfig { provider: string - model: string } -export function apply(ctx: Context, config: Config) { +export function apply(ctx: Context, config: ModelConfig) { if (!ctx.llm.listProviders().some(provider => provider.id === config.provider)) { throw new Error(`LLM provider "${config.provider}" is not registered`) } } ``` -模型目录只用于发现;适配器可能接受目录之外的模型 ID,因此不能把 `listModels()` 当作请求白名单。 - ## 配合 HMR 配置变更会触发插件热替换:修改 `cordis.yml` 中某个插件的 `config`,框架会卸载旧实例、加载新实例。由于注册都是效果(自动清理),这个过程是安全的。 @@ -110,4 +115,4 @@ export function apply(ctx: Context, config: Config) { ## 下一步 - [插件与生命周期](../framework/) — 深入了解插件的完整生命周期 -- [服务与依赖](../framework/service) — 让你的插件对外提供服务 +- [服务与依赖](../framework/service.md) — 让你的插件对外提供服务 diff --git a/docs/user/develop/basic/index.i18n.yaml b/docs/user/develop/basic/index.i18n.yaml new file mode 100644 index 0000000000..22b03af93e --- /dev/null +++ b/docs/user/develop/basic/index.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 +index.md: 5fa46806bc195ad2566fc0a29b45eb1dd7a68179 +index.zh.md: a6d238c12841c8c25b00376ee032e5db50fc6b4e diff --git a/docs/user/develop/basic/index.md b/docs/user/develop/basic/index.md new file mode 100644 index 0000000000..5fa46806bc --- /dev/null +++ b/docs/user/develop/basic/index.md @@ -0,0 +1,151 @@ +# Your first plugin + +English | [中文](index.zh.md) + +This guide creates a minimal Harness plugin and loads it into an agent. + +## What is a plugin? + +In Harness, a plugin is a TypeScript module that exports an `apply` function. The framework calls `apply` when loading the plugin and passes a `ctx` context object through which the plugin registers capabilities: + +```ts +import type { Context } from 'cordis' + +export const name = 'my-plugin' + +export function apply(ctx: Context) { + // Register capabilities here. +} +``` + +That is the complete shape. + +## Create the plugin file + +Create `src/my-plugin.ts` in your project: + +```ts +import type { Context } from 'cordis' + +export const name = 'hello-plugin' + +export function apply(ctx: Context) { + // Required dependencies are ready before apply runs. + console.log('[hello-plugin] plugin loaded!') +} +``` + +## Register it in cordis.yml + +Add an entry to `cordis.yml`: + +```yaml +- id: hello + name: './src/my-plugin.ts' +``` + +After startup, the console prints `[hello-plugin] plugin loaded!`. + +## Automatic cleanup + +Anything registered through `ctx`—event listeners, tools, or timers—is cleaned up when the plugin unloads. You do not need to call removeListener or clearInterval manually. + +For a resource that needs explicit cleanup, such as a network connection, use `ctx.effect()` to provide its disposer: + +```ts +import type { Context } from 'cordis' + +export function apply(ctx: Context) { + ctx.effect(() => { + const timer = setInterval(() => { + console.log('heartbeat') + }, 5000) + + // The returned function runs when the plugin unloads. + return () => clearInterval(timer) + }) +} +``` + +## Declare dependencies + +If the plugin consumes another service such as `tools` or `llm`, declare it in `inject`: + +```ts ignore-check +import type { Context } from 'cordis' + +export const name = 'my-tool-plugin' +export const inject = ['tools'] + +export function apply(ctx: Context) { + // ctx.tools is ready here. + ctx.tools.register(/* ... */) +} +``` + +The framework waits for every required service before loading the plugin. + +## Three plugin forms + +In addition to a function module, a plugin can use object or class form. + +### Object form + +```ts +import type { Context } from 'cordis' + +export default { + name: 'my-plugin', + inject: ['tools'], + apply(ctx: Context) { + // ... + }, +} +``` + +### Class form + +```ts +import { Service, type Context } from 'cordis' + +export default class MyService extends Service { + static inject = ['tools'] + + constructor(ctx: Context) { + super(ctx, 'myService') + // Perform synchronous initialization in the constructor. + } +} +``` + +Function form is sufficient in most cases. Use class form when the plugin provides a service to other plugins; see [services and dependencies](../framework/service.md). + +## Complete example + +`examples/echo-agent/src/echo-tool.ts` is a plugin that registers a tool: + +```ts +import type { Context } from 'cordis' +import { defineTool } from '@deepseek-ai/dsh-tools' + +export const name = 'echo-tool' +export const inject = ['tools'] + +export function apply(ctx: Context) { + ctx.tools.register(defineTool({ + name: 'echo', + description: 'Echo the given text back, uppercased.', + parameters: { + text: { type: 'string', required: true }, + }, + async execute(args) { + return [{ type: 'text', text: `ECHO: ${args.text.toUpperCase()}` }] + }, + })) +} +``` + +## Next steps + +- [Build a tool](./tool.md) — learn the tool definition DSL +- [Plugin configuration](./config.md) — accept user configuration diff --git a/website/zh-CN/develop/basic/index.md b/docs/user/develop/basic/index.zh.md similarity index 78% rename from website/zh-CN/develop/basic/index.md rename to docs/user/develop/basic/index.zh.md index 81862c4d36..a6d238c128 100644 --- a/website/zh-CN/develop/basic/index.md +++ b/docs/user/develop/basic/index.zh.md @@ -1,5 +1,7 @@ # 第一个插件 +[English](index.md) | 中文 + 本文带你编写一个最小的 Harness 插件并加载到 Agent 中。 ## 插件是什么 @@ -12,7 +14,7 @@ import type { Context } from 'cordis' export const name = 'my-plugin' export function apply(ctx: Context) { - // 在这里注册能力 + // Register capabilities here. } ``` @@ -28,8 +30,8 @@ import type { Context } from 'cordis' export const name = 'hello-plugin' export function apply(ctx: Context) { - // apply 函数体在插件加载时执行 - console.log('[hello-plugin] 插件已加载!') + // Required dependencies are ready before apply runs. + console.log('[hello-plugin] plugin loaded!') } ``` @@ -42,7 +44,7 @@ export function apply(ctx: Context) { name: './src/my-plugin.ts' ``` -启动后你会在控制台看到 `[hello-plugin] 插件已加载!`。 +启动后你会在控制台看到 `[hello-plugin] plugin loaded!`。 ## 自动清理 @@ -59,7 +61,7 @@ export function apply(ctx: Context) { console.log('heartbeat') }, 5000) - // 返回的函数会在插件卸载时被调用 + // The returned function runs when the plugin unloads. return () => clearInterval(timer) }) } @@ -69,23 +71,15 @@ export function apply(ctx: Context) { 如果你的插件需要使用其他服务(如 `tools`、`llm`),需要声明 `inject`: -```ts +```ts ignore-check import type { Context } from 'cordis' -import { defineTool } from '@deepseek-ai/dsh-tools' export const name = 'my-tool-plugin' export const inject = ['tools'] export function apply(ctx: Context) { - // ctx.tools 现在可用 - ctx.tools.register(defineTool({ - name: 'demo', - description: 'Demo tool.', - parameters: {}, - async execute() { - return [] - }, - })) + // ctx.tools is ready here. + ctx.tools.register(/* ... */) } ``` @@ -99,7 +93,6 @@ export function apply(ctx: Context) { ```ts import type { Context } from 'cordis' -import type {} from '@deepseek-ai/dsh-tools' export default { name: 'my-plugin', @@ -114,23 +107,18 @@ export default { ```ts import { Service, type Context } from 'cordis' -import type {} from '@deepseek-ai/dsh-tools' export default class MyService extends Service { static inject = ['tools'] constructor(ctx: Context) { super(ctx, 'myService') - } - - // 服务的公开方法 - greet(name: string) { - return `Hello, ${name}!` + // Perform synchronous initialization in the constructor. } } ``` -大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service))。 +大多数情况下,函数形式足够了。类形式用于需要对外提供服务的插件(见 [服务与依赖](../framework/service.md))。 ## 完整示例 @@ -159,5 +147,5 @@ export function apply(ctx: Context) { ## 下一步 -- [开发一个 Tool](tool) — 详细了解 tool 定义 DSL -- [插件配置](config) — 让插件接受用户配置 +- [开发一个 Tool](./tool.md) — 详细了解 tool 定义 DSL +- [插件配置](./config.md) — 让插件接受用户配置 diff --git a/docs/user/develop/basic/tool.i18n.yaml b/docs/user/develop/basic/tool.i18n.yaml new file mode 100644 index 0000000000..d2f4343cf1 --- /dev/null +++ b/docs/user/develop/basic/tool.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 +tool.md: 416733bcb584fa5303a8b3ba5e6e904302e7f992 +tool.zh.md: fce9a7d9b973853c8b4fb9ae2c034e749d8da999 diff --git a/docs/user/develop/basic/tool.md b/docs/user/develop/basic/tool.md new file mode 100644 index 0000000000..416733bcb5 --- /dev/null +++ b/docs/user/develop/basic/tool.md @@ -0,0 +1,208 @@ +# Build a tool + +English | [中文](tool.zh.md) + +A tool is a capability the model can call. This guide builds one with `defineTool`. + +## Minimal example + +```ts +import type { Context } from 'cordis' +import { defineTool } from '@deepseek-ai/dsh-tools' + +export const name = 'my-tool' +export const inject = ['tools'] + +export function apply(ctx: Context) { + ctx.tools.register(defineTool({ + name: 'greet', + description: 'Greet someone by name.', + parameters: { + name: { type: 'string', required: true, description: 'The name to greet' }, + }, + async execute(args) { + // args is inferred as { name: string }. + return [{ type: 'text', text: `Hello, ${args.name}!` }] + }, + })) +} +``` + +## Parameter definitions + +`parameters` uses a compact format that the framework converts to the JSON Schema sent to the model. + +### Primitive types + +```ts +export const parameters = { + path: { type: 'string', required: true }, + limit: { type: 'number' }, + recursive: { type: 'boolean' }, +} +// Inferred type: { path: string; limit?: number; recursive?: boolean } +``` + +### Enums + +```ts +export const parameters = { + mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] }, +} +// Inferred type: { mode: string } (enum values are validated at runtime) +``` + +### Nested objects + +```ts +export const parameters = { + options: { + type: 'object', + properties: { + timeout: { type: 'number' }, + retries: { type: 'number' }, + }, + }, +} +// Inferred type: { options?: { timeout?: number; retries?: number } } +``` + +### Arrays + +```ts +export const parameters = { + tags: { + type: 'array', + items: { type: 'string' }, + }, +} +// Inferred type: { tags?: string[] } +``` + +### Property fields + +| Field | Type | Meaning | +|------|------|------| +| `type` | `'string' \| 'number' \| 'boolean' \| 'object' \| 'array'` | Value type | +| `required` | `true` | Marks the property required and affects inference | +| `description` | `string` | Description sent to the model | +| `enum` | `string[]` | Allowed string values | +| `properties` | `SchemaSpec` | Nested properties for an object | +| `items` | `SchemaProp` | Element schema for an array | + +## The execute function + +`execute` receives validated, inferred `args` and an `exec` execution context: + +```ts +import { defineTool } from '@deepseek-ai/dsh-tools' + +export const tool = defineTool({ + name: 'example', + description: 'Return an example result.', + parameters: {}, + async execute(args, exec) { + // args: inferred from parameters + // exec: ToolExecution context + + // Return a ContentBlock array. + void args + void exec + return [{ type: 'text', text: 'result here' }] + }, +}) +``` + +### Return value + +`execute` returns a `ContentBlock[]` that becomes the tool result visible to the model: + +```ts ignore-check +// Text result +return [{ type: 'text', text: 'file content here...' }] + +// Multiple blocks +return [ + { type: 'text', text: 'Found 3 matches:' }, + { type: 'text', text: matchResults.join('\n') }, +] +``` + +### Argument validation + +Before calling `execute`, `defineTool` validates model-generated arguments. Invalid input raises `ToolArgsError`; the framework turns it into an `isError` result so the model can correct its call. + +Do not repeat type validation inside `execute`. + +## Presentation + +A tool can define UI presentation methods for terminal and ACP clients: + +```ts ignore-check +defineTool({ + name: 'bash', + // ... + presentCall(args) { + return { + card: 'terminal', + title: args.command, + } + }, + presentResult(args, result) { + return { + card: 'terminal', + output: result.content.map(b => b.type === 'text' ? b.text : '').join(''), + } + }, +}) +``` + +`presentCall` and `presentResult` are **pure functions**. Streaming UI and session replay may call them more than once. + +## Registration and unloading + +`ctx.tools.register()` returns a disposer, but a registration made through `ctx` is already tracked by the framework. Unloading the plugin removes the tool automatically, so the plugin does not call the disposer itself. + +```ts ignore-check +// This is sufficient: +ctx.tools.register(defineTool({ /* ... */ })) + +// No saved disposer or extra cleanup registration is needed. +``` + +## Complete example + +This tool counts files in a directory: + +```ts +import type { Context } from 'cordis' +import { defineTool } from '@deepseek-ai/dsh-tools' +import { readdir } from 'node:fs/promises' + +export const name = 'file-counter' +export const inject = ['tools'] + +export function apply(ctx: Context) { + ctx.tools.register(defineTool({ + name: 'count_files', + description: 'Count files in a directory.', + parameters: { + path: { type: 'string', required: true, description: 'Directory path' }, + extension: { type: 'string', description: 'Filter by extension (e.g. ".ts")' }, + }, + async execute(args) { + const entries = await readdir(args.path, { withFileTypes: true }) + let files = entries.filter(e => e.isFile()) + if (args.extension) { + files = files.filter(f => f.name.endsWith(args.extension!)) + } + return [{ type: 'text', text: `Found ${files.length} files.` }] + }, + })) +} +``` + +## Next steps + +- [Plugin configuration](./config.md) — make the tool configurable +- [Capability layering](../practice/) — understand the interface/implementation/consumer pattern diff --git a/website/zh-CN/develop/basic/tool.md b/docs/user/develop/basic/tool.zh.md similarity index 67% rename from website/zh-CN/develop/basic/tool.md rename to docs/user/develop/basic/tool.zh.md index 96f4d7ba24..fce9a7d9b9 100644 --- a/website/zh-CN/develop/basic/tool.md +++ b/docs/user/develop/basic/tool.zh.md @@ -1,5 +1,7 @@ # 开发一个 Tool +[English](tool.md) | 中文 + Tool 是模型可以调用的能力。本文介绍如何用 `defineTool` 编写一个 tool。 ## 最小示例 @@ -19,7 +21,7 @@ export function apply(ctx: Context) { name: { type: 'string', required: true, description: 'The name to greet' }, }, async execute(args) { - // args 自动推导为 { name: string } + // args is inferred as { name: string }. return [{ type: 'text', text: `Hello, ${args.name}!` }] }, })) @@ -33,33 +35,27 @@ export function apply(ctx: Context) { ### 基本类型 ```ts -import type { SchemaSpec } from '@deepseek-ai/dsh-tools' - -const parameters = { +export const parameters = { path: { type: 'string', required: true }, limit: { type: 'number' }, recursive: { type: 'boolean' }, -} satisfies SchemaSpec -// 推导类型: { path: string; limit?: number; recursive?: boolean } +} +// Inferred type: { path: string; limit?: number; recursive?: boolean } ``` ### 枚举 ```ts -import type { SchemaSpec } from '@deepseek-ai/dsh-tools' - -const parameters = { +export const parameters = { mode: { type: 'string', required: true, enum: ['read', 'write', 'append'] }, -} satisfies SchemaSpec -// 推导类型: { mode: string } (运行时校验 enum 值) +} +// Inferred type: { mode: string } (enum values are validated at runtime) ``` ### 嵌套对象 ```ts -import type { SchemaSpec } from '@deepseek-ai/dsh-tools' - -const parameters = { +export const parameters = { options: { type: 'object', properties: { @@ -67,22 +63,20 @@ const parameters = { retries: { type: 'number' }, }, }, -} satisfies SchemaSpec -// 推导类型: { options?: { timeout?: number; retries?: number } } +} +// Inferred type: { options?: { timeout?: number; retries?: number } } ``` ### 数组 ```ts -import type { SchemaSpec } from '@deepseek-ai/dsh-tools' - -const parameters = { +export const parameters = { tags: { type: 'array', items: { type: 'string' }, }, -} satisfies SchemaSpec -// 推导类型: { tags?: string[] } +} +// Inferred type: { tags?: string[] } ``` ### 每个属性的字段 @@ -103,15 +97,17 @@ const parameters = { ```ts import { defineTool } from '@deepseek-ai/dsh-tools' -defineTool({ - name: 'demo', - description: 'Demo tool.', +export const tool = defineTool({ + name: 'example', + description: 'Return an example result.', parameters: {}, async execute(args, exec) { - // args: 根据 parameters 自动推导的类型 - // exec: ToolExecution 对象,提供执行上下文 + // args: inferred from parameters + // exec: ToolExecution context - // 返回 ContentBlock 数组 + // Return a ContentBlock array. + void args + void exec return [{ type: 'text', text: 'result here' }] }, }) @@ -121,23 +117,15 @@ defineTool({ `execute` 必须返回一个 `ContentBlock[]`,告诉模型 tool 的执行结果: -```ts -import type { ContentBlock } from '@deepseek-ai/dsh-llm' +```ts ignore-check +// Text result +return [{ type: 'text', text: 'file content here...' }] -declare const matchResults: string[] - -// 文本结果 -function textResult(): ContentBlock[] { - return [{ type: 'text', text: 'file content here...' }] -} - -// 多个 block -function multiBlockResult(): ContentBlock[] { - return [ - { type: 'text', text: 'Found 3 matches:' }, - { type: 'text', text: matchResults.join('\n') }, - ] -} +// Multiple blocks +return [ + { type: 'text', text: 'Found 3 matches:' }, + { type: 'text', text: matchResults.join('\n') }, +] ``` ### 参数校验 @@ -150,22 +138,14 @@ function multiBlockResult(): ContentBlock[] { Tool 可以定义 UI 渲染方法,用于在终端或 ACP 客户端中展示 tool call 和 result: -```ts -import { defineTool } from '@deepseek-ai/dsh-tools' - +```ts ignore-check defineTool({ name: 'bash', - description: 'Run a shell command.', - parameters: { - command: { type: 'string', required: true }, - }, - async execute(args) { - return [{ type: 'text', text: `ran: ${args.command}` }] - }, + // ... presentCall(args) { return { card: 'terminal', - title: args.command.slice(0, 60), + title: args.command, } }, presentResult(args, result) { @@ -183,25 +163,11 @@ defineTool({ `ctx.tools.register()` 返回值就是 disposer。但由于你在 `ctx` 上调用,框架已经自动追踪了这个注册——插件卸载时会自动移除 tool。你不需要手动调用 disposer。 -```ts -import type { Context } from 'cordis' -import { defineTool } from '@deepseek-ai/dsh-tools' +```ts ignore-check +// This is sufficient: +ctx.tools.register(defineTool({ /* ... */ })) -declare const ctx: Context - -// 这样就够了: -ctx.tools.register(defineTool({ - name: 'noop', - description: 'Do nothing.', - parameters: {}, - async execute() { - return [] - }, -})) - -// 不需要: -// const dispose = ctx.tools.register(...) -// ctx.effect(() => dispose) +// No saved disposer or extra cleanup registration is needed. ``` ## 完整实战示例 @@ -238,5 +204,5 @@ export function apply(ctx: Context) { ## 下一步 -- [插件配置](config) — 让你的 tool 可配置 +- [插件配置](./config.md) — 让你的 tool 可配置 - [能力三件套](../practice/) — 了解 seam/impl/consumer 模式 diff --git a/docs/user/develop/framework/events.i18n.yaml b/docs/user/develop/framework/events.i18n.yaml new file mode 100644 index 0000000000..9704eff7c5 --- /dev/null +++ b/docs/user/develop/framework/events.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 +events.md: 0c57681a55ea0200fe8f33293176fc94f09a4ce5 +events.zh.md: 3e14739d4a97ba014d545c9f226000507aaeacef diff --git a/docs/user/develop/framework/events.md b/docs/user/develop/framework/events.md new file mode 100644 index 0000000000..0c57681a55 --- /dev/null +++ b/docs/user/develop/framework/events.md @@ -0,0 +1,143 @@ +# Event system + +English | [中文](events.zh.md) + +Events are the core communication mechanism between Cordis plugins. Harness uses them extensively for loosely coupled extension points. + +## Basic use + +### Listen for an event + +```ts ignore-check +ctx.on('event-name', (payload) => { + // Handle the event. +}) +``` + +### Emit an event + +```ts ignore-check +ctx.emit('event-name', payload) +``` + +## Event modes + +Cordis provides several event modes for different interaction contracts. + +### emit — broadcast + +Every listener runs synchronously and return values are ignored: + +```ts ignore-check +// Emit +ctx.emit('my-plugin/ready', { id: 'worker-1' }) + +// Listen +ctx.on('my-plugin/ready', ({ id }) => { + console.log(`${id} is ready`) +}) +``` + +### bail — short circuit + +Listeners run in order; the first non-`undefined` result becomes the final result: + +```ts ignore-check +// Dispatch +const result = ctx.bail('some-check', input) + +// Listen: a returned value stops later listeners. +ctx.on('some-check', (input) => { + if (shouldBlock(input)) return 'blocked' + // Return undefined to continue to the next listener. +}) +``` + +### serial — ordered execution + +Listeners run in registration order and asynchronous results are awaited. The first listener to return a non-empty value stops further execution: + +```ts ignore-check +await ctx.serial('setup-phase', context) +``` + +### waterfall — pipeline + +Each listener may wrap the downstream result to form a processing chain. A listener **must call `next()` to delegate downstream**; omitting the call vetoes the pipeline: + +```ts ignore-check +// Dispatch +const output = await ctx.waterfall('my-plugin/transform', input, async () => input) + +// Listen: next() is mandatory. +ctx.on('my-plugin/transform', async (_input, next) => { + const downstream = await next() + return downstream.trim() +}) +``` + +::: warning +A waterfall listener **must call `next()`**. Omitting it vetoes the pipeline by design, enabling interception and gateway behavior. +::: + +## Typed events + +Harness uses TypeScript declaration merging for type-safe events: + +```ts +import 'cordis' + +declare module 'cordis' { + interface Events { + 'my-plugin/ready': (payload: { id: string }) => void + 'my-plugin/check': (input: string) => boolean | undefined + 'my-plugin/transform': (input: string, next: () => Promise) => Promise + } +} + +// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...) +// are now inferred correctly. +``` + +## Cordis events and session records + +Harness Cordis events use `namespace/action` names, including `agent/pre-step`, `agent/request`, `agent/step-result`, `tools/result`, and `session/event`. The generated [event catalog](../../../cordis-catalog/events.md) records complete signatures and modes. + +`turn/*`, `step/*`, `tool/call`, `tool/result`, and `compact/*` are durable session-event types, not same-named Cordis events. To observe them, listen to `session/event` and inspect `event.type`. + +## Event listeners are effects + +A listener registered with `ctx.on()` is removed automatically when its plugin unloads: + +```ts ignore-check +export function apply(ctx: Context) { + // This listener is removed when the plugin disposes. + ctx.on('tools/result', handler) +} +``` + +## Example: logging plugin + +This plugin logs tool calls and results: + +```ts +import type { Context } from 'cordis' +import '@deepseek-ai/dsh-tools' + +export const name = 'tool-logger' + +export function apply(ctx: Context) { + ctx.on('tools/result', (exec, result) => { + console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`) + const text = result.content + .map(block => block.type === 'text' ? block.text : '') + .join('') + console.log(`[tool result] ${text.slice(0, 100)}`) + }) +} +``` + +## Next steps + +- [Capability layering](../practice/) — understand events within capability interfaces +- [LLM adapters](../practice/llm-adapter.md) — implement a complete LLM backend diff --git a/docs/user/develop/framework/events.zh.md b/docs/user/develop/framework/events.zh.md new file mode 100644 index 0000000000..3e14739d4a --- /dev/null +++ b/docs/user/develop/framework/events.zh.md @@ -0,0 +1,143 @@ +# 事件系统 + +[English](events.md) | 中文 + +事件是 Cordis 插件间通信的核心机制。Harness 大量使用事件来实现松耦合的扩展点。 + +## 基本用法 + +### 监听事件 + +```ts ignore-check +ctx.on('event-name', (payload) => { + // Handle the event. +}) +``` + +### 触发事件 + +```ts ignore-check +ctx.emit('event-name', payload) +``` + +## 事件模式 + +Cordis 提供多种事件触发模式,适用于不同场景: + +### emit — 广播 + +所有监听器同步执行,不关心返回值: + +```ts ignore-check +// Emit +ctx.emit('my-plugin/ready', { id: 'worker-1' }) + +// Listen +ctx.on('my-plugin/ready', ({ id }) => { + console.log(`${id} is ready`) +}) +``` + +### bail — 短路 + +依次调用监听器,第一个返回非 `undefined` 值的结果作为最终值: + +```ts ignore-check +// Dispatch +const result = ctx.bail('some-check', input) + +// Listen: a returned value stops later listeners. +ctx.on('some-check', (input) => { + if (shouldBlock(input)) return 'blocked' + // Return undefined to continue to the next listener. +}) +``` + +### serial — 顺序执行 + +监听器按注册顺序依次执行,并等待异步结果;第一个返回非空值的监听器会终止后续执行: + +```ts ignore-check +await ctx.serial('setup-phase', context) +``` + +### waterfall — 管道 + +每个监听器可以包装下游返回值,形成处理链。**必须调用 `next()` 传递给下游**,不调用即为否决: + +```ts ignore-check +// Dispatch +const output = await ctx.waterfall('my-plugin/transform', input, async () => input) + +// Listen: next() is mandatory. +ctx.on('my-plugin/transform', async (_input, next) => { + const downstream = await next() + return downstream.trim() +}) +``` + +::: warning +Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整个管道,这是故意为之的设计——用于实现拦截/网关逻辑。 +::: + +## Typed Events + +Harness 使用 TypeScript 声明合并来为事件提供类型安全: + +```ts +import 'cordis' + +declare module 'cordis' { + interface Events { + 'my-plugin/ready': (payload: { id: string }) => void + 'my-plugin/check': (input: string) => boolean | undefined + 'my-plugin/transform': (input: string, next: () => Promise) => Promise + } +} + +// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...) +// are now inferred correctly. +``` + +## Cordis 事件与会话记录 + +Harness 的 Cordis 事件遵循 `namespace/action` 命名,例如 `agent/pre-step`、`agent/request`、`agent/step-result`、`tools/result` 和 `session/event`。完整签名与触发模式见[Events 目录](../../../cordis-catalog/events.md)。 + +`turn/*`、`step/*`、`tool/call`、`tool/result` 和 `compact/*` 是持久化的会话事件类型,不是同名 Cordis 事件。需要观察它们时,监听 `session/event` 并检查 `event.type`。 + +## 事件也是效果 + +通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除: + +```ts ignore-check +export function apply(ctx: Context) { + // This listener is removed when the plugin disposes. + ctx.on('tools/result', handler) +} +``` + +## 实战示例:日志插件 + +一个记录所有 tool 调用的简单插件: + +```ts +import type { Context } from 'cordis' +import '@deepseek-ai/dsh-tools' + +export const name = 'tool-logger' + +export function apply(ctx: Context) { + ctx.on('tools/result', (exec, result) => { + console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`) + const text = result.content + .map(block => block.type === 'text' ? block.text : '') + .join('') + console.log(`[tool result] ${text.slice(0, 100)}`) + }) +} +``` + +## 下一步 + +- [能力三件套](../practice/) — 事件在 capability seam 中的角色 +- [LLM 适配器](../practice/llm-adapter.md) — 实现一个完整的 LLM 后端 diff --git a/docs/user/develop/framework/index.i18n.yaml b/docs/user/develop/framework/index.i18n.yaml new file mode 100644 index 0000000000..1712837d16 --- /dev/null +++ b/docs/user/develop/framework/index.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 +index.md: 79e925b54509da41535735527e283850384257ec +index.zh.md: 62be8c706510704f7b07286f166f14fa81235a0a diff --git a/docs/user/develop/framework/index.md b/docs/user/develop/framework/index.md new file mode 100644 index 0000000000..79e925b545 --- /dev/null +++ b/docs/user/develop/framework/index.md @@ -0,0 +1,136 @@ +# Plugins and lifecycle + +English | [中文](index.zh.md) + +This page describes the Cordis plugin model and lifecycle state machine. + +## Fiber state machine + +Every loaded plugin owns a **Fiber** scope with the following states: + +``` +PENDING → LOADING → ACTIVE + ↘ FAILED +ACTIVE → UNLOADING → DISPOSED +``` + +| State | Meaning | +|------|------| +| PENDING | Declared, but required dependencies are not ready | +| LOADING | Dependencies are ready and `apply` is running | +| ACTIVE | The plugin is running | +| FAILED | `apply` threw an error | +| UNLOADING | The plugin is unloading and disposing resources | +| DISPOSED | The plugin is fully unloaded | + +## Dependency-driven loading + +A plugin with `inject` waits for every required service before loading: + +```ts ignore-check +export const inject = ['tools', 'llm'] + +export function apply(ctx: Context) { + // ctx.tools and ctx.llm are ready here. +} +``` + +If a required service disappears, for example during provider replacement, the plugin unloads automatically (ACTIVE → DISPOSED) and loads again when the service returns. + +## Automatic cleanup + +Every registration made through `ctx` is undone when the plugin unloads: + +```ts ignore-check +export function apply(ctx: Context) { + // Event listener: removed automatically on unload. + ctx.on('some-event', handler) + + // Custom resource: the returned disposer runs on unload. + ctx.effect(() => { + const connection = createConnection() + return () => connection.close() + }) +} +``` + +The framework tracks and disposes all of these operations: +- `ctx.on(event, handler)` — event listener +- `ctx.tools.register(tool)` — tool registration +- `ctx.llm.registerAdapter(names, adapter)` — LLM adapter registration +- `ctx.effect(() => cleanup)` — custom resource + +During unload, disposer invocation starts in reverse registration order, but multiple async disposers run concurrently and have no serial completion guarantee. Put order-dependent cleanup in one disposer returned from a single `ctx.effect()` and await its steps serially there. + +## Nested contexts + +`ctx.plugin()` creates a child Fiber that inherits the parent context but has an independent lifecycle: + +```ts ignore-check +export function apply(ctx: Context) { + // Register a child plugin. + ctx.plugin(childPlugin) + + // The child has its own Fiber and unloads with its parent. +} +``` + +## Dispose semantics + +To stop a plugin instance early: + +```ts +import type { Context } from 'cordis' + +declare const ctx: Context +declare function myPlugin(ctx: Context): void + +const fiber = ctx.plugin(myPlugin) + +// Dispose it manually later. +await fiber.dispose() +``` + +`dispose` guarantees: +1. All registrations owned by the plugin are removed. +2. Child plugins are recursively unloaded. +3. The returned promise resolves after all asynchronous cleanup finishes. + +## Hot replacement (HMR) + +With `@cordisjs/plugin-hmr` loaded from `cordis.yml`, editing a plugin source file triggers: + +1. Unload the old plugin and clean up its registrations. +2. Load the new code. +3. Run the new `apply`. + +Because plugin registrations clean themselves up, hot replacement does not retain registrations from the old instance. + +## Example lifecycle + +```ts ignore-check +export function apply(ctx: Context) { + console.log('plugin loading') + + ctx.effect(() => { + console.log('effect registered') + return () => console.log('effect cleaned up') + }) +} +``` + +Loading prints: +``` +plugin loading +effect registered +``` + +Unloading prints: +``` +effect cleaned up +``` + +## Next steps + +- [Services and dependencies](./service.md) — expose a capability to other plugins +- [Event system](./events.md) — communicate between plugins diff --git a/website/zh-CN/develop/framework/index.md b/docs/user/develop/framework/index.zh.md similarity index 70% rename from website/zh-CN/develop/framework/index.md rename to docs/user/develop/framework/index.zh.md index c23b4aa221..62be8c7065 100644 --- a/website/zh-CN/develop/framework/index.md +++ b/docs/user/develop/framework/index.zh.md @@ -1,5 +1,7 @@ # 插件与生命周期 +[English](index.md) | 中文 + 深入了解 Cordis 插件模型和生命周期状态机。 ## Fiber 状态机 @@ -25,15 +27,11 @@ ACTIVE → UNLOADING → DISPOSED 声明了 `inject` 的插件不会立即加载,而是等待依赖的服务就绪: -```ts -import type { Context } from 'cordis' -import type {} from '@deepseek-ai/dsh-tools' -import type {} from '@deepseek-ai/dsh-llm' - +```ts ignore-check export const inject = ['tools', 'llm'] export function apply(ctx: Context) { - // 到这里时,ctx.tools 和 ctx.llm 一定存在 + // ctx.tools and ctx.llm are ready here. } ``` @@ -43,23 +41,12 @@ export function apply(ctx: Context) { 通过 `ctx` 做的任何注册,在插件卸载时都会自动撤销: -```ts -import type { Context } from 'cordis' - -declare module 'cordis' { - interface Events { - 'my-plugin/some-event'(): void - } -} - -declare function handler(): void -declare function createConnection(): { close(): void } - +```ts ignore-check export function apply(ctx: Context) { - // 事件监听——卸载时自动移除 - ctx.on('my-plugin/some-event', handler) + // Event listener: removed automatically on unload. + ctx.on('some-event', handler) - // 自定义资源——卸载时调用返回的函数 + // Custom resource: the returned disposer runs on unload. ctx.effect(() => { const connection = createConnection() return () => connection.close() @@ -73,22 +60,18 @@ export function apply(ctx: Context) { - `ctx.llm.registerAdapter(names, adapter)` — LLM 适配器注册 - `ctx.effect(() => cleanup)` — 自定义资源 -插件卸载时,这些注册按倒序逐个撤销。 +插件卸载时,处置器按注册顺序的反向发起,但多个异步处置器会并发执行,不保证逐个完成。存在顺序依赖的清理步骤必须放进同一个 `ctx.effect()` 返回的处置器中,由该处置器负责串行等待。 ## 嵌套上下文 `ctx.plugin()` 创建子 Fiber,它继承父上下文但有独立的生命周期: -```ts -import type { Context } from 'cordis' - -declare function childPlugin(ctx: Context): void - +```ts ignore-check export function apply(ctx: Context) { - // 注册一个子插件 + // Register a child plugin. ctx.plugin(childPlugin) - // 子插件有自己的 Fiber,父卸载时子也卸载 + // The child has its own Fiber and unloads with its parent. } ``` @@ -104,7 +87,7 @@ declare function myPlugin(ctx: Context): void const fiber = ctx.plugin(myPlugin) -// 之后可以手动 dispose +// Dispose it manually later. await fiber.dispose() ``` @@ -125,11 +108,7 @@ await fiber.dispose() ## 实战:理解生命周期 -`apply` 函数体就是加载钩子;卸载没有专门的事件——把清理逻辑放进 `ctx.effect()` 的返回函数即可: - -```ts -import type { Context } from 'cordis' - +```ts ignore-check export function apply(ctx: Context) { console.log('plugin loading') @@ -153,5 +132,5 @@ effect cleaned up ## 下一步 -- [服务与依赖](service) — 让你的插件对外提供能力 -- [事件系统](events) — 插件间通信的核心机制 +- [服务与依赖](./service.md) — 让你的插件对外提供能力 +- [事件系统](./events.md) — 插件间通信的核心机制 diff --git a/docs/user/develop/framework/service.i18n.yaml b/docs/user/develop/framework/service.i18n.yaml new file mode 100644 index 0000000000..f0deb18959 --- /dev/null +++ b/docs/user/develop/framework/service.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 +service.md: 1bf28cb3c7dfdfbd6d0babfa3b1688ac65eea01e +service.zh.md: 17785c056ab9a0a21974e6ed8bbe7f7de05fa00e diff --git a/docs/user/develop/framework/service.md b/docs/user/develop/framework/service.md new file mode 100644 index 0000000000..1bf28cb3c7 --- /dev/null +++ b/docs/user/develop/framework/service.md @@ -0,0 +1,148 @@ +# Services and dependencies + +English | [中文](service.zh.md) + +A service is a capability one plugin exposes to other plugins. `inject` declares the services a plugin requires. + +## What is a service? + +In Harness, `tools`, `llm`, and `agents` are services. Each is a named capability mounted on `ctx`: + +```ts ignore-check +ctx.tools // ToolRegistry service +ctx.llm // LLM service +ctx.agents // Agent service +``` + +Any plugin can provide a service for other plugins to consume. + +## Consume a service + +Declare `inject` to use an existing service: + +```ts ignore-check +export const inject = ['tools'] + +export function apply(ctx: Context) { + // ctx.tools exists and is ready here. + ctx.tools.register(/* ... */) +} +``` + +When `apply` runs, every service declared by `inject` is ready. If a service is not ready, the plugin waits instead of running. + +## Provide a service + +### Extend Service + +```ts +import { Service, type Context } from 'cordis' + +export default class MetricsService extends Service { + static inject = ['llm'] // A service may depend on other services. + + constructor(ctx: Context) { + super(ctx, 'metrics') // 'metrics' is the service name. + } + + // Public service method. + record(event: string, value: number) { + // ... + } +} +``` + +After loading this plugin, consumers access the service as `ctx.metrics`: + +```ts ignore-check +export const inject = ['metrics'] + +export function apply(ctx: Context) { + ctx.metrics.record('tool_call', 1) +} +``` + +### Declare its type + +Use TypeScript declaration merging to type `ctx.metrics`: + +```ts +import { Service, type Context } from 'cordis' + +declare module 'cordis' { + interface Context { + metrics: MetricsService + } +} + +export default class MetricsService extends Service { + constructor(ctx: Context) { + super(ctx, 'metrics') + } + + record(event: string, value: number) { /* ... */ } +} +``` + +## Dependency behavior + +### Required and optional dependencies + +```ts ignore-check +// Required: the plugin does not load while the service is absent. +export const inject = ['tools'] + +// Optional: omit inject and query with ctx.get() at the use site. +export function apply(ctx: Context) { + const metrics = ctx.get('metrics') + metrics?.record('plugin_loaded', 1) +} +``` + +### When a service disappears + +If a required service disappears while the application is running, for example because its provider unloads: + +1. Dependent plugins dispose automatically. +2. They load again when the service returns. + +This prevents a plugin from calling a service that no longer exists. + +## Service isolation + +`cordis.yml` can isolate services so separate plugin groups see separate instances of the same service: + +```yaml +- id: group-a + name: '@cordisjs/plugin-group' + group: true + isolate: + bash: true + config: + - name: '@deepseek-ai/dsh-bash-local' + config: + timeoutMs: 5000 + - name: './src/plugin-a.ts' + +- id: group-b + name: '@cordisjs/plugin-group' + group: true + isolate: + bash: true + config: + - name: '@deepseek-ai/dsh-bash-local' + config: + timeoutMs: 60000 + - name: './src/plugin-b.ts' +``` + +`plugin-a` and `plugin-b` each see the Bash instance in their own group, with no cross-group effect. + +## Built-in Harness services + +The repository generates the service names, public methods, and source locations in the [service catalog](../../../cordis-catalog/services.md). Use that catalog and the service's TypeScript interface while developing a plugin; do not maintain a second static list. + +## Next steps + +- [Event system](./events.md) — communicate between plugins without tight coupling +- [Capability layering](../practice/) — use services as capability interfaces diff --git a/website/zh-CN/develop/framework/service.md b/docs/user/develop/framework/service.zh.md similarity index 52% rename from website/zh-CN/develop/framework/service.md rename to docs/user/develop/framework/service.zh.md index 7b31a1c23b..17785c056a 100644 --- a/website/zh-CN/develop/framework/service.md +++ b/docs/user/develop/framework/service.zh.md @@ -1,22 +1,17 @@ # 服务与依赖 +[English](service.md) | 中文 + 服务 (Service) 是插件对外暴露能力的方式。依赖 (inject) 是插件声明自己需要哪些服务。 ## 什么是服务 在 Harness 中,`tools`、`llm`、`agents` 都是服务。服务是挂载在 `ctx` 上的命名能力: -```ts -import type { Context } from 'cordis' -import type {} from '@deepseek-ai/dsh-tools' -import type {} from '@deepseek-ai/dsh-llm' -import type {} from '@deepseek-ai/dsh-agent' - -declare const ctx: Context - -ctx.tools // ToolRegistry 服务 -ctx.llm // LLM 服务 -ctx.agents // Agent 注册表服务 +```ts ignore-check +ctx.tools // ToolRegistry service +ctx.llm // LLM service +ctx.agents // Agent service ``` 任何插件都可以提供一个新服务,供其他插件使用。 @@ -25,22 +20,12 @@ ctx.agents // Agent 注册表服务 声明 `inject` 来使用已有服务: -```ts -import type { Context } from 'cordis' -import { defineTool } from '@deepseek-ai/dsh-tools' - +```ts ignore-check export const inject = ['tools'] export function apply(ctx: Context) { - // ctx.tools 在这里一定存在且就绪 - ctx.tools.register(defineTool({ - name: 'demo', - description: 'Demo tool.', - parameters: {}, - async execute() { - return [] - }, - })) + // ctx.tools exists and is ready here. + ctx.tools.register(/* ... */) } ``` @@ -52,16 +37,15 @@ export function apply(ctx: Context) { ```ts import { Service, type Context } from 'cordis' -import type {} from '@deepseek-ai/dsh-llm' export default class MetricsService extends Service { - static inject = ['llm'] // 本服务也可以依赖其他服务 + static inject = ['llm'] // A service may depend on other services. constructor(ctx: Context) { - super(ctx, 'metrics') // 'metrics' 是服务名 + super(ctx, 'metrics') // 'metrics' is the service name. } - // 服务的公开方法 + // Public service method. record(event: string, value: number) { // ... } @@ -70,9 +54,7 @@ export default class MetricsService extends Service { 加载这个插件后,其他插件就可以通过 `ctx.metrics` 访问它: -```ts -import type { Context } from 'cordis' - +```ts ignore-check export const inject = ['metrics'] export function apply(ctx: Context) { @@ -104,18 +86,14 @@ export default class MetricsService extends Service { ## 依赖的行为 -### 必选依赖 vs 可选读取 +### 必选依赖 vs 可选依赖 -`inject` 声明的依赖都是必选的:服务不存在时,插件不会加载。如果只想"有则用之",用 `ctx.get()` 读取——服务不存在时返回 `undefined`,插件照常加载: - -```ts -import type { Context } from 'cordis' - -// 必选:服务不存在时,插件不会加载 +```ts ignore-check +// Required: the plugin does not load while the service is absent. export const inject = ['tools'] +// Optional: omit inject and query with ctx.get() at the use site. export function apply(ctx: Context) { - // 可选读取:不声明 inject,服务不存在时返回 undefined const metrics = ctx.get('metrics') metrics?.record('plugin_loaded', 1) } @@ -132,7 +110,7 @@ export function apply(ctx: Context) { ## 服务隔离 -`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例。用 `@cordisjs/plugin-group` 建组(`group: true` 标记组条目),并在组上声明 `isolate`,把该服务隔离进组内作用域: +`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例: ```yaml - id: group-a @@ -158,24 +136,13 @@ export function apply(ctx: Context) { - name: './src/plugin-b.ts' ``` -`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。`isolate: { bash: true }` 是必需的:不隔离的话,两个组在同一作用域注册同名服务,第二个会直接报重复注册错误。 +`plugin-a` 和 `plugin-b` 各自看到自己组内的 bash 实例,互不影响。 -## Harness 内置服务一览 +## Harness 内置服务 -| 服务名 | 提供者 | 用途 | -|--------|--------|------| -| `tools` | dsh-tools | Tool 注册表 | -| `llm` | dsh-llm | LLM 调用 + 适配器注册 | -| `agents` | dsh-agent | Agent 注册表 | -| `agentLoop` | dsh-agent-loop | Agent 创建与循环执行 | -| `sessions` | dsh-session | 会话存储与事件流 | -| `systemPrompt` | dsh-system-prompt | 系统提示词组装 | -| `bash` | dsh-bash(实现:dsh-bash-local) | Bash 命令执行 | -| `fs` | dsh-fs(实现:dsh-fs-local) | 文件系统操作 | -| `subagents` | dsh-subagent | 子代理委派 | -| `sessionPersistence` | dsh-session-persistence(实现:-jsonl / -sqlite) | 会话持久化 | +服务名、公开方法和源码位置由仓库自动生成,见[服务目录](../../../cordis-catalog/services.md)。开发插件时应以该目录和服务接口的 TypeScript 类型为准,不要复制一份静态清单。 ## 下一步 -- [事件系统](events) — 插件间松耦合通信 +- [事件系统](./events.md) — 插件间松耦合通信 - [能力三件套](../practice/) — 服务在 seam 模式中的应用 diff --git a/docs/user/develop/practice/index.i18n.yaml b/docs/user/develop/practice/index.i18n.yaml new file mode 100644 index 0000000000..d2478abf75 --- /dev/null +++ b/docs/user/develop/practice/index.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 +index.md: 0261b49b071167f7c2a33f78bbc1959cc6f1879f +index.zh.md: 5819344430fcbde31bf825e9815120983e44e3f6 diff --git a/docs/user/develop/practice/index.md b/docs/user/develop/practice/index.md new file mode 100644 index 0000000000..0261b49b07 --- /dev/null +++ b/docs/user/develop/practice/index.md @@ -0,0 +1,158 @@ +# Three-layer capability design + +English | [中文](index.zh.md) + +When a capability is general enough to need replaceable implementations, such as Bash execution, Harness splits it into three packages: an **interface**, an **implementation**, and a **consumer**. Each layer can evolve or be replaced independently. + +## Bash example + +The Bash execution capability consists of: + +- **Interface** (`dsh-bash`) — defines Bash request and result shapes +- **Implementation** (`dsh-bash-local`) — executes commands on the local machine +- **Consumer** (`dsh-tool-bash`) — exposes the capability as a model-callable tool + +``` +┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│ +│ (interface) │ │ (implementation) │ │(consumer/tool)│ +└─────────────┘ └──────────────────┘ └──────────────┘ + ▲ │ + └────────────────────────────────────────────┘ + inject: ['bash'] +``` + +## Benefits of the split + +### Replace implementations + +One interface can have multiple implementations selected through `cordis.yml`: + +```yaml +# Local execution +- name: '@deepseek-ai/dsh-bash-local' + +# Or a future remote sandbox implementation +# - name: '@deepseek-ai/dsh-bash-remote' +# config: +# endpoint: 'https://sandbox.example.com' +``` + +The interface and tool remain unchanged while the implementation changes. + +### Evolve independently + +- The interface changes rarely after its contract stabilizes. +- Implementations can improve performance and security independently. +- Consumers can change how they present the capability to the model. + +### Decouple dependencies + +- The implementation depends on the interface. +- The consumer depends on the interface. +- The implementation and consumer **do not depend on each other**. + +## Built-in three-layer capabilities + +| Capability | Interface | Implementation | Consumer | +|------|-------------|------|---------------| +| Bash | `dsh-bash` | `dsh-bash-local` | `dsh-tool-bash` | +| Filesystem | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` | +| Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` | +| Subagent | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` | +| Compaction | `dsh-compact` | `dsh-compact-basic` | The implementation consumes agent-loop extension events | + +## Develop a three-layer capability + +### Step 1: define the interface + +```ts ignore-check +// packages/my-cap/my-cap/src/index.ts +import { Service, type Context } from 'cordis' + +declare module 'cordis' { + interface Context { + myCap: MyCapService + } +} + +export abstract class MyCapService extends Service { + constructor(ctx: Context) { + super(ctx, 'myCap') + } + + /** Execute the capability. */ + abstract execute(request: MyCapRequest): Promise +} + +export interface MyCapRequest { + input: string +} + +export interface MyCapResult { + output: string +} +``` + +### Step 2: write an implementation + +```ts ignore-check +// packages/my-cap/my-cap-local/src/index.ts +import type { Context } from 'cordis' +import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap' + +class MyCapLocal extends MyCapService { + async execute(request: MyCapRequest): Promise { + // Concrete implementation. + return { output: request.input.toUpperCase() } + } +} + +export const name = 'my-cap-local' + +export function apply(ctx: Context) { + ctx.plugin(MyCapLocal) +} +``` + +### Step 3: write a consumer + +```ts ignore-check +// packages/my-cap/tool-my-cap/src/index.ts +import type { Context } from 'cordis' +import { defineTool } from '@deepseek-ai/dsh-tools' + +export const name = 'tool-my-cap' +export const inject = ['tools', 'myCap'] + +export function apply(ctx: Context) { + ctx.tools.register(defineTool({ + name: 'my_cap', + description: 'Execute my capability.', + parameters: { + input: { type: 'string', required: true }, + }, + async execute(args) { + const result = await ctx.myCap.execute({ input: args.input }) + return [{ type: 'text', text: result.output }] + }, + })) +} +``` + +### Compose them in cordis.yml + +```yaml +- name: '@deepseek-ai/dsh-my-cap-local' +- name: '@deepseek-ai/dsh-tool-my-cap' +``` + +## Design points + +- **Do not split preemptively** — use three packages only when the capability needs replaceable implementations. A simple tool plugin does not. +- **The interface owns Request/Result types** — implementations and consumers depend only on the interface package. +- **Explicit > implicit** — resolve defaults in an explicit `resolve(request): Spec` step rather than hiding `?? default` expressions inside `run()`. + +## Next steps + +- [LLM adapter](./llm-adapter.md) — implement an LLM backend, a common capability interface extension diff --git a/website/zh-CN/develop/practice/index.md b/docs/user/develop/practice/index.zh.md similarity index 90% rename from website/zh-CN/develop/practice/index.md rename to docs/user/develop/practice/index.zh.md index 24ad0ffa52..5819344430 100644 --- a/website/zh-CN/develop/practice/index.md +++ b/docs/user/develop/practice/index.zh.md @@ -1,5 +1,7 @@ # 能力的三层拆分 +[English](index.md) | 中文 + 当一个能力(插件)足够通用(比如"执行 bash 命令"),Harness 会把它拆成三个包:**接口**、**实现**、**消费者**。这样可以独立替换其中任何一层。 ## 以 Bash 为例 @@ -13,7 +15,7 @@ ``` ┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ │ dsh-bash │────▶│ dsh-bash-local │ │ dsh-tool-bash│ -│ (接口) │ │ (实现) │ │ (消费者/tool)│ +│ (interface) │ │ (implementation) │ │(consumer/tool)│ └─────────────┘ └──────────────────┘ └──────────────┘ ▲ │ └────────────────────────────────────────────┘ @@ -27,10 +29,10 @@ 同一个接口可以有多种实现。用户通过 `cordis.yml` 选择: ```yaml -# 本地执行 +# Local execution - name: '@deepseek-ai/dsh-bash-local' -# 或:远程沙箱执行(未来) +# Or a future remote sandbox implementation # - name: '@deepseek-ai/dsh-bash-remote' # config: # endpoint: 'https://sandbox.example.com' @@ -58,13 +60,13 @@ | 文件系统 | `dsh-fs` | `dsh-fs-local` + `dsh-fs-policy` | `dsh-tool-fs` | | Web | `dsh-web` | `dsh-web-fetch-local` / `dsh-web-search-*` | `dsh-tool-web` | | 子代理 | `dsh-subagent` | `dsh-subagent-spawn` / `dsh-subagent-fork` | `dsh-tool-subagent` | -| 压缩 | `dsh-compact` | `dsh-compact-basic` | (内置于 agent-loop) | +| 压缩 | `dsh-compact` | `dsh-compact-basic` | 由实现插件消费 agent-loop 的扩展事件 | ## 开发你自己的三件套 ### 第一步:定义接口 -```ts +```ts ignore-check // packages/my-cap/my-cap/src/index.ts import { Service, type Context } from 'cordis' @@ -79,7 +81,7 @@ export abstract class MyCapService extends Service { super(ctx, 'myCap') } - /** 执行能力的核心方法 */ + /** Execute the capability. */ abstract execute(request: MyCapRequest): Promise } @@ -101,7 +103,7 @@ import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/ class MyCapLocal extends MyCapService { async execute(request: MyCapRequest): Promise { - // 具体实现 + // Concrete implementation. return { output: request.input.toUpperCase() } } } @@ -115,7 +117,7 @@ export function apply(ctx: Context) { ### 第三步:编写消费者 (tool) -```ts +```ts ignore-check // packages/my-cap/tool-my-cap/src/index.ts import type { Context } from 'cordis' import { defineTool } from '@deepseek-ai/dsh-tools' @@ -140,7 +142,7 @@ export function apply(ctx: Context) { ### 在 cordis.yml 中组合 -```yaml ignore-check +```yaml - name: '@deepseek-ai/dsh-my-cap-local' - name: '@deepseek-ai/dsh-tool-my-cap' ``` @@ -153,4 +155,4 @@ export function apply(ctx: Context) { ## 下一步 -- [LLM 适配器](llm-adapter) — 实现一个 LLM 后端(最常见的 seam 扩展) +- [LLM 适配器](./llm-adapter.md) — 实现一个 LLM 后端(最常见的 seam 扩展) diff --git a/docs/user/develop/practice/llm-adapter.i18n.yaml b/docs/user/develop/practice/llm-adapter.i18n.yaml new file mode 100644 index 0000000000..30805c97b4 --- /dev/null +++ b/docs/user/develop/practice/llm-adapter.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 +llm-adapter.md: f34fc9e1d5b59a323bb562764821ef910025880e +llm-adapter.zh.md: 3c781ae8a1a011e2f73d5f6de43f6f75e1fb549f diff --git a/docs/user/develop/practice/llm-adapter.md b/docs/user/develop/practice/llm-adapter.md new file mode 100644 index 0000000000..f34fc9e1d5 --- /dev/null +++ b/docs/user/develop/practice/llm-adapter.md @@ -0,0 +1,185 @@ +# LLM adapters + +English | [中文](llm-adapter.zh.md) + +This guide connects a new LLM provider to Harness. + +## Overview + +An LLM adapter extends `LlmAdapter` and implements `stream()`, translating Harness's provider-neutral request into a provider API call and translating the response back into Harness chunks. + +## Minimal implementation + +```ts +import type { Context } from 'cordis' +import Schema from 'schemastery' +import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm' + +class MyAdapter extends LlmAdapter { + private apiKey: string + + constructor(apiKey: string) { + super() + this.apiKey = apiKey + } + + async *stream(options: GenerateOptions): AsyncIterable { + // 1. Convert options.messages to the provider format. + // 2. Call the streaming API. + // 3. Convert the response into StreamChunk values. + } +} + +export interface Config { + apiKey: string + models: string[] +} + +export const Config: Schema = Schema.object({ + apiKey: Schema.string().required(), + models: Schema.array(Schema.string()).required(), +}) + +export const name = 'my-llm-adapter' +export const inject = ['llm'] + +export function apply(ctx: Context, config: Config) { + const adapter = new MyAdapter(config.apiKey) + ctx.llm.registerAdapter(config.models, adapter) +} +``` + +## StreamChunk protocol + +`stream()` yields chunks using this protocol: + +```ts +import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm' + +async function* exampleChunks(): AsyncIterable { + // 1. Start each content block with block-start. + yield { type: 'block-start', index: 0, blockType: 'text' } + + // 2. Stream text through text-delta. + yield { type: 'text-delta', index: 0, text: 'Hello' } + yield { type: 'text-delta', index: 0, text: ' world' } + + // 3. End each content block with block-end and the complete block. + yield { + type: 'block-end', + index: 0, + block: { type: 'text', text: 'Hello world' }, + } + + // 4. Tool-call block. + yield { type: 'block-start', index: 1, blockType: 'tool-call' } + yield { + type: 'tool-call-delta', + index: 1, + id: CallId('call-123'), + name: 'bash', + argumentsDelta: '{"command":"ls"}', + } + yield { + type: 'block-end', + index: 1, + block: { + type: 'tool-call', + id: CallId('call-123'), + name: 'bash', + arguments: '{"command":"ls"}', + }, + } + + // 5. Token usage. + yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } } + + // 6. Finish reason. + yield { type: 'finish', reason: { kind: 'stop' } } + // Alternatively, { kind: 'tool-calls' } requests tool execution. +} +``` + +### Key rules + +- Every `block-start` has a matching `block-end`. +- `index` increases from 0 and identifies content-block order. +- A `tool-call-delta` carries raw JSON text in `argumentsDelta`, either all at once or over multiple chunks. +- `finish` is the final chunk. +- Emit `usage` before `finish`. + +## GenerateOptions + +`stream()` receives the exported `GenerateOptions` type. It includes the model, conversation history, system prompt, tool schemas, generation parameters, stop sequences, and abort signal; treat the TypeScript type exported by `@deepseek-ai/dsh-llm` as authoritative. Map supported fields to the provider API. If the provider cannot honor a field, throw `LlmError` with a stable code instead of silently dropping it. + +## Register an adapter + +```ts ignore-check +ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter) +``` + +The first argument lists the model names handled by the adapter. If `cordis.yml` selects `model: model-name-1`, the service routes that request to this adapter. + +## Use it from cordis.yml + +```yaml +- id: my-llm + name: './src/my-llm-adapter.ts' + config: + apiKey: !!js process.env.MY_API_KEY + models: + - my-model-v1 + - my-model-v2 + +- id: stdio-agent + name: '@deepseek-ai/dsh-stdio-demo' + config: + model: my-model-v1 # References the model registered above. +``` + +## Reference implementations + +The repository contains complete implementations: + +- `packages/llm/llm-deepseek/` — DeepSeek API adapter using the OpenAI-compatible format +- `packages/llm/llm-pi-ai/` — Pi AI adapter using a different API format +- `examples/echo-agent/src/mock-llm.ts` — minimal local teaching adapter + +Start with the mock adapter to study a complete chunk sequence without network behavior. + +## Error handling + +Adapters throw transport and protocol failures as `LlmError` values with stable codes. The agent loop preserves the error and code for diagnostics and policy; it does not convert an ordinary `Error` automatically. Every provider HTTP request must also merge `attributionHeaders()` and forward `options.signal`. + +```ts +import { + attributionHeaders, + LlmAdapter, + LlmError, + type GenerateOptions, + type StreamChunk, +} from '@deepseek-ai/dsh-llm' + +class HttpAdapter extends LlmAdapter { + constructor(private readonly endpoint: string) { + super() + } + + async *stream(options: GenerateOptions): AsyncIterable { + const response = await fetch(this.endpoint, { + method: 'POST', + headers: { + 'content-type': 'application/json', + ...attributionHeaders(), + }, + body: JSON.stringify({ model: options.model, messages: options.messages }), + ...options.signal ? { signal: options.signal } : {}, + }) + if (!response.ok) { + throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR') + } + // A real adapter parses the response and emits the complete chunk sequence. + yield { type: 'finish', reason: { kind: 'stop' } } + } +} +``` diff --git a/website/zh-CN/develop/practice/llm-adapter.md b/docs/user/develop/practice/llm-adapter.zh.md similarity index 59% rename from website/zh-CN/develop/practice/llm-adapter.md rename to docs/user/develop/practice/llm-adapter.zh.md index f2984820c3..3c781ae8a1 100644 --- a/website/zh-CN/develop/practice/llm-adapter.md +++ b/docs/user/develop/practice/llm-adapter.zh.md @@ -1,5 +1,7 @@ # LLM 适配器 +[English](llm-adapter.md) | 中文 + 本文介绍如何为 Harness 接入一个新的 LLM 提供方。 ## 概述 @@ -10,6 +12,7 @@ LLM 适配器是一个继承 `LlmAdapter` 的类,实现 `stream()` 方法, ```ts import type { Context } from 'cordis' +import Schema from 'schemastery' import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm' class MyAdapter extends LlmAdapter { @@ -21,9 +24,9 @@ class MyAdapter extends LlmAdapter { } async *stream(options: GenerateOptions): AsyncIterable { - // 1. 将 options.messages 转换为你的 API 格式 - // 2. 调用 API(流式) - // 3. 将 API 响应转换为 StreamChunk 序列 + // 1. Convert options.messages to the provider format. + // 2. Call the streaming API. + // 3. Convert the response into StreamChunk values. } } @@ -32,6 +35,11 @@ export interface Config { models: string[] } +export const Config: Schema = Schema.object({ + apiKey: Schema.string().required(), + models: Schema.array(Schema.string()).required(), +}) + export const name = 'my-llm-adapter' export const inject = ['llm'] @@ -48,22 +56,22 @@ export function apply(ctx: Context, config: Config) { ```ts import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm' -async function* demo(): AsyncIterable { - // 1. 每个内容块以 block-start 开始 +async function* exampleChunks(): AsyncIterable { + // 1. Start each content block with block-start. yield { type: 'block-start', index: 0, blockType: 'text' } - // 2. 文本块使用 text-delta + // 2. Stream text through text-delta. yield { type: 'text-delta', index: 0, text: 'Hello' } yield { type: 'text-delta', index: 0, text: ' world' } - // 3. 每个内容块以 block-end 结束(携带完整 block) + // 3. End each content block with block-end and the complete block. yield { type: 'block-end', index: 0, block: { type: 'text', text: 'Hello world' }, } - // 4. Tool call 块 + // 4. Tool-call block. yield { type: 'block-start', index: 1, blockType: 'tool-call' } yield { type: 'tool-call-delta', @@ -83,12 +91,12 @@ async function* demo(): AsyncIterable { }, } - // 5. Token 用量 + // 5. Token usage. yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } } - // 6. 结束原因 + // 6. Finish reason. yield { type: 'finish', reason: { kind: 'stop' } } - // 或: { kind: 'tool-calls' } 表示模型想调用 tool + // Alternatively, { kind: 'tool-calls' } requests tool execution. } ``` @@ -102,33 +110,11 @@ async function* demo(): AsyncIterable { ## GenerateOptions -`stream()` 接收的请求包含: - -```ts -import type { GenerateOptions } from '@deepseek-ai/dsh-llm' - -declare const options: GenerateOptions - -options.model // 模型名 -options.messages // 对话历史 (Message[]) -options.tools // 可用的 tool schema 列表 (ToolSchema[]) -options.system // 系统提示词 -options.maxTokens // 最大输出 token -options.temperature // 温度 -options.signal // 取消信号(必须响应) -``` - -你的适配器需要将这些映射到具体 API 的参数。 +`stream()` 接收仓库导出的 `GenerateOptions`。它包含模型名、对话历史、系统提示词、tool schema、生成参数、停止序列和中止信号;完整字段以 `@deepseek-ai/dsh-llm` 导出的 TypeScript 类型为准。适配器必须将支持的字段映射到具体 API;无法支持的字段应抛出带稳定 code 的 `LlmError`,不能静默丢弃。 ## 注册适配器 -```ts -import type { Context } from 'cordis' -import type { LlmAdapter } from '@deepseek-ai/dsh-llm' - -declare const ctx: Context -declare const adapter: LlmAdapter - +```ts ignore-check ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter) ``` @@ -148,7 +134,7 @@ ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter) - id: stdio-agent name: '@deepseek-ai/dsh-stdio-demo' config: - model: my-model-v1 # 引用上面注册的模型名 + model: my-model-v1 # References the model registered above. ``` ## 实战参考 @@ -163,20 +149,37 @@ mock 适配器是学习 StreamChunk 协议的最佳起点——它用纯本地 ## 错误处理 -适配器中的异常会被 agent-loop 捕获并转化为 `LlmError`,告知上层。不需要在 `stream()` 内部做错误恢复——让异常冒泡即可。 +适配器应将传输和协议故障作为带稳定 code 的 `LlmError` 抛出;agent loop 会保留该错误及其 code,供诊断和策略使用。不要依赖普通 `Error` 被自动转换。每个提供方 HTTP 请求还必须合并 `attributionHeaders()`,并传递 `options.signal`。 ```ts -import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { + attributionHeaders, + LlmAdapter, + LlmError, + type GenerateOptions, + type StreamChunk, +} from '@deepseek-ai/dsh-llm' class HttpAdapter extends LlmAdapter { - private endpoint = 'https://api.example.com/v1/chat' + constructor(private readonly endpoint: string) { + super() + } async *stream(options: GenerateOptions): AsyncIterable { - const response = await fetch(this.endpoint, { method: 'POST' }) + const response = await fetch(this.endpoint, { + method: 'POST', + headers: { + 'content-type': 'application/json', + ...attributionHeaders(), + }, + body: JSON.stringify({ model: options.model, messages: options.messages }), + ...options.signal ? { signal: options.signal } : {}, + }) if (!response.ok) { - throw new Error(`API error: ${response.status}`) + throw new LlmError(`Provider API error: ${response.status}`, 'PROVIDER_HTTP_ERROR') } - // ... 正常流式处理 + // A real adapter parses the response and emits the complete chunk sequence. + yield { type: 'finish', reason: { kind: 'stop' } } } } ``` diff --git a/docs/user/guide/config.i18n.yaml b/docs/user/guide/config.i18n.yaml new file mode 100644 index 0000000000..9894ca95bc --- /dev/null +++ b/docs/user/guide/config.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 +config.md: a3f56018fd43cc803c1710f97c29a77340a0b257 +config.zh.md: af661b9d7ef72e4085551202169e975bd0c3ec99 diff --git a/docs/user/guide/config.md b/docs/user/guide/config.md new file mode 100644 index 0000000000..a3f56018fd --- /dev/null +++ b/docs/user/guide/config.md @@ -0,0 +1,59 @@ +# Configuration + +English | [中文](config.zh.md) + +Harness uses `cordis.yml` to describe which plugins an agent loads and the configuration passed to each one. The file composes capabilities; the generated configuration catalog records the fields and defaults each package actually supports, avoiding a second hand-maintained reference. + +## Start from a real configuration + +The repository examples are runnable configurations and the most reliable starting points for a new project: + +- [echo-agent](../../../examples/echo-agent/cordis.yml) uses a local mock model and needs no API key. +- [repl-agent](../../../examples/repl-agent/cordis.yml) combines the DeepSeek model, Bash, filesystem, compaction, subagents, and workflows. +- [acp-agent](../../../examples/acp-agent/cordis.yml) connects to editor clients over ACP. + +A minimal configuration is a list of plugin entries: + +```yaml +- id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + config: + apiKey: !!js process.env.DEEPSEEK_API_KEY + models: + - deepseek-v4-flash + +- id: stdio-agent + name: '@deepseek-ai/dsh-stdio-demo' + config: + model: deepseek-v4-flash +``` + +## Plugin entries + +`name` identifies an npm package or a local module relative to `cordis.yml`; `id` gives the plugin instance a stable identity; and `config` supplies plugin-specific configuration. Set `disabled: true` to skip an entry temporarily. + +```yaml +- id: local-tool + name: './src/my-tool.ts' + disabled: false + config: + toolName: my_tool +``` + +Plugins load in file order. Place plugins that depend on services after the applications or capability plugins that provide them. Missing models, tools, and plugins fail as early as possible instead of being silently ignored. + +## JavaScript values and environment variables + +The Cordis loader evaluates runtime expressions tagged with `!!js`. Keep API keys and other secrets in the gitignored `.env` file at the repository root, never in committed configuration. + +```yaml +config: + apiKey: !!js process.env.DEEPSEEK_API_KEY + cwd: !!js process.cwd() +``` + +The tag is `!!js`, not `!js`. + +## Exact configuration reference + +The generated [plugin configuration catalog](../../config-catalog.md) lists every current field, type, and default. For composition concepts, continue to the [architecture](../../architecture.md) and [capability interfaces](../../capability-seams.md). To create a configuration, copy the closest entry from the [examples overview](../../../examples/README.md) and adapt it. diff --git a/docs/user/guide/config.zh.md b/docs/user/guide/config.zh.md new file mode 100644 index 0000000000..af661b9d7e --- /dev/null +++ b/docs/user/guide/config.zh.md @@ -0,0 +1,59 @@ +# 配置文件 + +[English](config.md) | 中文 + +Harness 使用 `cordis.yml` 描述 Agent 加载哪些插件以及每个插件的参数。配置文件负责组合能力;每个包真正支持的字段和默认值由源码生成的配置目录负责记录,避免两份手写表格逐渐不一致。 + +## 从真实配置开始 + +仓库中的示例就是可以运行的配置,也是新项目最可靠的起点: + +- [echo-agent](../../../examples/echo-agent/cordis.yml) 使用本地 mock 模型,不需要 API key。 +- [repl-agent](../../../examples/repl-agent/cordis.yml) 组合 DeepSeek 模型、Bash、文件系统、压缩、子代理和工作流。 +- [acp-agent](../../../examples/acp-agent/cordis.yml) 通过 ACP 接入编辑器客户端。 + +最小配置由一组插件条目组成: + +```yaml +- id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + config: + apiKey: !!js process.env.DEEPSEEK_API_KEY + models: + - deepseek-v4-flash + +- id: stdio-agent + name: '@deepseek-ai/dsh-stdio-demo' + config: + model: deepseek-v4-flash +``` + +## 插件条目 + +`name` 指定 npm 包或相对于 `cordis.yml` 的本地模块,`id` 为插件实例提供稳定标识,`config` 传入插件自己的配置。需要临时跳过某个条目时可设置 `disabled: true`。 + +```yaml +- id: local-tool + name: './src/my-tool.ts' + disabled: false + config: + toolName: my_tool +``` + +插件按文件中的顺序加载。依赖其他服务的插件应该排在提供这些服务的应用或能力插件之后;引用不存在的模型、工具或插件会尽早报错,而不是被静默忽略。 + +## JavaScript 值和环境变量 + +Cordis loader 使用 `!!js` 标签读取运行时表达式。API key 等凭据应放在仓库根目录、已被 Git 忽略的 `.env` 中,不能提交到配置文件。 + +```yaml +config: + apiKey: !!js process.env.DEEPSEEK_API_KEY + cwd: !!js process.cwd() +``` + +标签是 `!!js`,不是 `!js`。 + +## 精确配置参考 + +每个插件当前支持的字段、类型和默认值见自动生成的[插件配置目录](../../config-catalog.md)。理解插件如何组合可继续阅读[架构说明](../../architecture.md)和[能力接口](../../capability-seams.md);要创建自己的配置,优先复制并修改[示例目录说明](../../../examples/README.md)中最接近的例子。 diff --git a/docs/user/guide/index.i18n.yaml b/docs/user/guide/index.i18n.yaml new file mode 100644 index 0000000000..6743abcdd4 --- /dev/null +++ b/docs/user/guide/index.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 +index.md: a20b1041e13b01b6b1d01a5baa8975d3e68c6aa0 +index.zh.md: 56ec50352218e2e28ad2dd7a6ef387376de75606 diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md new file mode 100644 index 0000000000..a20b1041e1 --- /dev/null +++ b/docs/user/guide/index.md @@ -0,0 +1,49 @@ +# Introduction + +English | [中文](index.zh.md) + +DeepSeek Harness is a **plugin-based agent development framework** built on the [Cordis](https://github.com/cordiverse/cordis) microkernel. Its central idea is simple: **everything is a plugin**. + +## What it is + +Harness implements every capability an AI agent needs—including LLM calls, tool execution, session management, and subtask delegation—as a composable plugin. A `cordis.yml` file declares which plugins to load and how to configure them, assembling a complete agent. + +```yaml +# Select the LLM backend +- name: '@deepseek-ai/dsh-llm-deepseek' + config: + apiKey: !!js process.env.DEEPSEEK_API_KEY + +# Select the application template +- name: '@deepseek-ai/dsh-stdio-demo' + config: + model: deepseek-v4-flash +``` + +## Who it is for + +### Application users + +To run an existing agent application, such as a coding assistant or conversational agent: + +1. Copy an example template. +2. Add an API key. +3. Run it. + +No code is required. See the [quick start](./quickstart.md). + +### Plugin developers + +To add a custom tool, a new LLM adapter, or another execution backend, write a plugin. Harness provides explicit extension interfaces and a type-safe development experience. See [development](../develop/basic/). + +## Core features + +- **Configuration only** — `cordis.yml` selects the capability set; changing a model or adding a tool is a configuration edit. +- **Hot replacement (HMR)** — edit plugin code during development without restarting the process. + +## Technology + +- **Runtime**: Node.js ^22.19 or >= 24 +- **Language**: TypeScript (ESM) +- **Framework**: Cordis +- **Package manager**: pnpm workspaces (the repository pins pnpm 11) diff --git a/website/zh-CN/guide/index.md b/docs/user/guide/index.zh.md similarity index 84% rename from website/zh-CN/guide/index.md rename to docs/user/guide/index.zh.md index f0d77a3735..56ec503522 100644 --- a/website/zh-CN/guide/index.md +++ b/docs/user/guide/index.zh.md @@ -1,5 +1,7 @@ # 介绍 +[English](index.md) | 中文 + DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis](https://github.com/cordiverse/cordis) 微内核构建。它的核心理念是:**一切皆插件**。 ## 它是什么 @@ -7,12 +9,12 @@ DeepSeek Harness 是一个**插件化的 Agent 开发框架**,基于 [Cordis]( Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调用、工具执行、会话管理、子任务分配——全部构建为可组合的插件。你通过一个 `cordis.yml` 配置文件来声明加载哪些插件、使用什么参数,就能组装出一个完整的 Agent。 ```yaml -# 选择 LLM 后端 +# Select the LLM backend - name: '@deepseek-ai/dsh-llm-deepseek' config: apiKey: !!js process.env.DEEPSEEK_API_KEY -# 选择应用模板 +# Select the application template - name: '@deepseek-ai/dsh-stdio-demo' config: model: deepseek-v4-flash @@ -28,7 +30,7 @@ Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调 2. 填写 API key 3. 运行 -不需要写任何代码。详见 [快速开始](quickstart)。 +不需要写任何代码。详见 [快速开始](./quickstart.md)。 ### 插件开发者 @@ -41,7 +43,7 @@ Harness 将一个 AI Agent(智能体) 所需要的所有能力——LLM 调 ## 技术栈 -- **运行时**: Node.js >= 24 +- **运行时**: Node.js ^22.19 或 >= 24 - **语言**: TypeScript (ESM) - **框架**: Cordis -- **包管理**: pnpm workspaces +- **包管理**: pnpm workspaces(仓库固定使用 pnpm 11) diff --git a/docs/user/guide/quickstart.i18n.yaml b/docs/user/guide/quickstart.i18n.yaml new file mode 100644 index 0000000000..a4898be8e0 --- /dev/null +++ b/docs/user/guide/quickstart.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 +quickstart.md: acae2ac095e057971043c2bcece7a52d3ebc1c2c +quickstart.zh.md: 54643fe54e62dbbd3696362cb43ff8569577c53b diff --git a/docs/user/guide/quickstart.md b/docs/user/guide/quickstart.md new file mode 100644 index 0000000000..acae2ac095 --- /dev/null +++ b/docs/user/guide/quickstart.md @@ -0,0 +1,99 @@ +# Quick start + +English | [中文](quickstart.zh.md) + +This guide gets an agent running in five minutes. + +## Prerequisites + +- [Node.js](https://nodejs.org/) ^22.19 or >= 24 +- [pnpm](https://pnpm.io/) 11 (use Corepack to select the repository-pinned version) + +```sh +# Check versions +node -v # v22.19.x, or v24.x and newer +corepack enable +pnpm -v # 11.x +``` + +## Step 1: run echo-agent + +echo-agent needs no API key and runs after dependencies are installed. + +```sh +# Clone the repository +git clone https://github.com/deepseek-harness/deepseek-harness.git +cd deepseek-harness + +# Install dependencies +pnpm install + +# Start echo-agent +pnpm run demo:echo +``` + +The process prints: + +``` +echo-agent ready. Type a message ("echo " triggers the tool). +> +``` + +Enter: + +``` +> echo hello world +``` + +The model issues a tool call, and the echo tool returns the text in uppercase: + +``` +[tool call] echo({"text":"hello world"}) +[tool result] ECHO: HELLO WORLD +``` + +Your local environment is ready. + +## Step 2: use a real model + +Next, connect a real DeepSeek model and run the complete command-line agent. + +### Get an API key + +Get an API key from [DeepSeek Platform](https://platform.deepseek.com/). + +### Configure the environment + +Create a gitignored `.env` file in the repository root: + +```sh +DEEPSEEK_API_KEY=sk-your-key-here +``` + +### Start repl-agent + +```sh +pnpm run demo:repl +``` + +``` +agent REPL ready. Give it a coding task. +> +``` + +This is a complete coding assistant that can read and write files, run commands, and delegate subtasks. + +Try a task: + +``` +> Create hello.js in the current directory, print "Hello from Harness!", and run it +``` + +## What happened + +echo-agent and repl-agent use the same application framework (`@deepseek-ai/dsh-stdio-demo`). Their `cordis.yml` files select different plugins and configuration. Custom agents use the same composition model. + +## Next steps + +- [Configuration](./config.md) — understand the `cordis.yml` format +- [Develop a plugin](../develop/basic/) — build your own tool or backend diff --git a/website/zh-CN/guide/quickstart.md b/docs/user/guide/quickstart.zh.md similarity index 76% rename from website/zh-CN/guide/quickstart.md rename to docs/user/guide/quickstart.zh.md index 13e25694de..54643fe54e 100644 --- a/website/zh-CN/guide/quickstart.md +++ b/docs/user/guide/quickstart.zh.md @@ -1,16 +1,19 @@ # 快速开始 +[English](quickstart.md) | 中文 + 本指南带你在 5 分钟内跑起一个 Agent。 ## 环境准备 -- [Node.js](https://nodejs.org/) >= 24 -- [pnpm](https://pnpm.io/) >= 9 +- [Node.js](https://nodejs.org/) ^22.19 或 >= 24 +- [pnpm](https://pnpm.io/) 11(建议通过 Corepack 使用仓库固定的版本) ```sh -# 确认版本 -node -v # v24.x 或更高 -pnpm -v # 9.x 或更高 +# Check versions +node -v # v22.19.x, or v24.x and newer +corepack enable +pnpm -v # 11.x ``` ## 第一步:运行 echo-agent @@ -18,16 +21,14 @@ pnpm -v # 9.x 或更高 echo-agent 不需要 API key,装好依赖就能跑。 ```sh -# 克隆仓库 +# Clone the repository git clone https://github.com/deepseek-harness/deepseek-harness.git cd deepseek-harness -# 安装依赖 +# Install dependencies pnpm install -# 如果看到 ERR_PNPM_IGNORED_BUILDS,可以忽略——安装已经成功了。 -# 想消除这个提示可以跑一次: pnpm approve-builds -# 启动 echo-agent +# Start echo-agent pnpm run demo:echo ``` @@ -85,7 +86,7 @@ agent REPL ready. Give it a coding task. 试着给它一个任务: ``` -> 在当前目录创建一个 hello.js,内容是打印 "Hello from Harness!",然后运行它 +> Create hello.js in the current directory, print "Hello from Harness!", and run it ``` ## 回头看 @@ -94,5 +95,5 @@ echo-agent 和 repl-agent 用的是同一个应用框架(`@deepseek-ai/dsh-stdio ## 下一步 -- [配置文件](config) — 了解 `cordis.yml` 的完整语法 +- [配置文件](./config.md) — 了解 `cordis.yml` 的完整语法 - [开发插件](../develop/basic/) — 编写你自己的 tool 或后端 diff --git a/docs/user/index.i18n.yaml b/docs/user/index.i18n.yaml new file mode 100644 index 0000000000..b3fc8da2d2 --- /dev/null +++ b/docs/user/index.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 +index.md: e9a1f03785c7472c47550ec59ea0165d28d3d9a6 +index.zh.md: 907f1452c9ff50d619989c18dcf2727addb2573d diff --git a/docs/user/index.md b/docs/user/index.md new file mode 100644 index 0000000000..e9a1f03785 --- /dev/null +++ b/docs/user/index.md @@ -0,0 +1,25 @@ +--- +layout: home +hero: + name: DeepSeek Harness + text: Plugin-based agent development framework + tagline: Built on the Cordis microkernel; everything is a plugin + actions: + - theme: brand + text: Quick start + link: /en/guide/quickstart + - theme: alt + text: Develop plugins + link: /en/develop/basic/ +features: + - title: Plugin architecture + details: Built on the Cordis plugin system. Every capability is registered by a plugin, takes effect when loaded, and is reverted when unloaded. + - title: Configuration as composition + details: One cordis.yml determines the agent's complete capability set. Change a model or add a tool by editing configuration. + - title: Ready to use + details: Includes LLM calls, file access, Bash execution, subagent delegation, and the rest of the core toolchain. Copy a template to get started. +--- + +# DeepSeek Harness + +English | [中文](index.zh.md) diff --git a/website/zh-CN/index.md b/docs/user/index.zh.md similarity index 78% rename from website/zh-CN/index.md rename to docs/user/index.zh.md index 90b23e483a..907f1452c9 100644 --- a/website/zh-CN/index.md +++ b/docs/user/index.zh.md @@ -7,15 +7,19 @@ hero: actions: - theme: brand text: 快速开始 - link: /zh-CN/guide/quickstart + link: /guide/quickstart - theme: alt text: 开发插件 - link: /zh-CN/develop/basic/ + link: /develop/basic/ features: - title: 插件化架构 - details: 基于 Cordis 效果系统,所有能力通过插件注册,加载即生效、卸载即还原。 + details: 基于 Cordis 插件系统,所有能力通过插件注册,加载即生效、卸载即还原。 - title: 配置即组合 details: 一个 cordis.yml 决定整个 Agent 的能力组合——换模型、加工具,只需改一行配置。 - title: 开箱即用 details: 内置 LLM 调用、文件读写、Bash 执行、子代理委派等完整工具链,复制模板即可运行。 --- + +# DeepSeek Harness + +[English](index.md) | 中文 diff --git a/eslint.config.mjs b/eslint.config.mjs index 57a2d618ea..03dcf7edac 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,6 +12,7 @@ export default tseslint.config( '**/.sessions/**', '.claude/**', // harness-local state (worktrees, skills) — other checkouts, not this one's sources '**/.doc-typecheck-*/**', + 'website/.generated/**', 'vendor/**', // vendored source keeps upstream style and idioms 'native/**', // imported landlock-run subtree: self-contained workspace with its own gates (native/README.md) '**/*.js', @@ -22,7 +23,7 @@ export default tseslint.config( // --- our packages: full strictness ------------------------------------- { - files: ['packages/*/*/src/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts'], + files: ['packages/*/*/src/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts'], extends: [ ...tseslint.configs.strictTypeChecked, ], @@ -109,7 +110,7 @@ export default tseslint.config( // --- file-local duplication (all owned TypeScript) --------------------- { - files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts'], + files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts'], plugins: { sonarjs }, rules: { // Cross-file clones are covered separately by jscpd. @@ -126,7 +127,7 @@ export default tseslint.config( // --- formatting (everything we own) ------------------------------------- { - files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'eslint.config.mjs'], + files: ['packages/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts', 'eslint.config.mjs'], plugins: { '@stylistic': stylistic }, rules: { '@stylistic/indent': ['error', 2], diff --git a/examples/acp-agent/README.md b/examples/acp-agent/README.md index ab559388e7..6e97204963 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, 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 the unconfined in-process filesystem stack and 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, 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,7 +29,7 @@ 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). Filesystem tools are omitted from the confined default because they execute in-process and do not ride the bash sandbox. +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`. ## Snapshot tests (record-once / replay-deterministic) @@ -37,11 +37,11 @@ This example hosts the ACP snapshot suite. It replays through `dsh-llm-replay`, ## Permissions and sandboxing -The default tree 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/). Bash starts in `workspace-write`; a denied operation returns a structured marker, and a retry with `sandbox_permissions` plus `justification` becomes a one-shot `session/request_permission` prompt in the editor. "Allow once" runs exactly that retry under the wider mode ([sandbox Agent Note § Escalation](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)). +The default tree composes [`@deepseek-ai/dsh-sandbox-local`](../../packages/sandbox/sandbox-local/), [`@deepseek-ai/dsh-sandbox-policy`](../../packages/sandbox/sandbox-policy/), [`@deepseek-ai/dsh-bash-sandbox`](../../packages/bash/bash-sandbox/), [`@deepseek-ai/dsh-fs-sandbox`](../../packages/fs/fs-sandbox/), [`@deepseek-ai/dsh-user-approval`](../../packages/ui/user-approval/), and [`@deepseek-ai/dsh-permission`](../../packages/ui/permission/). Bash and the `read`/`write`/`edit` tools start in `workspace-write`; a denied operation returns a structured marker, and a retry with `sandbox_permissions` plus `justification` becomes a one-shot `session/request_permission` prompt in the editor. "Allow once" runs exactly that retry under the wider mode ([sandbox Agent Note § Escalation](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)). - **One session config option is live**: a capable client shows one `Permissions` select. `workspace-write` means workspace-confined bash plus `ask`; `danger-full-access` means unconfined bash plus `never`. Switching writes one `permission/preset` event through to the sandbox-mode and approval-policy events, and `session/load` reports the resumed value. - **Every approval is one-shot**: the choices are `Allow once` and `Reject`; a dismissal, rejection, missing editor, or unavailable runner fails closed. -- **The boundary is bash-only and config-fixed today**: in-process filesystem tools are omitted from the confined live default, while the sandbox workspace root remains the server's launch directory. +- **The boundary spans bash and the filesystem tools, and is config-fixed today**: bash confines through the OS runner and the `read`/`write`/`edit` tools through an in-process path fence ([`dsh-fs-sandbox`](../../packages/fs/fs-sandbox/)), both keyed to the same `workspaceRoot` — which remains the server's launch directory (a per-session root is deferred). `tests/escalation.e2e.ts` boots this default tree keyless, drives the permission select, and—with a key and usable runner—proves both approval outcomes against the filesystem. Most snapshots use that tree and start at `danger-full-access` so bash fixtures remain runner-independent; scenarios that call `read`, `write`, or `edit` use the fixed full-access fs overlay and a separate request-header pin. The permission-switching and escalation inputs select `workspace-write` before exercising the bash policy path. No fixture pins a real denial because kernel error text is backend-specific; real confinement remains covered by the sandbox packages' kernel e2e suites. diff --git a/examples/acp-agent/code-mode-workspace-context.cordis.snapshot.yml b/examples/acp-agent/code-mode-workspace-context.cordis.snapshot.yml index a741091cce..8dad8832a0 100644 --- a/examples/acp-agent/code-mode-workspace-context.cordis.snapshot.yml +++ b/examples/acp-agent/code-mode-workspace-context.cordis.snapshot.yml @@ -1,5 +1,5 @@ -# Keyless replay counterpart of code-mode-workspace-context.cordis.yml. It -# enables the filesystem entries needed by this scenario and swaps in replay. +# Keyless replay counterpart of code-mode-workspace-context.cordis.yml. It adds +# Code Mode to the default filesystem suite and swaps in replay. - id: base name: '@cordisjs/plugin-include' config: @@ -23,14 +23,6 @@ Verify your work by running the code or tests. Keep answers brief and factual. - insert: - - id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.cwd() - - id: fs-policy - name: '@deepseek-ai/dsh-fs-policy' - - id: tool-fs - name: '@deepseek-ai/dsh-tool-fs' - id: code-runtime name: '@deepseek-ai/dsh-code-runtime-worker' - id: llm-replay diff --git a/examples/acp-agent/code-mode-workspace-context.cordis.yml b/examples/acp-agent/code-mode-workspace-context.cordis.yml index b932f06e64..71edf9750e 100644 --- a/examples/acp-agent/code-mode-workspace-context.cordis.yml +++ b/examples/acp-agent/code-mode-workspace-context.cordis.yml @@ -1,5 +1,5 @@ -# Code Mode workspace-context snapshot recording overlay. The scenario needs -# filesystem tools to trigger nested instruction discovery after a read. +# Code Mode workspace-context snapshot recording overlay. The default filesystem +# tools trigger nested instruction discovery after a read. - id: base name: '@cordisjs/plugin-include' config: @@ -20,13 +20,5 @@ Verify your work by running the code or tests. Keep answers brief and factual. - insert: - - id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.cwd() - - id: fs-policy - name: '@deepseek-ai/dsh-fs-policy' - - id: tool-fs - name: '@deepseek-ai/dsh-tool-fs' - id: code-runtime name: '@deepseek-ai/dsh-code-runtime-worker' diff --git a/examples/acp-agent/composition.md b/examples/acp-agent/composition.md index 7f016e685c..a598f109ad 100644 --- a/examples/acp-agent/composition.md +++ b/examples/acp-agent/composition.md @@ -12,6 +12,8 @@ flowchart LR cfg --> plugin_acp_llm_deepseek plugin_acp_sandbox["sandbox
@deepseek-ai/dsh-sandbox-local"] cfg --> plugin_acp_sandbox + plugin_acp_sandbox_policy["sandbox-policy
@deepseek-ai/dsh-sandbox-policy"] + cfg --> plugin_acp_sandbox_policy plugin_acp_bash["bash
@deepseek-ai/dsh-bash-sandbox"] cfg --> plugin_acp_bash plugin_acp_approval["approval
@deepseek-ai/dsh-user-approval"] @@ -49,6 +51,12 @@ flowchart LR cfg --> plugin_acp_tool_todo plugin_acp_repeat_tool_guard["repeat-tool-guard
@deepseek-ai/dsh-repeat-tool-guard"] cfg --> plugin_acp_repeat_tool_guard + plugin_acp_fs_sandbox["fs-sandbox
@deepseek-ai/dsh-fs-sandbox"] + cfg --> plugin_acp_fs_sandbox + plugin_acp_fs_policy["fs-policy
@deepseek-ai/dsh-fs-policy"] + cfg --> plugin_acp_fs_policy + plugin_acp_tool_fs["tool-fs
@deepseek-ai/dsh-tool-fs"] + cfg --> plugin_acp_tool_fs plugin_acp_hooks_claude["hooks-claude
@deepseek-ai/dsh-hooks-claude"] cfg --> plugin_acp_hooks_claude plugin_acp_hooks_codex["hooks-codex
@deepseek-ai/dsh-hooks-codex"] @@ -59,6 +67,7 @@ flowchart LR | --- | --- | | `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` | | `sandbox` | `@deepseek-ai/dsh-sandbox-local` | +| `sandbox-policy` | `@deepseek-ai/dsh-sandbox-policy` | | `bash` | `@deepseek-ai/dsh-bash-sandbox` | | `approval` | `@deepseek-ai/dsh-user-approval` | | `permission` | `@deepseek-ai/dsh-permission` | @@ -74,6 +83,9 @@ flowchart LR | `tool-workflow` | `@deepseek-ai/dsh-tool-workflow` | | `tool-todo` | `@deepseek-ai/dsh-tool-todo` | | `repeat-tool-guard` | `@deepseek-ai/dsh-repeat-tool-guard` | +| `fs-sandbox` | `@deepseek-ai/dsh-fs-sandbox` | +| `fs-policy` | `@deepseek-ai/dsh-fs-policy` | +| `tool-fs` | `@deepseek-ai/dsh-tool-fs` | | `hooks-claude` | `@deepseek-ai/dsh-hooks-claude` | | `hooks-codex` | `@deepseek-ai/dsh-hooks-codex` | diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml index 510fac46b6..b2ad545fe7 100644 --- a/examples/acp-agent/cordis.yml +++ b/examples/acp-agent/cordis.yml @@ -10,17 +10,25 @@ apiKey: !!js process.env.DEEPSEEK_API_KEY baseURL: !!js process.env.DEEPSEEK_BASE_URL -# The default composition confines bash to the workspace and asks before a -# wider retry. Snapshots use danger-full-access; DSH_PERMISSION_MODE overrides -# both mode and approval policy for deployments and tests. +# The default composition confines bash AND the filesystem tools to the +# workspace and asks before a wider retry. Snapshot runs select +# danger-full-access so the established scenarios remain runner-independent; +# DSH_PERMISSION_MODE provides the same explicit deployment/test override +# outside the snapshot harness. The sandbox mode + workspace root live on +# ctx.sandboxPolicy — the one home both enforcing families (bash, fs) read. - id: sandbox name: '@deepseek-ai/dsh-sandbox-local' + +- id: sandbox-policy + name: '@deepseek-ai/dsh-sandbox-policy' + config: + mode: !!js "process.env.DSH_PERMISSION_MODE ?? (process.env.DSH_SNAPSHOT === undefined ? 'workspace-write' : 'danger-full-access')" + workspaceRoot: !!js process.cwd() + - id: bash name: '@deepseek-ai/dsh-bash-sandbox' config: timeoutMs: 60000 - mode: !!js "process.env.DSH_PERMISSION_MODE ?? (process.env.DSH_SNAPSHOT === undefined ? 'workspace-write' : 'danger-full-access')" - workspaceRoot: !!js process.cwd() - id: approval name: '@deepseek-ai/dsh-user-approval' @@ -86,12 +94,14 @@ config: provider: spawn toolName: subagent + maxDepth: 1 - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: provider: fork toolName: subagent_fork + maxDepth: 1 # The worker-thread workflow engine fans a model-written JavaScript script's @@ -112,6 +122,22 @@ - id: repeat-tool-guard name: '@deepseek-ai/dsh-repeat-tool-guard' +# The filesystem stack rides the SAME sandbox policy as bash: dsh-fs-sandbox +# replaces dsh-fs-local behind ctx.fs and fences write/edit by the effective +# mode (read-only denies, workspace-write contains to the workspace + temp +# roots, danger-full-access passes through), so read/write/edit are available +# under every mode. fs-policy (read-before-edit) composes orthogonally on top. +- id: fs-sandbox + name: '@deepseek-ai/dsh-fs-sandbox' + config: + cwd: !!js process.cwd() + +- id: fs-policy + name: '@deepseek-ai/dsh-fs-policy' + +- id: tool-fs + name: '@deepseek-ai/dsh-tool-fs' + # `configPath` is read once at load and resolves from the server launch cwd, not # `session/new.cwd`; one `hooks.json` therefore applies to every session and a # project-local file is not discovered. Missing config registers nothing. Hook diff --git a/examples/acp-agent/depth-two.cordis.snapshot.yml b/examples/acp-agent/depth-two.cordis.snapshot.yml new file mode 100644 index 0000000000..16aa6f587c --- /dev/null +++ b/examples/acp-agent/depth-two.cordis.snapshot.yml @@ -0,0 +1,36 @@ +# Keyless counterpart to depth-two.cordis.yml: apply the depth patch and replace +# the live adapter with per-session replay. +- id: base + name: '@cordisjs/plugin-include' + config: + path: ./cordis.yml + patches: + - id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + disabled: true + - id: sandbox + name: '@deepseek-ai/dsh-sandbox-local' + config: + runnerCommand: + - bash + - -c + - while [ "$1" != "--" ]; do shift; done; shift; exec "$@" + - passthrough-runner + runnerFailureSignatures: + - 'passthrough-runner: profile rejected' + - id: tool-subagent + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn + toolName: subagent + maxDepth: 2 + - insert: + - id: llm-replay + name: '@deepseek-ai/dsh-llm-replay' + config: + providers: + - id: deepseek + name: DeepSeek + models: + - id: deepseek-v4-flash + - id: deepseek-v4-pro diff --git a/examples/acp-agent/depth-two.cordis.yml b/examples/acp-agent/depth-two.cordis.yml new file mode 100644 index 0000000000..25b0ee8e38 --- /dev/null +++ b/examples/acp-agent/depth-two.cordis.yml @@ -0,0 +1,13 @@ +# Depth-limit snapshot overlay: keep the default composition and allow two +# generations of spawn children before runtime enforcement rejects another. +- id: base + name: '@cordisjs/plugin-include' + config: + path: ./cordis.yml + patches: + - id: tool-subagent + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn + toolName: subagent + maxDepth: 2 diff --git a/examples/acp-agent/fs.cordis.snapshot.yml b/examples/acp-agent/fs.cordis.snapshot.yml index da0b2ca59b..d55521a8dc 100644 --- a/examples/acp-agent/fs.cordis.snapshot.yml +++ b/examples/acp-agent/fs.cordis.snapshot.yml @@ -1,5 +1,6 @@ -# Keyless filesystem snapshots apply the filesystem and replay overlays directly -# because include patches cannot target entries behind a nested include. +# Keyless filesystem snapshots apply the spill and replay overlays directly +# because include patches cannot target entries behind a nested include. The +# sandboxed filesystem stack already lives in the base cordis.yml. - id: base name: '@cordisjs/plugin-include' config: @@ -9,14 +10,6 @@ name: '@deepseek-ai/dsh-llm-deepseek' disabled: true - insert: - - id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.cwd() - - id: fs-policy - name: '@deepseek-ai/dsh-fs-policy' - - id: tool-fs - name: '@deepseek-ai/dsh-tool-fs' - id: spill-local name: '@deepseek-ai/dsh-spill-local' config: diff --git a/examples/acp-agent/fs.cordis.yml b/examples/acp-agent/fs.cordis.yml index 52ca959a89..0d667255c8 100644 --- a/examples/acp-agent/fs.cordis.yml +++ b/examples/acp-agent/fs.cordis.yml @@ -1,20 +1,12 @@ -# Filesystem snapshots need the in-process local provider, policy gate, and -# model-facing tools. This explicit overlay is always full-access: the session -# permission preset controls bash only and cannot confine or unmount these plugins. +# Filesystem-scenario overlay: the sandboxed filesystem stack already lives in +# the base cordis.yml, so this overlay adds only the local tool-result spill +# storage those scenarios exercise. - id: base name: '@cordisjs/plugin-include' config: path: ./cordis.yml patches: - insert: - - id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.cwd() - - id: fs-policy - name: '@deepseek-ai/dsh-fs-policy' - - id: tool-fs - name: '@deepseek-ai/dsh-tool-fs' - id: spill-local name: '@deepseek-ai/dsh-spill-local' config: diff --git a/examples/acp-agent/tests/acp.snapshot.ts b/examples/acp-agent/tests/acp.snapshot.ts index 4bb08db20e..88ae18222c 100644 --- a/examples/acp-agent/tests/acp.snapshot.ts +++ b/examples/acp-agent/tests/acp.snapshot.ts @@ -30,6 +30,7 @@ const BOTH_MODE_CONFIG = fileURLToPath(new URL('../both-mode.cordis.yml', import const WORKSPACE_CONTEXT_CONFIG = fileURLToPath(new URL('../workspace-context.cordis.yml', import.meta.url)) const ADVANCED_CONFIG = fileURLToPath(new URL('../advanced.cordis.yml', import.meta.url)) const FS_CONFIG = fileURLToPath(new URL('../fs.cordis.yml', import.meta.url)) +const DEPTH_TWO_CONFIG = fileURLToPath(new URL('../depth-two.cordis.yml', import.meta.url)) function snapshotModeFromEnv(value: string | undefined): SnapshotSuiteOptions['mode'] { switch (value) { @@ -53,24 +54,25 @@ const SCENARIOS: Scenario[] = [ // Its prompt and tool-schema sidecars pin the composed header. { name: 'text-turn', hasModelTurn: true, recorded: true, pinsHeader: true }, { name: 'tool-call-turn', hasModelTurn: true, recorded: true }, + // The fs overlay only adds the spill stack (the sandboxed filesystem tools + // live in the base tree), so these scenarios share the default header class. { name: 'parallel-tool-calls', hasModelTurn: true, recorded: false, - headerClass: 'fs', configPath: FS_CONFIG, }, - { name: 'bash-spill', hasModelTurn: true, recorded: false, headerClass: 'fs', configPath: FS_CONFIG }, + { name: 'bash-spill', hasModelTurn: true, recorded: false, configPath: FS_CONFIG }, { name: 'fs-terminal-card', hasModelTurn: true, recorded: true }, { name: 'todo-plan', hasModelTurn: true, recorded: true }, { name: 'skill-load', hasModelTurn: true, recorded: false, pinsHeader: true, headerClass: 'skill' }, - { name: 'workspace-edit', hasModelTurn: true, recorded: true, pinsHeader: true, headerClass: 'fs', configPath: FS_CONFIG }, - { name: 'fs-read', hasModelTurn: true, recorded: true, headerClass: 'fs', configPath: FS_CONFIG }, - { name: 'fs-write', hasModelTurn: true, recorded: true, headerClass: 'fs', configPath: FS_CONFIG }, - { name: 'fs-edit', hasModelTurn: true, recorded: true, headerClass: 'fs', configPath: FS_CONFIG }, - { name: 'fs-write-overwrite', hasModelTurn: true, recorded: true, headerClass: 'fs', configPath: FS_CONFIG }, - { name: 'fs-read-window', hasModelTurn: true, recorded: true, headerClass: 'fs', configPath: FS_CONFIG }, - { name: 'fs-policy-reject', hasModelTurn: true, recorded: true, headerClass: 'fs', configPath: FS_CONFIG }, + { name: 'workspace-edit', hasModelTurn: true, recorded: true }, + { name: 'fs-read', hasModelTurn: true, recorded: true }, + { name: 'fs-write', hasModelTurn: true, recorded: true }, + { name: 'fs-edit', hasModelTurn: true, recorded: true }, + { name: 'fs-write-overwrite', hasModelTurn: true, recorded: true }, + { name: 'fs-read-window', hasModelTurn: true, recorded: true }, + { name: 'fs-policy-reject', hasModelTurn: true, recorded: true }, { name: 'multi-turn', hasModelTurn: true, recorded: true }, // ACP exposes the adapter catalog as a session-scoped model select. This // scenario pins the default flash request, the switch response, and the @@ -108,6 +110,13 @@ const SCENARIOS: Scenario[] = [ { name: 'subagent-multi', hasModelTurn: true, recorded: true }, { name: 'subagent-fork', hasModelTurn: true, recorded: true }, { name: 'subagent-mixed', hasModelTurn: true, recorded: true }, + { + name: 'subagent-depth-two-rejection', + hasModelTurn: true, + recorded: false, + overridden: true, + configPath: DEPTH_TWO_CONFIG, + }, // The workflow tool: the model writes a one-child orchestration script; the // child runs as a spawn subagent under the worker-thread engine (its session is the // child fixture), and the tool result carries the script's return value. @@ -175,6 +184,7 @@ const SCENARIOS: Scenario[] = [ { name: 'permission-switching', hasModelTurn: true, recorded: true, pinsHeader: true, expectedHeaderChanges: 1, headerClass: 'sandbox' }, { name: 'escalation-approved', hasModelTurn: true, recorded: true, headerClass: 'sandbox' }, { name: 'escalation-rejected', hasModelTurn: true, recorded: true, headerClass: 'sandbox' }, + { name: 'fs-escalation-approved', hasModelTurn: true, recorded: true, headerClass: 'sandbox' }, ] defineAcpSnapshotSuite({ diff --git a/examples/acp-agent/tests/fixtures/live-mode-switching-2026-07-07.session.jsonl b/examples/acp-agent/tests/fixtures/live-mode-switching-2026-07-07.session.jsonl new file mode 100644 index 0000000000..10ffb8c507 --- /dev/null +++ b/examples/acp-agent/tests/fixtures/live-mode-switching-2026-07-07.session.jsonl @@ -0,0 +1,2212 @@ +{"type":"session","version":0,"id":"ed16a7e7-a76f-459f-b889-d4c424d66ef6","createdAt":1783421406247,"cwd":"/Users/wwl/workspace/deepseek-harness","delegationDepth":0} +{"type":"turn/start","seq":0,"time":1783421410388,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1783421410388,"data":{"content":[{"type":"text","text":"你好"}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1783421410389,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1783421410389,"data":{"header":{"config":{"model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are a coding assistant powered by the deepseek-v4-flash model. Your working\ndirectory is /Users/wwl/workspace/deepseek-harness. Your bash tool runs under a file sandbox — a\n`[sandbox: file access denied …]` result is policy, not a command bug.\n\nVerify your work by running the code or tests. Keep answers brief and\nfactual.\n\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nBash commands run under the \"read-only\" file sandbox.","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. 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 (a background task reports the same marker via bash_output once it has finished). 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; poll it with `bash_output` and stop it with `bash_kill`. The ONE sanctioned exception to a sandbox denial: retry the exact same command once with `sandbox_permissions` (the wider mode it needs) plus a one-sentence `justification` — the user is asked to approve that single run. Never request escalation before a real denial, and treat a rejected escalation as final: stop and explain instead of working around it.","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. 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":"bash_output","description":"Read new output from a background bash task started with `bash` + `run_in_background`. Returns only output produced since the previous bash_output call, plus the task status. Tasks keep running while you do other work; poll again later for more output.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the bash tool."}},"required":["task_id"]}},{"name":"bash_kill","description":"Ask the executor to kill a running background bash task by task id.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the bash tool."}},"required":["task_id"]}}]},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1783421411079,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":5,"time":1783421411079,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":6,"time":1783421411233,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":7,"time":1783421411262,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} +{"type":"assistant/chunk","seq":8,"time":1783421411262,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" greeting"}}} +{"type":"assistant/chunk","seq":9,"time":1783421411263,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":10,"time":1783421411263,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} +{"type":"assistant/chunk","seq":11,"time":1783421411290,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Chinese"}}} +{"type":"assistant/chunk","seq":12,"time":1783421411290,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":13,"time":1783421411290,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":14,"time":1783421411290,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":15,"time":1783421411290,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" respond"}}} +{"type":"assistant/chunk","seq":16,"time":1783421411291,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} +{"type":"assistant/chunk","seq":17,"time":1783421411318,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Chinese"}}} +{"type":"assistant/chunk","seq":18,"time":1783421411318,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" as"}}} +{"type":"assistant/chunk","seq":19,"time":1783421411347,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" well"}}} +{"type":"assistant/chunk","seq":20,"time":1783421411347,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":21,"time":1783421411347,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":22,"time":1783421411347,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"你好"}}} +{"type":"assistant/chunk","seq":23,"time":1783421411347,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"!"}}} +{"type":"assistant/chunk","seq":24,"time":1783421411347,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"我是"}}} +{"type":"assistant/chunk","seq":25,"time":1783421411405,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"基于"}}} +{"type":"assistant/chunk","seq":26,"time":1783421411405,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" Deep"}}} +{"type":"assistant/chunk","seq":27,"time":1783421411405,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"Se"}}} +{"type":"assistant/chunk","seq":28,"time":1783421411405,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ek"}}} +{"type":"assistant/chunk","seq":29,"time":1783421411405,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" Har"}}} +{"type":"assistant/chunk","seq":30,"time":1783421411429,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ness"}}} +{"type":"assistant/chunk","seq":31,"time":1783421411429,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" SDK"}}} +{"type":"assistant/chunk","seq":32,"time":1783421411429,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":33,"time":1783421411429,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"的"}}} +{"type":"assistant/chunk","seq":34,"time":1783421411429,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" AI"}}} +{"type":"assistant/chunk","seq":35,"time":1783421411430,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":36,"time":1783421411456,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"助手"}}} +{"type":"assistant/chunk","seq":37,"time":1783421411457,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":38,"time":1783421411457,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"由"}}} +{"type":"assistant/chunk","seq":39,"time":1783421411485,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" deep"}}} +{"type":"assistant/chunk","seq":40,"time":1783421411485,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"seek"}}} +{"type":"assistant/chunk","seq":41,"time":1783421411486,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"-v"}}} +{"type":"assistant/chunk","seq":42,"time":1783421411486,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"4"}}} +{"type":"assistant/chunk","seq":43,"time":1783421411486,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"-fl"}}} +{"type":"assistant/chunk","seq":44,"time":1783421411486,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ash"}}} +{"type":"assistant/chunk","seq":45,"time":1783421411513,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":46,"time":1783421411513,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"模型"}}} +{"type":"assistant/chunk","seq":47,"time":1783421411513,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"驱动"}}} +{"type":"assistant/chunk","seq":48,"time":1783421411513,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"。"}}} +{"type":"assistant/chunk","seq":49,"time":1783421411513,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"有什么"}}} +{"type":"assistant/chunk","seq":50,"time":1783421411513,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"可以"}}} +{"type":"assistant/chunk","seq":51,"time":1783421411540,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"帮"}}} +{"type":"assistant/chunk","seq":52,"time":1783421411541,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"你的"}}} +{"type":"assistant/chunk","seq":53,"time":1783421411541,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"吗"}}} +{"type":"assistant/chunk","seq":54,"time":1783421411569,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"?"}}} +{"type":"assistant/chunk","seq":55,"time":1783421411570,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user is greeting me in Chinese. Let me respond in Chinese as well."}}}} +{"type":"assistant/chunk","seq":56,"time":1783421411570,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"你好!我是基于 DeepSeek Harness SDK 的 AI 助手,由 deepseek-v4-flash 模型驱动。有什么可以帮你的吗?"}}}} +{"type":"assistant/chunk","seq":57,"time":1783421411570,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1133,"outputTokens":50,"cacheReadTokens":0,"reasoningTokens":16}}}} +{"type":"assistant/chunk","seq":58,"time":1783421411570,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":59,"time":1783421411572,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user is greeting me in Chinese. Let me respond in Chinese as well."},{"type":"text","text":"你好!我是基于 DeepSeek Harness SDK 的 AI 助手,由 deepseek-v4-flash 模型驱动。有什么可以帮你的吗?"}],"usage":{"inputTokens":1133,"outputTokens":50,"cacheReadTokens":0,"reasoningTokens":16}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58],"surfaceOp":"append"} +{"type":"step/end","seq":60,"time":1783421411572,"data":{"turn":1,"step":1}} +{"type":"turn/end","seq":61,"time":1783421411573,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":62,"time":1783421423227,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":63,"time":1783421423227,"data":{"content":[{"type":"text","text":"帮我创建一个 a.md"}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":64,"time":1783421423228,"data":{"turn":2,"step":1}} +{"type":"assistant/chunk","seq":65,"time":1783421423837,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":66,"time":1783421423837,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":67,"time":1783421423966,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"想"}}} +{"type":"assistant/chunk","seq":68,"time":1783421423990,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"创建一个"}}} +{"type":"assistant/chunk","seq":69,"time":1783421423990,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":70,"time":1783421424016,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":71,"time":1783421424017,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":72,"time":1783421424017,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":73,"time":1783421424017,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"我的"}}} +{"type":"assistant/chunk","seq":74,"time":1783421424044,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"工作"}}} +{"type":"assistant/chunk","seq":75,"time":1783421424045,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":76,"time":1783421424045,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"是"}}} +{"type":"assistant/chunk","seq":77,"time":1783421424045,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" /"}}} +{"type":"assistant/chunk","seq":78,"time":1783421424073,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Users"}}} +{"type":"assistant/chunk","seq":79,"time":1783421424074,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"/"}}} +{"type":"assistant/chunk","seq":80,"time":1783421424074,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ww"}}} +{"type":"assistant/chunk","seq":81,"time":1783421424074,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"l"}}} +{"type":"assistant/chunk","seq":82,"time":1783421424101,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"/"}}} +{"type":"assistant/chunk","seq":83,"time":1783421424102,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"works"}}} +{"type":"assistant/chunk","seq":84,"time":1783421424102,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"pace"}}} +{"type":"assistant/chunk","seq":85,"time":1783421424102,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"/de"}}} +{"type":"assistant/chunk","seq":86,"time":1783421424102,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ep"}}} +{"type":"assistant/chunk","seq":87,"time":1783421424102,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"seek"}}} +{"type":"assistant/chunk","seq":88,"time":1783421424129,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-h"}}} +{"type":"assistant/chunk","seq":89,"time":1783421424129,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ar"}}} +{"type":"assistant/chunk","seq":90,"time":1783421424129,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ness"}}} +{"type":"assistant/chunk","seq":91,"time":1783421424130,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":92,"time":1783421424130,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"我需要"}}} +{"type":"assistant/chunk","seq":93,"time":1783421424130,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"使用"}}} +{"type":"assistant/chunk","seq":94,"time":1783421424157,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" bash"}}} +{"type":"assistant/chunk","seq":95,"time":1783421424158,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":96,"time":1783421424158,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"工具"}}} +{"type":"assistant/chunk","seq":97,"time":1783421424186,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"来"}}} +{"type":"assistant/chunk","seq":98,"time":1783421424186,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":99,"time":1783421424186,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"这个"}}} +{"type":"assistant/chunk","seq":100,"time":1783421424186,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":101,"time":1783421424186,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":102,"time":1783421424187,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"不过"}}} +{"type":"assistant/chunk","seq":103,"time":1783421424214,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"我需要"}}} +{"type":"assistant/chunk","seq":104,"time":1783421424248,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"确认"}}} +{"type":"assistant/chunk","seq":105,"time":1783421424271,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"一下"}}} +{"type":"assistant/chunk","seq":106,"time":1783421424271,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sand"}}} +{"type":"assistant/chunk","seq":107,"time":1783421424300,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"box"}}} +{"type":"assistant/chunk","seq":108,"time":1783421424300,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":109,"time":1783421424300,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"权限"}}} +{"type":"assistant/chunk","seq":110,"time":1783421424300,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"的情况"}}} +{"type":"assistant/chunk","seq":111,"time":1783421424327,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":112,"time":1783421424355,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"先"}}} +{"type":"assistant/chunk","seq":113,"time":1783421424355,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"试试"}}} +{"type":"assistant/chunk","seq":114,"time":1783421424383,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"看"}}} +{"type":"assistant/chunk","seq":115,"time":1783421424383,"data":{"turn":2,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":116,"time":1783421424467,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":117,"time":1783421424468,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":118,"time":1783421424496,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":119,"time":1783421424496,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":120,"time":1783421424496,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":121,"time":1783421424496,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":122,"time":1783421424496,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":123,"time":1783421424526,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":124,"time":1783421424527,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"touch"}}} +{"type":"assistant/chunk","seq":125,"time":1783421424527,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":" /"}}} +{"type":"assistant/chunk","seq":126,"time":1783421424527,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"Users"}}} +{"type":"assistant/chunk","seq":127,"time":1783421424555,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":128,"time":1783421424555,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"ww"}}} +{"type":"assistant/chunk","seq":129,"time":1783421424555,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"l"}}} +{"type":"assistant/chunk","seq":130,"time":1783421424555,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":131,"time":1783421424555,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"works"}}} +{"type":"assistant/chunk","seq":132,"time":1783421424555,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"pace"}}} +{"type":"assistant/chunk","seq":133,"time":1783421424582,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"/de"}}} +{"type":"assistant/chunk","seq":134,"time":1783421424582,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"ep"}}} +{"type":"assistant/chunk","seq":135,"time":1783421424582,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"seek"}}} +{"type":"assistant/chunk","seq":136,"time":1783421424582,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"-h"}}} +{"type":"assistant/chunk","seq":137,"time":1783421424582,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"ar"}}} +{"type":"assistant/chunk","seq":138,"time":1783421424582,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"ness"}}} +{"type":"assistant/chunk","seq":139,"time":1783421424610,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"/a"}}} +{"type":"assistant/chunk","seq":140,"time":1783421424610,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":141,"time":1783421424610,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":142,"time":1783421424637,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":143,"time":1783421424638,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":144,"time":1783421424638,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":145,"time":1783421424669,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":146,"time":1783421424669,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":147,"time":1783421424669,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":148,"time":1783421424669,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":149,"time":1783421424694,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":" a"}}} +{"type":"assistant/chunk","seq":150,"time":1783421424694,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":151,"time":1783421424694,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":152,"time":1783421424694,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":153,"time":1783421424694,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":154,"time":1783421424721,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":155,"time":1783421424780,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"用户想创建一个文件 a.md。我的工作目录是 /Users/wwl/workspace/deepseek-harness。我需要使用 bash 工具来创建这个文件。不过我需要确认一下 sandbox 权限的情况。先试试看。"}}}} +{"type":"assistant/chunk","seq":156,"time":1783421424780,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/a.md\", \"description\": \"创建 a.md 文件\"}"}}}} +{"type":"assistant/chunk","seq":157,"time":1783421424780,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":150,"outputTokens":129,"cacheReadTokens":1024,"reasoningTokens":50}}}} +{"type":"assistant/chunk","seq":158,"time":1783421424780,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":159,"time":1783421424780,"data":{"turn":2,"step":1,"content":[{"type":"reasoning","text":"用户想创建一个文件 a.md。我的工作目录是 /Users/wwl/workspace/deepseek-harness。我需要使用 bash 工具来创建这个文件。不过我需要确认一下 sandbox 权限的情况。先试试看。"},{"type":"tool-call","id":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/a.md\", \"description\": \"创建 a.md 文件\"}"}],"usage":{"inputTokens":150,"outputTokens":129,"cacheReadTokens":1024,"reasoningTokens":50}},"sourceEventSeqs":[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],"surfaceOp":"append"} +{"type":"tool/call","seq":160,"time":1783421424780,"data":{"turn":2,"step":1,"callId":"call_00_RcKdFJb419Z4TsbFjsiD5503","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/a.md\", \"description\": \"创建 a.md 文件\"}"}} +{"type":"tool/result","seq":161,"time":1783421424968,"data":{"turn":2,"step":1,"callId":"call_00_RcKdFJb419Z4TsbFjsiD5503","content":[{"type":"text","text":"[stderr]\ntouch: /Users/wwl/workspace/deepseek-harness/a.md: Operation not permitted\n[sandbox: file access denied under read-only mode]\n[exit code: 1]"}],"isError":false},"sourceEventSeqs":[160],"surfaceOp":"append"} +{"type":"step/end","seq":162,"time":1783421424969,"data":{"turn":2,"step":1}} +{"type":"step/start","seq":163,"time":1783421424969,"data":{"turn":2,"step":2}} +{"type":"assistant/chunk","seq":164,"time":1783421425508,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":165,"time":1783421425508,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":166,"time":1783421425639,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":167,"time":1783421425666,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"被"}}} +{"type":"assistant/chunk","seq":168,"time":1783421425666,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"沙"}}} +{"type":"assistant/chunk","seq":169,"time":1783421425666,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"盒"}}} +{"type":"assistant/chunk","seq":170,"time":1783421425692,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"阻止"}}} +{"type":"assistant/chunk","seq":171,"time":1783421425693,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"了"}}} +{"type":"assistant/chunk","seq":172,"time":1783421425693,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":173,"time":1783421425694,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"因为"}}} +{"type":"assistant/chunk","seq":174,"time":1783421425720,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"当前"}}} +{"type":"assistant/chunk","seq":175,"time":1783421425720,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"是"}}} +{"type":"assistant/chunk","seq":176,"time":1783421425749,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"只"}}} +{"type":"assistant/chunk","seq":177,"time":1783421425749,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"读"}}} +{"type":"assistant/chunk","seq":178,"time":1783421425749,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"模式"}}} +{"type":"assistant/chunk","seq":179,"time":1783421425749,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":180,"time":1783421425749,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"我需要"}}} +{"type":"assistant/chunk","seq":181,"time":1783421425749,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"申请"}}} +{"type":"assistant/chunk","seq":182,"time":1783421425776,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"写"}}} +{"type":"assistant/chunk","seq":183,"time":1783421425804,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"权限"}}} +{"type":"assistant/chunk","seq":184,"time":1783421425804,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":185,"time":1783421425832,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"根据"}}} +{"type":"assistant/chunk","seq":186,"time":1783421425860,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"规则"}}} +{"type":"assistant/chunk","seq":187,"time":1783421425860,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":188,"time":1783421425860,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"我需要"}}} +{"type":"assistant/chunk","seq":189,"time":1783421425860,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"使用"}}} +{"type":"assistant/chunk","seq":190,"time":1783421425887,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" sand"}}} +{"type":"assistant/chunk","seq":191,"time":1783421425915,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"box"}}} +{"type":"assistant/chunk","seq":192,"time":1783421425916,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"_per"}}} +{"type":"assistant/chunk","seq":193,"time":1783421425916,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"missions"}}} +{"type":"assistant/chunk","seq":194,"time":1783421425916,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":195,"time":1783421425916,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"参数"}}} +{"type":"assistant/chunk","seq":196,"time":1783421425942,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"并"}}} +{"type":"assistant/chunk","seq":197,"time":1783421425942,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"提供一个"}}} +{"type":"assistant/chunk","seq":198,"time":1783421425970,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"理由"}}} +{"type":"assistant/chunk","seq":199,"time":1783421425999,"data":{"turn":2,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":200,"time":1783421425999,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":201,"time":1783421425999,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"当前"}}} +{"type":"assistant/chunk","seq":202,"time":1783421426026,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"环境"}}} +{"type":"assistant/chunk","seq":203,"time":1783421426072,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"为"}}} +{"type":"assistant/chunk","seq":204,"time":1783421426082,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"只"}}} +{"type":"assistant/chunk","seq":205,"time":1783421426082,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"读"}}} +{"type":"assistant/chunk","seq":206,"time":1783421426082,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"模式"}}} +{"type":"assistant/chunk","seq":207,"time":1783421426082,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":208,"time":1783421426082,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"无法"}}} +{"type":"assistant/chunk","seq":209,"time":1783421426082,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"直接"}}} +{"type":"assistant/chunk","seq":210,"time":1783421426109,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"写入"}}} +{"type":"assistant/chunk","seq":211,"time":1783421426140,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"文件"}}} +{"type":"assistant/chunk","seq":212,"time":1783421426141,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"。"}}} +{"type":"assistant/chunk","seq":213,"time":1783421426141,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"我需要"}}} +{"type":"assistant/chunk","seq":214,"time":1783421426141,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"申请"}}} +{"type":"assistant/chunk","seq":215,"time":1783421426167,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"一次"}}} +{"type":"assistant/chunk","seq":216,"time":1783421426168,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"写"}}} +{"type":"assistant/chunk","seq":217,"time":1783421426195,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"权限"}}} +{"type":"assistant/chunk","seq":218,"time":1783421426195,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"来"}}} +{"type":"assistant/chunk","seq":219,"time":1783421426243,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":220,"time":1783421426244,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"这个"}}} +{"type":"assistant/chunk","seq":221,"time":1783421426244,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"文件"}}} +{"type":"assistant/chunk","seq":222,"time":1783421426244,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":1,"text":"。"}}} +{"type":"assistant/chunk","seq":223,"time":1783421426280,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":224,"time":1783421426280,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":225,"time":1783421426306,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":226,"time":1783421426307,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":227,"time":1783421426307,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":228,"time":1783421426335,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":229,"time":1783421426335,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":230,"time":1783421426335,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":231,"time":1783421426335,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"touch"}}} +{"type":"assistant/chunk","seq":232,"time":1783421426361,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":" /"}}} +{"type":"assistant/chunk","seq":233,"time":1783421426362,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"Users"}}} +{"type":"assistant/chunk","seq":234,"time":1783421426362,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":235,"time":1783421426362,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"ww"}}} +{"type":"assistant/chunk","seq":236,"time":1783421426362,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"l"}}} +{"type":"assistant/chunk","seq":237,"time":1783421426362,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":238,"time":1783421426389,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"works"}}} +{"type":"assistant/chunk","seq":239,"time":1783421426389,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"pace"}}} +{"type":"assistant/chunk","seq":240,"time":1783421426389,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"/de"}}} +{"type":"assistant/chunk","seq":241,"time":1783421426389,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"ep"}}} +{"type":"assistant/chunk","seq":242,"time":1783421426389,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"seek"}}} +{"type":"assistant/chunk","seq":243,"time":1783421426390,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"-h"}}} +{"type":"assistant/chunk","seq":244,"time":1783421426417,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"ar"}}} +{"type":"assistant/chunk","seq":245,"time":1783421426418,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"ness"}}} +{"type":"assistant/chunk","seq":246,"time":1783421426418,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"/a"}}} +{"type":"assistant/chunk","seq":247,"time":1783421426418,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":248,"time":1783421426418,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":249,"time":1783421426472,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":250,"time":1783421426473,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":251,"time":1783421426473,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":252,"time":1783421426473,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":253,"time":1783421426473,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":254,"time":1783421426500,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":255,"time":1783421426501,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":256,"time":1783421426501,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":" a"}}} +{"type":"assistant/chunk","seq":257,"time":1783421426501,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":258,"time":1783421426501,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":259,"time":1783421426501,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":260,"time":1783421426528,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":261,"time":1783421426555,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":262,"time":1783421426556,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":263,"time":1783421426556,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"sand"}}} +{"type":"assistant/chunk","seq":264,"time":1783421426556,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"box"}}} +{"type":"assistant/chunk","seq":265,"time":1783421426556,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"_per"}}} +{"type":"assistant/chunk","seq":266,"time":1783421426584,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"missions"}}} +{"type":"assistant/chunk","seq":267,"time":1783421426584,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":268,"time":1783421426584,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":269,"time":1783421426584,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":270,"time":1783421426612,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"works"}}} +{"type":"assistant/chunk","seq":271,"time":1783421426612,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"pace"}}} +{"type":"assistant/chunk","seq":272,"time":1783421426612,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"-w"}}} +{"type":"assistant/chunk","seq":273,"time":1783421426612,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"rite"}}} +{"type":"assistant/chunk","seq":274,"time":1783421426612,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":275,"time":1783421426667,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":276,"time":1783421426668,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":277,"time":1783421426668,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"just"}}} +{"type":"assistant/chunk","seq":278,"time":1783421426668,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"ification"}}} +{"type":"assistant/chunk","seq":279,"time":1783421426668,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":280,"time":1783421426668,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":281,"time":1783421426697,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":282,"time":1783421426698,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"用户"}}} +{"type":"assistant/chunk","seq":283,"time":1783421426698,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"要求"}}} +{"type":"assistant/chunk","seq":284,"time":1783421426698,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":285,"time":1783421426734,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":" a"}}} +{"type":"assistant/chunk","seq":286,"time":1783421426735,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":287,"time":1783421426735,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":288,"time":1783421426735,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":289,"time":1783421426735,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":","}}} +{"type":"assistant/chunk","seq":290,"time":1783421426735,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"需要"}}} +{"type":"assistant/chunk","seq":291,"time":1783421426753,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"写"}}} +{"type":"assistant/chunk","seq":292,"time":1783421426782,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"权限"}}} +{"type":"assistant/chunk","seq":293,"time":1783421426782,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"来"}}} +{"type":"assistant/chunk","seq":294,"time":1783421426782,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"在工作"}}} +{"type":"assistant/chunk","seq":295,"time":1783421426808,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"目录"}}} +{"type":"assistant/chunk","seq":296,"time":1783421426809,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"中"}}} +{"type":"assistant/chunk","seq":297,"time":1783421426809,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":298,"time":1783421426836,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"该"}}} +{"type":"assistant/chunk","seq":299,"time":1783421426836,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":300,"time":1783421426836,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"。"}}} +{"type":"assistant/chunk","seq":301,"time":1783421426836,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":302,"time":1783421426867,"data":{"turn":2,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":303,"time":1783421426894,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"文件创建被沙盒阻止了,因为当前是只读模式。我需要申请写权限。根据规则,我需要使用 sandbox_permissions 参数并提供一个理由。"}}}} +{"type":"assistant/chunk","seq":304,"time":1783421426894,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"当前环境为只读模式,无法直接写入文件。我需要申请一次写权限来创建这个文件。"}}}} +{"type":"assistant/chunk","seq":305,"time":1783421426894,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/a.md\", \"description\": \"创建 a.md 文件\", \"sandbox_permissions\": \"workspace-write\", \"justification\": \"用户要求创建 a.md 文件,需要写权限来在工作目录中创建该文件。\"}"}}}} +{"type":"assistant/chunk","seq":306,"time":1783421426894,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":80,"outputTokens":191,"cacheReadTokens":1280,"reasoningTokens":35}}}} +{"type":"assistant/chunk","seq":307,"time":1783421426894,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":308,"time":1783421426895,"data":{"turn":2,"step":2,"content":[{"type":"reasoning","text":"文件创建被沙盒阻止了,因为当前是只读模式。我需要申请写权限。根据规则,我需要使用 sandbox_permissions 参数并提供一个理由。"},{"type":"text","text":"当前环境为只读模式,无法直接写入文件。我需要申请一次写权限来创建这个文件。"},{"type":"tool-call","id":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/a.md\", \"description\": \"创建 a.md 文件\", \"sandbox_permissions\": \"workspace-write\", \"justification\": \"用户要求创建 a.md 文件,需要写权限来在工作目录中创建该文件。\"}"}],"usage":{"inputTokens":80,"outputTokens":191,"cacheReadTokens":1280,"reasoningTokens":35}},"sourceEventSeqs":[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,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],"surfaceOp":"append"} +{"type":"tool/call","seq":309,"time":1783421426895,"data":{"turn":2,"step":2,"callId":"call_00_Bm860a1oOrGFTnCY3xlU4459","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/a.md\", \"description\": \"创建 a.md 文件\", \"sandbox_permissions\": \"workspace-write\", \"justification\": \"用户要求创建 a.md 文件,需要写权限来在工作目录中创建该文件。\"}"}} +{"type":"approval/asked","seq":310,"time":1783421426895,"data":{"id":"fcb14c8f-9080-4260-b870-852432d8a46c","toolName":"bash","callId":"call_00_Bm860a1oOrGFTnCY3xlU4459","reason":"escalate sandbox to workspace-write: 用户要求创建 a.md 文件,需要写权限来在工作目录中创建该文件。"}} +{"type":"approval/decided","seq":311,"time":1783421428529,"data":{"id":"fcb14c8f-9080-4260-b870-852432d8a46c","outcome":"allowed-once"}} +{"type":"tool/result","seq":312,"time":1783421428617,"data":{"turn":2,"step":2,"callId":"call_00_Bm860a1oOrGFTnCY3xlU4459","content":[{"type":"text","text":"(no output)"}],"isError":false},"sourceEventSeqs":[309],"surfaceOp":"append"} +{"type":"step/end","seq":313,"time":1783421428617,"data":{"turn":2,"step":2}} +{"type":"step/start","seq":314,"time":1783421428617,"data":{"turn":2,"step":3}} +{"type":"assistant/chunk","seq":315,"time":1783421429001,"data":{"turn":2,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":316,"time":1783421429001,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":317,"time":1783421429121,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":318,"time":1783421429149,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"成功了"}}} +{"type":"assistant/chunk","seq":319,"time":1783421429149,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":320,"time":1783421429149,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"没有"}}} +{"type":"assistant/chunk","seq":321,"time":1783421429149,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"报"}}} +{"type":"assistant/chunk","seq":322,"time":1783421429178,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"错"}}} +{"type":"assistant/chunk","seq":323,"time":1783421429178,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":324,"time":1783421429178,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"让我"}}} +{"type":"assistant/chunk","seq":325,"time":1783421429178,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"验证"}}} +{"type":"assistant/chunk","seq":326,"time":1783421429178,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"一下"}}} +{"type":"assistant/chunk","seq":327,"time":1783421429205,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":328,"time":1783421429205,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"是否存在"}}} +{"type":"assistant/chunk","seq":329,"time":1783421429205,"data":{"turn":2,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":330,"time":1783421429205,"data":{"turn":2,"step":3,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":331,"time":1783421429205,"data":{"turn":2,"step":3,"chunk":{"type":"text-delta","index":1,"text":"文件"}}} +{"type":"assistant/chunk","seq":332,"time":1783421429205,"data":{"turn":2,"step":3,"chunk":{"type":"text-delta","index":1,"text":"已"}}} +{"type":"assistant/chunk","seq":333,"time":1783421429233,"data":{"turn":2,"step":3,"chunk":{"type":"text-delta","index":1,"text":"成功"}}} +{"type":"assistant/chunk","seq":334,"time":1783421429233,"data":{"turn":2,"step":3,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":335,"time":1783421429233,"data":{"turn":2,"step":3,"chunk":{"type":"text-delta","index":1,"text":"!"}}} +{"type":"assistant/chunk","seq":336,"time":1783421429233,"data":{"turn":2,"step":3,"chunk":{"type":"text-delta","index":1,"text":"验证"}}} +{"type":"assistant/chunk","seq":337,"time":1783421429261,"data":{"turn":2,"step":3,"chunk":{"type":"text-delta","index":1,"text":"一下"}}} +{"type":"assistant/chunk","seq":338,"time":1783421429261,"data":{"turn":2,"step":3,"chunk":{"type":"text-delta","index":1,"text":":"}}} +{"type":"assistant/chunk","seq":339,"time":1783421429317,"data":{"turn":2,"step":3,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":340,"time":1783421429317,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":341,"time":1783421429345,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":342,"time":1783421429345,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":343,"time":1783421429345,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":344,"time":1783421429345,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":345,"time":1783421429345,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":346,"time":1783421429373,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":347,"time":1783421429373,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"验证"}}} +{"type":"assistant/chunk","seq":348,"time":1783421429373,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":" a"}}} +{"type":"assistant/chunk","seq":349,"time":1783421429373,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":350,"time":1783421429400,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":351,"time":1783421429401,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":352,"time":1783421429401,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"已"}}} +{"type":"assistant/chunk","seq":353,"time":1783421429429,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":354,"time":1783421429429,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":355,"time":1783421429457,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":356,"time":1783421429457,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":357,"time":1783421429457,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":358,"time":1783421429457,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":359,"time":1783421429488,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":360,"time":1783421429488,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":361,"time":1783421429488,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"ls"}}} +{"type":"assistant/chunk","seq":362,"time":1783421429488,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":" -"}}} +{"type":"assistant/chunk","seq":363,"time":1783421429512,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"la"}}} +{"type":"assistant/chunk","seq":364,"time":1783421429512,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":" /"}}} +{"type":"assistant/chunk","seq":365,"time":1783421429512,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"Users"}}} +{"type":"assistant/chunk","seq":366,"time":1783421429512,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":367,"time":1783421429512,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"ww"}}} +{"type":"assistant/chunk","seq":368,"time":1783421429540,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"l"}}} +{"type":"assistant/chunk","seq":369,"time":1783421429541,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":370,"time":1783421429541,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"works"}}} +{"type":"assistant/chunk","seq":371,"time":1783421429541,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"pace"}}} +{"type":"assistant/chunk","seq":372,"time":1783421429541,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"/de"}}} +{"type":"assistant/chunk","seq":373,"time":1783421429568,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"ep"}}} +{"type":"assistant/chunk","seq":374,"time":1783421429569,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"seek"}}} +{"type":"assistant/chunk","seq":375,"time":1783421429569,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"-h"}}} +{"type":"assistant/chunk","seq":376,"time":1783421429569,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"ar"}}} +{"type":"assistant/chunk","seq":377,"time":1783421429569,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"ness"}}} +{"type":"assistant/chunk","seq":378,"time":1783421429569,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"/a"}}} +{"type":"assistant/chunk","seq":379,"time":1783421429596,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":380,"time":1783421429596,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":381,"time":1783421429624,"data":{"turn":2,"step":3,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":382,"time":1783421429654,"data":{"turn":2,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"文件创建成功了,没有报错。让我验证一下文件是否存在。"}}}} +{"type":"assistant/chunk","seq":383,"time":1783421429654,"data":{"turn":2,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"文件已成功创建!验证一下:"}}}} +{"type":"assistant/chunk","seq":384,"time":1783421429654,"data":{"turn":2,"step":3,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","arguments":"{\"description\": \"验证 a.md 文件已创建\", \"command\": \"ls -la /Users/wwl/workspace/deepseek-harness/a.md\"}"}}}} +{"type":"assistant/chunk","seq":385,"time":1783421429654,"data":{"turn":2,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":29,"outputTokens":104,"cacheReadTokens":1536,"reasoningTokens":14}}}} +{"type":"assistant/chunk","seq":386,"time":1783421429654,"data":{"turn":2,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":387,"time":1783421429655,"data":{"turn":2,"step":3,"content":[{"type":"reasoning","text":"文件创建成功了,没有报错。让我验证一下文件是否存在。"},{"type":"text","text":"文件已成功创建!验证一下:"},{"type":"tool-call","id":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","arguments":"{\"description\": \"验证 a.md 文件已创建\", \"command\": \"ls -la /Users/wwl/workspace/deepseek-harness/a.md\"}"}],"usage":{"inputTokens":29,"outputTokens":104,"cacheReadTokens":1536,"reasoningTokens":14}},"sourceEventSeqs":[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],"surfaceOp":"append"} +{"type":"tool/call","seq":388,"time":1783421429655,"data":{"turn":2,"step":3,"callId":"call_00_ufORNimSmbklA2zLkAEA3773","name":"bash","arguments":"{\"description\": \"验证 a.md 文件已创建\", \"command\": \"ls -la /Users/wwl/workspace/deepseek-harness/a.md\"}"}} +{"type":"tool/result","seq":389,"time":1783421429745,"data":{"turn":2,"step":3,"callId":"call_00_ufORNimSmbklA2zLkAEA3773","content":[{"type":"text","text":"-rw-r--r--@ 1 wwl staff 0 Jul 7 18:50 /Users/wwl/workspace/deepseek-harness/a.md\n"}],"isError":false},"sourceEventSeqs":[388],"surfaceOp":"append"} +{"type":"step/end","seq":390,"time":1783421429745,"data":{"turn":2,"step":3}} +{"type":"step/start","seq":391,"time":1783421429746,"data":{"turn":2,"step":4}} +{"type":"assistant/chunk","seq":392,"time":1783421430100,"data":{"turn":2,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":393,"time":1783421430100,"data":{"turn":2,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":394,"time":1783421430202,"data":{"turn":2,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"已"}}} +{"type":"assistant/chunk","seq":395,"time":1783421430241,"data":{"turn":2,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"成功"}}} +{"type":"assistant/chunk","seq":396,"time":1783421430241,"data":{"turn":2,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":397,"time":1783421430241,"data":{"turn":2,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":398,"time":1783421430241,"data":{"turn":2,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"大小为"}}} +{"type":"assistant/chunk","seq":399,"time":1783421430257,"data":{"turn":2,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"0"}}} +{"type":"assistant/chunk","seq":400,"time":1783421430258,"data":{"turn":2,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"字节"}}} +{"type":"assistant/chunk","seq":401,"time":1783421430286,"data":{"turn":2,"step":4,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":402,"time":1783421430286,"data":{"turn":2,"step":4,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":403,"time":1783421430286,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"文件"}}} +{"type":"assistant/chunk","seq":404,"time":1783421430286,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":405,"time":1783421430344,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"a"}}} +{"type":"assistant/chunk","seq":406,"time":1783421430345,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":407,"time":1783421430345,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":408,"time":1783421430345,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":409,"time":1783421430345,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"已"}}} +{"type":"assistant/chunk","seq":410,"time":1783421430345,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"成功"}}} +{"type":"assistant/chunk","seq":411,"time":1783421430345,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":412,"time":1783421430345,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"在"}}} +{"type":"assistant/chunk","seq":413,"time":1783421430374,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":" `/"}}} +{"type":"assistant/chunk","seq":414,"time":1783421430375,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"Users"}}} +{"type":"assistant/chunk","seq":415,"time":1783421430375,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":416,"time":1783421430375,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"ww"}}} +{"type":"assistant/chunk","seq":417,"time":1783421430375,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"l"}}} +{"type":"assistant/chunk","seq":418,"time":1783421430375,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":419,"time":1783421430400,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"works"}}} +{"type":"assistant/chunk","seq":420,"time":1783421430400,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"pace"}}} +{"type":"assistant/chunk","seq":421,"time":1783421430400,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"/de"}}} +{"type":"assistant/chunk","seq":422,"time":1783421430400,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"ep"}}} +{"type":"assistant/chunk","seq":423,"time":1783421430400,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"seek"}}} +{"type":"assistant/chunk","seq":424,"time":1783421430400,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"-h"}}} +{"type":"assistant/chunk","seq":425,"time":1783421430427,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"ar"}}} +{"type":"assistant/chunk","seq":426,"time":1783421430427,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"ness"}}} +{"type":"assistant/chunk","seq":427,"time":1783421430427,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":428,"time":1783421430427,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":429,"time":1783421430427,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":430,"time":1783421430427,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":431,"time":1783421430455,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"下"}}} +{"type":"assistant/chunk","seq":432,"time":1783421430455,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"。"}}} +{"type":"assistant/chunk","seq":433,"time":1783421430494,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"目前"}}} +{"type":"assistant/chunk","seq":434,"time":1783421430494,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"是"}}} +{"type":"assistant/chunk","seq":435,"time":1783421430510,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"空"}}} +{"type":"assistant/chunk","seq":436,"time":1783421430510,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"文件"}}} +{"type":"assistant/chunk","seq":437,"time":1783421430510,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":438,"time":1783421430510,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"需要"}}} +{"type":"assistant/chunk","seq":439,"time":1783421430538,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"我"}}} +{"type":"assistant/chunk","seq":440,"time":1783421430538,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"往"}}} +{"type":"assistant/chunk","seq":441,"time":1783421430538,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"里面"}}} +{"type":"assistant/chunk","seq":442,"time":1783421430538,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"写"}}} +{"type":"assistant/chunk","seq":443,"time":1783421430567,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"点"}}} +{"type":"assistant/chunk","seq":444,"time":1783421430567,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"内容"}}} +{"type":"assistant/chunk","seq":445,"time":1783421430567,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"吗"}}} +{"type":"assistant/chunk","seq":446,"time":1783421430567,"data":{"turn":2,"step":4,"chunk":{"type":"text-delta","index":1,"text":"?"}}} +{"type":"assistant/chunk","seq":447,"time":1783421430568,"data":{"turn":2,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"文件已成功创建,大小为0字节。"}}}} +{"type":"assistant/chunk","seq":448,"time":1783421430568,"data":{"turn":2,"step":4,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"文件 `a.md` 已成功创建在 `/Users/wwl/workspace/deepseek-harness/` 目录下。目前是空文件,需要我往里面写点内容吗?"}}}} +{"type":"assistant/chunk","seq":449,"time":1783421430568,"data":{"turn":2,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":55,"outputTokens":54,"cacheReadTokens":1664,"reasoningTokens":9}}}} +{"type":"assistant/chunk","seq":450,"time":1783421430568,"data":{"turn":2,"step":4,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":451,"time":1783421430568,"data":{"turn":2,"step":4,"content":[{"type":"reasoning","text":"文件已成功创建,大小为0字节。"},{"type":"text","text":"文件 `a.md` 已成功创建在 `/Users/wwl/workspace/deepseek-harness/` 目录下。目前是空文件,需要我往里面写点内容吗?"}],"usage":{"inputTokens":55,"outputTokens":54,"cacheReadTokens":1664,"reasoningTokens":9}},"sourceEventSeqs":[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,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450],"surfaceOp":"append"} +{"type":"step/end","seq":452,"time":1783421430568,"data":{"turn":2,"step":4}} +{"type":"turn/end","seq":453,"time":1783421430568,"data":{"turn":2,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":454,"time":1783421439538,"data":{"turn":3,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":455,"time":1783421439538,"data":{"content":[{"type":"text","text":"帮我创建一个b.md"}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":456,"time":1783421439538,"data":{"turn":3,"step":1}} +{"type":"assistant/chunk","seq":457,"time":1783421439995,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":458,"time":1783421439995,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":459,"time":1783421440126,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"想"}}} +{"type":"assistant/chunk","seq":460,"time":1783421440153,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"再"}}} +{"type":"assistant/chunk","seq":461,"time":1783421440154,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"创建一个"}}} +{"type":"assistant/chunk","seq":462,"time":1783421440154,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" b"}}} +{"type":"assistant/chunk","seq":463,"time":1783421440154,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":464,"time":1783421440181,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":465,"time":1783421440182,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":466,"time":1783421440182,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":467,"time":1783421440182,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"同样"}}} +{"type":"assistant/chunk","seq":468,"time":1783421440182,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"需要"}}} +{"type":"assistant/chunk","seq":469,"time":1783421440209,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"写"}}} +{"type":"assistant/chunk","seq":470,"time":1783421440238,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"权限"}}} +{"type":"assistant/chunk","seq":471,"time":1783421440239,"data":{"turn":3,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":472,"time":1783421440321,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":473,"time":1783421440321,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":474,"time":1783421440350,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":475,"time":1783421440351,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":476,"time":1783421440351,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":477,"time":1783421440382,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":478,"time":1783421440382,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":479,"time":1783421440382,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":480,"time":1783421440382,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"touch"}}} +{"type":"assistant/chunk","seq":481,"time":1783421440459,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":" /"}}} +{"type":"assistant/chunk","seq":482,"time":1783421440459,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"Users"}}} +{"type":"assistant/chunk","seq":483,"time":1783421440459,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":484,"time":1783421440459,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"ww"}}} +{"type":"assistant/chunk","seq":485,"time":1783421440459,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"l"}}} +{"type":"assistant/chunk","seq":486,"time":1783421440459,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":487,"time":1783421440474,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"works"}}} +{"type":"assistant/chunk","seq":488,"time":1783421440475,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"pace"}}} +{"type":"assistant/chunk","seq":489,"time":1783421440475,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"/de"}}} +{"type":"assistant/chunk","seq":490,"time":1783421440475,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"ep"}}} +{"type":"assistant/chunk","seq":491,"time":1783421440475,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"seek"}}} +{"type":"assistant/chunk","seq":492,"time":1783421440475,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"-h"}}} +{"type":"assistant/chunk","seq":493,"time":1783421440505,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"ar"}}} +{"type":"assistant/chunk","seq":494,"time":1783421440506,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"ness"}}} +{"type":"assistant/chunk","seq":495,"time":1783421440506,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"/b"}}} +{"type":"assistant/chunk","seq":496,"time":1783421440506,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":497,"time":1783421440506,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":498,"time":1783421440560,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":499,"time":1783421440561,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":500,"time":1783421440561,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":501,"time":1783421440561,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":502,"time":1783421440561,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":503,"time":1783421440588,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":504,"time":1783421440589,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":505,"time":1783421440589,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":" b"}}} +{"type":"assistant/chunk","seq":506,"time":1783421440589,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":507,"time":1783421440589,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":508,"time":1783421440589,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":509,"time":1783421440619,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":510,"time":1783421440648,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":511,"time":1783421440649,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":512,"time":1783421440649,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"sand"}}} +{"type":"assistant/chunk","seq":513,"time":1783421440649,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"box"}}} +{"type":"assistant/chunk","seq":514,"time":1783421440649,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"_per"}}} +{"type":"assistant/chunk","seq":515,"time":1783421440677,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"missions"}}} +{"type":"assistant/chunk","seq":516,"time":1783421440677,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":517,"time":1783421440678,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":518,"time":1783421440678,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":519,"time":1783421440704,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"works"}}} +{"type":"assistant/chunk","seq":520,"time":1783421440704,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"pace"}}} +{"type":"assistant/chunk","seq":521,"time":1783421440704,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"-w"}}} +{"type":"assistant/chunk","seq":522,"time":1783421440704,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"rite"}}} +{"type":"assistant/chunk","seq":523,"time":1783421440704,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":524,"time":1783421440761,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":525,"time":1783421440761,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":526,"time":1783421440761,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"just"}}} +{"type":"assistant/chunk","seq":527,"time":1783421440761,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"ification"}}} +{"type":"assistant/chunk","seq":528,"time":1783421440761,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":529,"time":1783421440761,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":530,"time":1783421440790,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":531,"time":1783421440790,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"用户"}}} +{"type":"assistant/chunk","seq":532,"time":1783421440790,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"要求"}}} +{"type":"assistant/chunk","seq":533,"time":1783421440790,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":534,"time":1783421440790,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":" b"}}} +{"type":"assistant/chunk","seq":535,"time":1783421440818,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":536,"time":1783421440818,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":537,"time":1783421440818,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":538,"time":1783421440818,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":","}}} +{"type":"assistant/chunk","seq":539,"time":1783421440818,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"需要"}}} +{"type":"assistant/chunk","seq":540,"time":1783421440818,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"写"}}} +{"type":"assistant/chunk","seq":541,"time":1783421440846,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"权限"}}} +{"type":"assistant/chunk","seq":542,"time":1783421440846,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"来完成"}}} +{"type":"assistant/chunk","seq":543,"time":1783421440875,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"。"}}} +{"type":"assistant/chunk","seq":544,"time":1783421440875,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":545,"time":1783421440875,"data":{"turn":3,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":546,"time":1783421440938,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"用户想再创建一个 b.md 文件。同样需要写权限。"}}}} +{"type":"assistant/chunk","seq":547,"time":1783421440938,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/b.md\", \"description\": \"创建 b.md 文件\", \"sandbox_permissions\": \"workspace-write\", \"justification\": \"用户要求创建 b.md 文件,需要写权限来完成。\"}"}}}} +{"type":"assistant/chunk","seq":548,"time":1783421440938,"data":{"turn":3,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":108,"outputTokens":143,"cacheReadTokens":1664,"reasoningTokens":14}}}} +{"type":"assistant/chunk","seq":549,"time":1783421440939,"data":{"turn":3,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":550,"time":1783421440939,"data":{"turn":3,"step":1,"content":[{"type":"reasoning","text":"用户想再创建一个 b.md 文件。同样需要写权限。"},{"type":"tool-call","id":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/b.md\", \"description\": \"创建 b.md 文件\", \"sandbox_permissions\": \"workspace-write\", \"justification\": \"用户要求创建 b.md 文件,需要写权限来完成。\"}"}],"usage":{"inputTokens":108,"outputTokens":143,"cacheReadTokens":1664,"reasoningTokens":14}},"sourceEventSeqs":[457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,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,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549],"surfaceOp":"append"} +{"type":"tool/call","seq":551,"time":1783421440939,"data":{"turn":3,"step":1,"callId":"call_00_xYndEeP8bz9o7WtBSExK3908","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/b.md\", \"description\": \"创建 b.md 文件\", \"sandbox_permissions\": \"workspace-write\", \"justification\": \"用户要求创建 b.md 文件,需要写权限来完成。\"}"}} +{"type":"approval/asked","seq":552,"time":1783421440939,"data":{"id":"c69fb956-43c4-4f62-8585-7758accd2116","toolName":"bash","callId":"call_00_xYndEeP8bz9o7WtBSExK3908","reason":"escalate sandbox to workspace-write: 用户要求创建 b.md 文件,需要写权限来完成。"}} +{"type":"approval/decided","seq":553,"time":1783421443589,"data":{"id":"c69fb956-43c4-4f62-8585-7758accd2116","outcome":"rejected"}} +{"type":"tool/result","seq":554,"time":1783421443589,"data":{"turn":3,"step":1,"callId":"call_00_xYndEeP8bz9o7WtBSExK3908","content":[{"type":"text","text":"Error: the user rejected escalating this command to \"workspace-write\""}],"isError":true},"sourceEventSeqs":[551],"surfaceOp":"append"} +{"type":"step/end","seq":555,"time":1783421443589,"data":{"turn":3,"step":1}} +{"type":"step/start","seq":556,"time":1783421443590,"data":{"turn":3,"step":2}} +{"type":"assistant/chunk","seq":557,"time":1783421444172,"data":{"turn":3,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":558,"time":1783421444172,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":559,"time":1783421444351,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"拒绝了"}}} +{"type":"assistant/chunk","seq":560,"time":1783421444380,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"写入"}}} +{"type":"assistant/chunk","seq":561,"time":1783421444380,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"权限"}}} +{"type":"assistant/chunk","seq":562,"time":1783421444407,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"的"}}} +{"type":"assistant/chunk","seq":563,"time":1783421444408,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"申请"}}} +{"type":"assistant/chunk","seq":564,"time":1783421444445,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":565,"time":1783421444445,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"这意味着"}}} +{"type":"assistant/chunk","seq":566,"time":1783421444445,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"我"}}} +{"type":"assistant/chunk","seq":567,"time":1783421444464,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"无法"}}} +{"type":"assistant/chunk","seq":568,"time":1783421444464,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"在当前"}}} +{"type":"assistant/chunk","seq":569,"time":1783421444464,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"沙"}}} +{"type":"assistant/chunk","seq":570,"time":1783421444496,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"盒"}}} +{"type":"assistant/chunk","seq":571,"time":1783421444496,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"模式下"}}} +{"type":"assistant/chunk","seq":572,"time":1783421444496,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":573,"time":1783421444496,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" b"}}} +{"type":"assistant/chunk","seq":574,"time":1783421444496,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":575,"time":1783421444496,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":576,"time":1783421444522,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":577,"time":1783421444522,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":578,"time":1783421444522,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"我需要"}}} +{"type":"assistant/chunk","seq":579,"time":1783421444522,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"如实"}}} +{"type":"assistant/chunk","seq":580,"time":1783421444549,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"告知"}}} +{"type":"assistant/chunk","seq":581,"time":1783421444549,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":582,"time":1783421444549,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"这个"}}} +{"type":"assistant/chunk","seq":583,"time":1783421444549,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"情况"}}} +{"type":"assistant/chunk","seq":584,"time":1783421444579,"data":{"turn":3,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":585,"time":1783421444580,"data":{"turn":3,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":586,"time":1783421444580,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"抱歉"}}} +{"type":"assistant/chunk","seq":587,"time":1783421444606,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":588,"time":1783421444606,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"写入"}}} +{"type":"assistant/chunk","seq":589,"time":1783421444634,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"权限"}}} +{"type":"assistant/chunk","seq":590,"time":1783421444634,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"的"}}} +{"type":"assistant/chunk","seq":591,"time":1783421444662,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"申请"}}} +{"type":"assistant/chunk","seq":592,"time":1783421444662,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"被"}}} +{"type":"assistant/chunk","seq":593,"time":1783421444662,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"拒绝了"}}} +{"type":"assistant/chunk","seq":594,"time":1783421444662,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":595,"time":1783421444663,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"我"}}} +{"type":"assistant/chunk","seq":596,"time":1783421444692,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"目前"}}} +{"type":"assistant/chunk","seq":597,"time":1783421444693,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"无法"}}} +{"type":"assistant/chunk","seq":598,"time":1783421444693,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"在"}}} +{"type":"assistant/chunk","seq":599,"time":1783421444719,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"只"}}} +{"type":"assistant/chunk","seq":600,"time":1783421444719,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"读"}}} +{"type":"assistant/chunk","seq":601,"time":1783421444719,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"沙"}}} +{"type":"assistant/chunk","seq":602,"time":1783421444747,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"盒"}}} +{"type":"assistant/chunk","seq":603,"time":1783421444747,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"环境下"}}} +{"type":"assistant/chunk","seq":604,"time":1783421444748,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":605,"time":1783421444776,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":606,"time":1783421444776,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"b"}}} +{"type":"assistant/chunk","seq":607,"time":1783421444776,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":608,"time":1783421444776,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":609,"time":1783421444776,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":610,"time":1783421444776,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"文件"}}} +{"type":"assistant/chunk","seq":611,"time":1783421444804,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"。\n\n"}}} +{"type":"assistant/chunk","seq":612,"time":1783421444805,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"你可以"}}} +{"type":"assistant/chunk","seq":613,"time":1783421444805,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"考虑"}}} +{"type":"assistant/chunk","seq":614,"time":1783421444832,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"以下"}}} +{"type":"assistant/chunk","seq":615,"time":1783421444832,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"替代"}}} +{"type":"assistant/chunk","seq":616,"time":1783421444860,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"方案"}}} +{"type":"assistant/chunk","seq":617,"time":1783421444888,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":":\n"}}} +{"type":"assistant/chunk","seq":618,"time":1783421444889,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"1"}}} +{"type":"assistant/chunk","seq":619,"time":1783421444889,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"."}}} +{"type":"assistant/chunk","seq":620,"time":1783421444889,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":" **"}}} +{"type":"assistant/chunk","seq":621,"time":1783421444889,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"自己在"}}} +{"type":"assistant/chunk","seq":622,"time":1783421444919,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"本地"}}} +{"type":"assistant/chunk","seq":623,"time":1783421444949,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":624,"time":1783421444949,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":625,"time":1783421444978,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":":"}}} +{"type":"assistant/chunk","seq":626,"time":1783421444979,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"在"}}} +{"type":"assistant/chunk","seq":627,"time":1783421445006,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"终端"}}} +{"type":"assistant/chunk","seq":628,"time":1783421445006,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"执行"}}} +{"type":"assistant/chunk","seq":629,"time":1783421445006,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":630,"time":1783421445034,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"touch"}}} +{"type":"assistant/chunk","seq":631,"time":1783421445034,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":" /"}}} +{"type":"assistant/chunk","seq":632,"time":1783421445061,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"Users"}}} +{"type":"assistant/chunk","seq":633,"time":1783421445061,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":634,"time":1783421445061,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ww"}}} +{"type":"assistant/chunk","seq":635,"time":1783421445089,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"l"}}} +{"type":"assistant/chunk","seq":636,"time":1783421445089,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":637,"time":1783421445089,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"works"}}} +{"type":"assistant/chunk","seq":638,"time":1783421445089,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"pace"}}} +{"type":"assistant/chunk","seq":639,"time":1783421445089,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"/de"}}} +{"type":"assistant/chunk","seq":640,"time":1783421445089,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ep"}}} +{"type":"assistant/chunk","seq":641,"time":1783421445117,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"seek"}}} +{"type":"assistant/chunk","seq":642,"time":1783421445117,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"-h"}}} +{"type":"assistant/chunk","seq":643,"time":1783421445117,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ar"}}} +{"type":"assistant/chunk","seq":644,"time":1783421445117,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ness"}}} +{"type":"assistant/chunk","seq":645,"time":1783421445117,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"/b"}}} +{"type":"assistant/chunk","seq":646,"time":1783421445118,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":647,"time":1783421445146,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`\n"}}} +{"type":"assistant/chunk","seq":648,"time":1783421445146,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"2"}}} +{"type":"assistant/chunk","seq":649,"time":1783421445146,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"."}}} +{"type":"assistant/chunk","seq":650,"time":1783421445146,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":" **"}}} +{"type":"assistant/chunk","seq":651,"time":1783421445146,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"通过"}}} +{"type":"assistant/chunk","seq":652,"time":1783421445173,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"其他"}}} +{"type":"assistant/chunk","seq":653,"time":1783421445173,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"有"}}} +{"type":"assistant/chunk","seq":654,"time":1783421445201,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"写"}}} +{"type":"assistant/chunk","seq":655,"time":1783421445229,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"权限"}}} +{"type":"assistant/chunk","seq":656,"time":1783421445229,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"的工具"}}} +{"type":"assistant/chunk","seq":657,"time":1783421445258,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"或"}}} +{"type":"assistant/chunk","seq":658,"time":1783421445286,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"方式"}}} +{"type":"assistant/chunk","seq":659,"time":1783421445286,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":660,"time":1783421445286,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"来完成"}}} +{"type":"assistant/chunk","seq":661,"time":1783421445319,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":662,"time":1783421445341,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"\n\n"}}} +{"type":"assistant/chunk","seq":663,"time":1783421445341,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"如果"}}} +{"type":"assistant/chunk","seq":664,"time":1783421445341,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"后续"}}} +{"type":"assistant/chunk","seq":665,"time":1783421445370,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"需要"}}} +{"type":"assistant/chunk","seq":666,"time":1783421445370,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"我"}}} +{"type":"assistant/chunk","seq":667,"time":1783421445398,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"处理"}}} +{"type":"assistant/chunk","seq":668,"time":1783421445426,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"其他"}}} +{"type":"assistant/chunk","seq":669,"time":1783421445455,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"不"}}} +{"type":"assistant/chunk","seq":670,"time":1783421445486,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"涉及"}}} +{"type":"assistant/chunk","seq":671,"time":1783421445487,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"文件"}}} +{"type":"assistant/chunk","seq":672,"time":1783421445487,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"写入"}}} +{"type":"assistant/chunk","seq":673,"time":1783421445515,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"的任务"}}} +{"type":"assistant/chunk","seq":674,"time":1783421445515,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":675,"time":1783421445515,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"随时"}}} +{"type":"assistant/chunk","seq":676,"time":1783421445515,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"可以"}}} +{"type":"assistant/chunk","seq":677,"time":1783421445545,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"告诉我"}}} +{"type":"assistant/chunk","seq":678,"time":1783421445545,"data":{"turn":3,"step":2,"chunk":{"type":"text-delta","index":1,"text":"!"}}} +{"type":"assistant/chunk","seq":679,"time":1783421445545,"data":{"turn":3,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"用户拒绝了写入权限的申请。这意味着我无法在当前沙盒模式下创建 b.md 文件。我需要如实告知用户这个情况。"}}}} +{"type":"assistant/chunk","seq":680,"time":1783421445545,"data":{"turn":3,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"抱歉,写入权限的申请被拒绝了,我目前无法在只读沙盒环境下创建 `b.md` 文件。\n\n你可以考虑以下替代方案:\n1. **自己在本地创建**:在终端执行 `touch /Users/wwl/workspace/deepseek-harness/b.md`\n2. **通过其他有写权限的工具或方式**来完成创建\n\n如果后续需要我处理其他不涉及文件写入的任务,随时可以告诉我!"}}}} +{"type":"assistant/chunk","seq":681,"time":1783421445545,"data":{"turn":3,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":150,"outputTokens":121,"cacheReadTokens":1792,"reasoningTokens":27}}}} +{"type":"assistant/chunk","seq":682,"time":1783421445545,"data":{"turn":3,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":683,"time":1783421445546,"data":{"turn":3,"step":2,"content":[{"type":"reasoning","text":"用户拒绝了写入权限的申请。这意味着我无法在当前沙盒模式下创建 b.md 文件。我需要如实告知用户这个情况。"},{"type":"text","text":"抱歉,写入权限的申请被拒绝了,我目前无法在只读沙盒环境下创建 `b.md` 文件。\n\n你可以考虑以下替代方案:\n1. **自己在本地创建**:在终端执行 `touch /Users/wwl/workspace/deepseek-harness/b.md`\n2. **通过其他有写权限的工具或方式**来完成创建\n\n如果后续需要我处理其他不涉及文件写入的任务,随时可以告诉我!"}],"usage":{"inputTokens":150,"outputTokens":121,"cacheReadTokens":1792,"reasoningTokens":27}},"sourceEventSeqs":[557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682],"surfaceOp":"append"} +{"type":"step/end","seq":684,"time":1783421445546,"data":{"turn":3,"step":2}} +{"type":"turn/end","seq":685,"time":1783421445546,"data":{"turn":3,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":686,"time":1783421455801,"data":{"turn":4,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"approval/policy","seq":687,"time":1783421455801,"data":{"policy":"never"}} +{"type":"user/message","seq":688,"time":1783421455801,"data":{"content":[{"type":"text","text":"帮我创建一个 c.md"}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"context/message","seq":689,"time":1783421455802,"data":{"content":[{"type":"text","text":"The approval policy changed from \"ask\" to \"never\" (changed by the user)."}],"source":{"kind":"plugin","plugin":"approval"}},"surfaceOp":"append"} +{"type":"step/start","seq":690,"time":1783421455802,"data":{"turn":4,"step":1}} +{"type":"request/header-delta","seq":691,"time":1783421455802,"data":{"system":{"keepStart":13,"keepEnd":0,"insert":["","Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation."]}}} +{"type":"assistant/chunk","seq":692,"time":1783421456825,"data":{"turn":4,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":693,"time":1783421456825,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":694,"time":1783421456957,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":695,"time":1783421456985,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":696,"time":1783421457014,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":697,"time":1783421457014,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" create"}}} +{"type":"assistant/chunk","seq":698,"time":1783421457014,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" c"}}} +{"type":"assistant/chunk","seq":699,"time":1783421457041,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":700,"time":1783421457042,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":701,"time":1783421457042,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} +{"type":"assistant/chunk","seq":702,"time":1783421457042,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" approval"}}} +{"type":"assistant/chunk","seq":703,"time":1783421457069,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" policy"}}} +{"type":"assistant/chunk","seq":704,"time":1783421457070,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" has"}}} +{"type":"assistant/chunk","seq":705,"time":1783421457070,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" changed"}}} +{"type":"assistant/chunk","seq":706,"time":1783421457070,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":707,"time":1783421457098,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":708,"time":1783421457099,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"never"}}} +{"type":"assistant/chunk","seq":709,"time":1783421457099,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":710,"time":1783421457099,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" which"}}} +{"type":"assistant/chunk","seq":711,"time":1783421457126,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" means"}}} +{"type":"assistant/chunk","seq":712,"time":1783421457126,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":713,"time":1783421457126,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" should"}}} +{"type":"assistant/chunk","seq":714,"time":1783421457156,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} +{"type":"assistant/chunk","seq":715,"time":1783421457157,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" request"}}} +{"type":"assistant/chunk","seq":716,"time":1783421457184,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" escalation"}}} +{"type":"assistant/chunk","seq":717,"time":1783421457184,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":718,"time":1783421457212,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Since"}}} +{"type":"assistant/chunk","seq":719,"time":1783421457240,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":720,"time":1783421457267,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" operations"}}} +{"type":"assistant/chunk","seq":721,"time":1783421457268,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" are"}}} +{"type":"assistant/chunk","seq":722,"time":1783421457296,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" denied"}}} +{"type":"assistant/chunk","seq":723,"time":1783421457297,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" in"}}} +{"type":"assistant/chunk","seq":724,"time":1783421457297,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" read"}}} +{"type":"assistant/chunk","seq":725,"time":1783421457322,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-only"}}} +{"type":"assistant/chunk","seq":726,"time":1783421457323,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" mode"}}} +{"type":"assistant/chunk","seq":727,"time":1783421457323,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":728,"time":1783421457350,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":729,"time":1783421457351,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" can"}}} +{"type":"assistant/chunk","seq":730,"time":1783421457378,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"'t"}}} +{"type":"assistant/chunk","seq":731,"time":1783421457379,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" request"}}} +{"type":"assistant/chunk","seq":732,"time":1783421457379,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" approval"}}} +{"type":"assistant/chunk","seq":733,"time":1783421457406,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":734,"time":1783421457437,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":735,"time":1783421457438,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" need"}}} +{"type":"assistant/chunk","seq":736,"time":1783421457438,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":737,"time":1783421457438,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" inform"}}} +{"type":"assistant/chunk","seq":738,"time":1783421457466,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":739,"time":1783421457467,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":740,"time":1783421457467,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":741,"time":1783421457467,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} +{"type":"assistant/chunk","seq":742,"time":1783421457493,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" cannot"}}} +{"type":"assistant/chunk","seq":743,"time":1783421457494,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" do"}}} +{"type":"assistant/chunk","seq":744,"time":1783421457521,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" this"}}} +{"type":"assistant/chunk","seq":745,"time":1783421457522,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" due"}}} +{"type":"assistant/chunk","seq":746,"time":1783421457522,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":747,"time":1783421457522,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sand"}}} +{"type":"assistant/chunk","seq":748,"time":1783421457550,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"box"}}} +{"type":"assistant/chunk","seq":749,"time":1783421457550,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" restrictions"}}} +{"type":"assistant/chunk","seq":750,"time":1783421457550,"data":{"turn":4,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":751,"time":1783421457551,"data":{"turn":4,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":752,"time":1783421457551,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"抱歉"}}} +{"type":"assistant/chunk","seq":753,"time":1783421457577,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":754,"time":1783421457577,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"当前"}}} +{"type":"assistant/chunk","seq":755,"time":1783421457606,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"环境"}}} +{"type":"assistant/chunk","seq":756,"time":1783421457606,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"仍"}}} +{"type":"assistant/chunk","seq":757,"time":1783421457606,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"处于"}}} +{"type":"assistant/chunk","seq":758,"time":1783421457634,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":759,"time":1783421457661,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"只"}}} +{"type":"assistant/chunk","seq":760,"time":1783421457662,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"读"}}} +{"type":"assistant/chunk","seq":761,"time":1783421457662,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"沙"}}} +{"type":"assistant/chunk","seq":762,"time":1783421457662,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"盒"}}} +{"type":"assistant/chunk","seq":763,"time":1783421457690,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"模式"}}} +{"type":"assistant/chunk","seq":764,"time":1783421457690,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":765,"time":1783421457690,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":766,"time":1783421457690,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"且"}}} +{"type":"assistant/chunk","seq":767,"time":1783421457718,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"写"}}} +{"type":"assistant/chunk","seq":768,"time":1783421457747,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"权限"}}} +{"type":"assistant/chunk","seq":769,"time":1783421457775,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"申请"}}} +{"type":"assistant/chunk","seq":770,"time":1783421457776,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"已被"}}} +{"type":"assistant/chunk","seq":771,"time":1783421457776,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"拒绝"}}} +{"type":"assistant/chunk","seq":772,"time":1783421457829,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":773,"time":1783421457829,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"我"}}} +{"type":"assistant/chunk","seq":774,"time":1783421457830,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"无法"}}} +{"type":"assistant/chunk","seq":775,"time":1783421457831,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":776,"time":1783421457831,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":777,"time":1783421457859,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"c"}}} +{"type":"assistant/chunk","seq":778,"time":1783421457859,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":779,"time":1783421457859,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":780,"time":1783421457860,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":781,"time":1783421457860,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"文件"}}} +{"type":"assistant/chunk","seq":782,"time":1783421457887,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"。\n\n"}}} +{"type":"assistant/chunk","seq":783,"time":1783421457888,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"建议"}}} +{"type":"assistant/chunk","seq":784,"time":1783421457888,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"你在"}}} +{"type":"assistant/chunk","seq":785,"time":1783421457915,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"本地"}}} +{"type":"assistant/chunk","seq":786,"time":1783421457915,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"手动"}}} +{"type":"assistant/chunk","seq":787,"time":1783421457944,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"执行"}}} +{"type":"assistant/chunk","seq":788,"time":1783421457944,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":":\n"}}} +{"type":"assistant/chunk","seq":789,"time":1783421457944,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"```"}}} +{"type":"assistant/chunk","seq":790,"time":1783421457944,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"bash"}}} +{"type":"assistant/chunk","seq":791,"time":1783421457944,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"\n"}}} +{"type":"assistant/chunk","seq":792,"time":1783421457944,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"touch"}}} +{"type":"assistant/chunk","seq":793,"time":1783421457972,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":" /"}}} +{"type":"assistant/chunk","seq":794,"time":1783421457972,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"Users"}}} +{"type":"assistant/chunk","seq":795,"time":1783421457973,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":796,"time":1783421458003,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ww"}}} +{"type":"assistant/chunk","seq":797,"time":1783421458003,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"l"}}} +{"type":"assistant/chunk","seq":798,"time":1783421458003,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":799,"time":1783421458003,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"works"}}} +{"type":"assistant/chunk","seq":800,"time":1783421458003,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"pace"}}} +{"type":"assistant/chunk","seq":801,"time":1783421458003,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"/de"}}} +{"type":"assistant/chunk","seq":802,"time":1783421458032,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ep"}}} +{"type":"assistant/chunk","seq":803,"time":1783421458032,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"seek"}}} +{"type":"assistant/chunk","seq":804,"time":1783421458032,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"-h"}}} +{"type":"assistant/chunk","seq":805,"time":1783421458032,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ar"}}} +{"type":"assistant/chunk","seq":806,"time":1783421458032,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ness"}}} +{"type":"assistant/chunk","seq":807,"time":1783421458032,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"/c"}}} +{"type":"assistant/chunk","seq":808,"time":1783421458059,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":809,"time":1783421458060,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"\n"}}} +{"type":"assistant/chunk","seq":810,"time":1783421458060,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"```\n\n"}}} +{"type":"assistant/chunk","seq":811,"time":1783421458060,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"有其他"}}} +{"type":"assistant/chunk","seq":812,"time":1783421458087,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"不需要"}}} +{"type":"assistant/chunk","seq":813,"time":1783421458115,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"写入"}}} +{"type":"assistant/chunk","seq":814,"time":1783421458115,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"文件"}}} +{"type":"assistant/chunk","seq":815,"time":1783421458143,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"的任务"}}} +{"type":"assistant/chunk","seq":816,"time":1783421458144,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"需要"}}} +{"type":"assistant/chunk","seq":817,"time":1783421458172,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"我"}}} +{"type":"assistant/chunk","seq":818,"time":1783421458172,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"帮忙"}}} +{"type":"assistant/chunk","seq":819,"time":1783421458172,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"吗"}}} +{"type":"assistant/chunk","seq":820,"time":1783421458202,"data":{"turn":4,"step":1,"chunk":{"type":"text-delta","index":1,"text":"?"}}} +{"type":"assistant/chunk","seq":821,"time":1783421458203,"data":{"turn":4,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants to create c.md. The approval policy has changed to \"never\" which means I should not request escalation. Since file operations are denied in read-only mode and I can't request approval, I need to inform the user that I cannot do this due to sandbox restrictions."}}}} +{"type":"assistant/chunk","seq":822,"time":1783421458203,"data":{"turn":4,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"抱歉,当前环境仍处于**只读沙盒模式**,且写权限申请已被拒绝,我无法创建 `c.md` 文件。\n\n建议你在本地手动执行:\n```bash\ntouch /Users/wwl/workspace/deepseek-harness/c.md\n```\n\n有其他不需要写入文件的任务需要我帮忙吗?"}}}} +{"type":"assistant/chunk","seq":823,"time":1783421458203,"data":{"turn":4,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2096,"outputTokens":128,"cacheReadTokens":0,"reasoningTokens":58}}}} +{"type":"assistant/chunk","seq":824,"time":1783421458203,"data":{"turn":4,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":825,"time":1783421458203,"data":{"turn":4,"step":1,"content":[{"type":"reasoning","text":"The user wants to create c.md. The approval policy has changed to \"never\" which means I should not request escalation. Since file operations are denied in read-only mode and I can't request approval, I need to inform the user that I cannot do this due to sandbox restrictions."},{"type":"text","text":"抱歉,当前环境仍处于**只读沙盒模式**,且写权限申请已被拒绝,我无法创建 `c.md` 文件。\n\n建议你在本地手动执行:\n```bash\ntouch /Users/wwl/workspace/deepseek-harness/c.md\n```\n\n有其他不需要写入文件的任务需要我帮忙吗?"}],"usage":{"inputTokens":2096,"outputTokens":128,"cacheReadTokens":0,"reasoningTokens":58}},"sourceEventSeqs":[692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824],"surfaceOp":"append"} +{"type":"step/end","seq":826,"time":1783421458203,"data":{"turn":4,"step":1}} +{"type":"turn/end","seq":827,"time":1783421458203,"data":{"turn":4,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":828,"time":1783421478599,"data":{"turn":5,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"bash/sandbox-mode","seq":829,"time":1783421478599,"data":{"mode":"workspace-write"}} +{"type":"user/message","seq":830,"time":1783421478599,"data":{"content":[{"type":"text","text":"帮我创建一个 d.md"}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"context/message","seq":831,"time":1783421478600,"data":{"content":[{"type":"text","text":"The bash sandbox mode changed from \"read-only\" to \"workspace-write\" (changed by the user)."}],"source":{"kind":"plugin","plugin":"tool-bash"}},"surfaceOp":"append"} +{"type":"step/start","seq":832,"time":1783421478600,"data":{"turn":5,"step":1}} +{"type":"request/header-delta","seq":833,"time":1783421478600,"data":{"system":{"keepStart":12,"keepEnd":2,"insert":["Bash commands run under the \"workspace-write\" file sandbox."]}}} +{"type":"assistant/chunk","seq":834,"time":1783421479489,"data":{"turn":5,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":835,"time":1783421479489,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"现在"}}} +{"type":"assistant/chunk","seq":836,"time":1783421479638,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"沙"}}} +{"type":"assistant/chunk","seq":837,"time":1783421479666,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"盒"}}} +{"type":"assistant/chunk","seq":838,"time":1783421479666,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"模式"}}} +{"type":"assistant/chunk","seq":839,"time":1783421479666,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"已经"}}} +{"type":"assistant/chunk","seq":840,"time":1783421479666,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"改为"}}} +{"type":"assistant/chunk","seq":841,"time":1783421479695,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" workspace"}}} +{"type":"assistant/chunk","seq":842,"time":1783421479695,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-w"}}} +{"type":"assistant/chunk","seq":843,"time":1783421479695,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"rite"}}} +{"type":"assistant/chunk","seq":844,"time":1783421479695,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":845,"time":1783421479722,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"我可以"}}} +{"type":"assistant/chunk","seq":846,"time":1783421479751,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"直接"}}} +{"type":"assistant/chunk","seq":847,"time":1783421479751,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":848,"time":1783421479751,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":849,"time":1783421479751,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"了"}}} +{"type":"assistant/chunk","seq":850,"time":1783421479751,"data":{"turn":5,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":851,"time":1783421479779,"data":{"turn":5,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":852,"time":1783421479779,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":"好的"}}} +{"type":"assistant/chunk","seq":853,"time":1783421479779,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":854,"time":1783421479779,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":"现在"}}} +{"type":"assistant/chunk","seq":855,"time":1783421479807,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":"可以在"}}} +{"type":"assistant/chunk","seq":856,"time":1783421479836,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":"工作"}}} +{"type":"assistant/chunk","seq":857,"time":1783421479836,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":858,"time":1783421479864,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":"下"}}} +{"type":"assistant/chunk","seq":859,"time":1783421479864,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":"直接"}}} +{"type":"assistant/chunk","seq":860,"time":1783421479864,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":861,"time":1783421479892,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":"了"}}} +{"type":"assistant/chunk","seq":862,"time":1783421479921,"data":{"turn":5,"step":1,"chunk":{"type":"text-delta","index":1,"text":":"}}} +{"type":"assistant/chunk","seq":863,"time":1783421479977,"data":{"turn":5,"step":1,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":864,"time":1783421479977,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":865,"time":1783421480009,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":866,"time":1783421480009,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":867,"time":1783421480009,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":868,"time":1783421480009,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":869,"time":1783421480009,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":870,"time":1783421480037,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":871,"time":1783421480037,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":872,"time":1783421480037,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":" d"}}} +{"type":"assistant/chunk","seq":873,"time":1783421480037,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":874,"time":1783421480037,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":875,"time":1783421480065,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":876,"time":1783421480065,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":877,"time":1783421480094,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":878,"time":1783421480094,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":879,"time":1783421480094,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":880,"time":1783421480094,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":881,"time":1783421480123,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":882,"time":1783421480123,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":883,"time":1783421480123,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"touch"}}} +{"type":"assistant/chunk","seq":884,"time":1783421480123,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":" /"}}} +{"type":"assistant/chunk","seq":885,"time":1783421480150,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"Users"}}} +{"type":"assistant/chunk","seq":886,"time":1783421480150,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":887,"time":1783421480150,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"ww"}}} +{"type":"assistant/chunk","seq":888,"time":1783421480150,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"l"}}} +{"type":"assistant/chunk","seq":889,"time":1783421480179,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":890,"time":1783421480179,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"works"}}} +{"type":"assistant/chunk","seq":891,"time":1783421480179,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"pace"}}} +{"type":"assistant/chunk","seq":892,"time":1783421480179,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"/de"}}} +{"type":"assistant/chunk","seq":893,"time":1783421480179,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"ep"}}} +{"type":"assistant/chunk","seq":894,"time":1783421480179,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"seek"}}} +{"type":"assistant/chunk","seq":895,"time":1783421480208,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"-h"}}} +{"type":"assistant/chunk","seq":896,"time":1783421480208,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"ar"}}} +{"type":"assistant/chunk","seq":897,"time":1783421480208,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"ness"}}} +{"type":"assistant/chunk","seq":898,"time":1783421480208,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"/d"}}} +{"type":"assistant/chunk","seq":899,"time":1783421480208,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":900,"time":1783421480234,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":901,"time":1783421480234,"data":{"turn":5,"step":1,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":902,"time":1783421480294,"data":{"turn":5,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"现在沙盒模式已经改为 workspace-write,我可以直接创建文件了。"}}}} +{"type":"assistant/chunk","seq":903,"time":1783421480294,"data":{"turn":5,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"好的,现在可以在工作目录下直接创建了:"}}}} +{"type":"assistant/chunk","seq":904,"time":1783421480294,"data":{"turn":5,"step":1,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","arguments":"{\"description\": \"创建 d.md 文件\", \"command\": \"touch /Users/wwl/workspace/deepseek-harness/d.md\"}"}}}} +{"type":"assistant/chunk","seq":905,"time":1783421480294,"data":{"turn":5,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2209,"outputTokens":105,"cacheReadTokens":0,"reasoningTokens":16}}}} +{"type":"assistant/chunk","seq":906,"time":1783421480294,"data":{"turn":5,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":907,"time":1783421480294,"data":{"turn":5,"step":1,"content":[{"type":"reasoning","text":"现在沙盒模式已经改为 workspace-write,我可以直接创建文件了。"},{"type":"text","text":"好的,现在可以在工作目录下直接创建了:"},{"type":"tool-call","id":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","arguments":"{\"description\": \"创建 d.md 文件\", \"command\": \"touch /Users/wwl/workspace/deepseek-harness/d.md\"}"}],"usage":{"inputTokens":2209,"outputTokens":105,"cacheReadTokens":0,"reasoningTokens":16}},"sourceEventSeqs":[834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906],"surfaceOp":"append"} +{"type":"tool/call","seq":908,"time":1783421480294,"data":{"turn":5,"step":1,"callId":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","name":"bash","arguments":"{\"description\": \"创建 d.md 文件\", \"command\": \"touch /Users/wwl/workspace/deepseek-harness/d.md\"}"}} +{"type":"tool/result","seq":909,"time":1783421480356,"data":{"turn":5,"step":1,"callId":"call_00_KE1pSG2tZ8MlgO1bAdFg3508","content":[{"type":"text","text":"(no output)"}],"isError":false},"sourceEventSeqs":[908],"surfaceOp":"append"} +{"type":"step/end","seq":910,"time":1783421480356,"data":{"turn":5,"step":1}} +{"type":"step/start","seq":911,"time":1783421480357,"data":{"turn":5,"step":2}} +{"type":"assistant/chunk","seq":912,"time":1783421480773,"data":{"turn":5,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":913,"time":1783421480773,"data":{"turn":5,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":914,"time":1783421480913,"data":{"turn":5,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"成功"}}} +{"type":"assistant/chunk","seq":915,"time":1783421480913,"data":{"turn":5,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":916,"time":1783421480913,"data":{"turn":5,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"验证"}}} +{"type":"assistant/chunk","seq":917,"time":1783421480913,"data":{"turn":5,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"一下"}}} +{"type":"assistant/chunk","seq":918,"time":1783421480913,"data":{"turn":5,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":919,"time":1783421480962,"data":{"turn":5,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":920,"time":1783421480962,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":921,"time":1783421480990,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":922,"time":1783421480990,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":923,"time":1783421480990,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":924,"time":1783421481020,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":925,"time":1783421481020,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":926,"time":1783421481020,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":927,"time":1783421481020,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"验证"}}} +{"type":"assistant/chunk","seq":928,"time":1783421481049,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":929,"time":1783421481077,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"已"}}} +{"type":"assistant/chunk","seq":930,"time":1783421481077,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":931,"time":1783421481077,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":932,"time":1783421481105,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":933,"time":1783421481106,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":934,"time":1783421481106,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":935,"time":1783421481134,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":936,"time":1783421481134,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":937,"time":1783421481134,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":938,"time":1783421481134,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"ls"}}} +{"type":"assistant/chunk","seq":939,"time":1783421481163,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":" -"}}} +{"type":"assistant/chunk","seq":940,"time":1783421481163,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"la"}}} +{"type":"assistant/chunk","seq":941,"time":1783421481163,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":" /"}}} +{"type":"assistant/chunk","seq":942,"time":1783421481163,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"Users"}}} +{"type":"assistant/chunk","seq":943,"time":1783421481163,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":944,"time":1783421481163,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"ww"}}} +{"type":"assistant/chunk","seq":945,"time":1783421481194,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"l"}}} +{"type":"assistant/chunk","seq":946,"time":1783421481194,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":947,"time":1783421481195,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"works"}}} +{"type":"assistant/chunk","seq":948,"time":1783421481195,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"pace"}}} +{"type":"assistant/chunk","seq":949,"time":1783421481195,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"/de"}}} +{"type":"assistant/chunk","seq":950,"time":1783421481195,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"ep"}}} +{"type":"assistant/chunk","seq":951,"time":1783421481223,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"seek"}}} +{"type":"assistant/chunk","seq":952,"time":1783421481223,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"-h"}}} +{"type":"assistant/chunk","seq":953,"time":1783421481223,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"ar"}}} +{"type":"assistant/chunk","seq":954,"time":1783421481223,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"ness"}}} +{"type":"assistant/chunk","seq":955,"time":1783421481223,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"/d"}}} +{"type":"assistant/chunk","seq":956,"time":1783421481223,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":957,"time":1783421481252,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":958,"time":1783421481252,"data":{"turn":5,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":959,"time":1783421481315,"data":{"turn":5,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"创建成功,验证一下。"}}}} +{"type":"assistant/chunk","seq":960,"time":1783421481315,"data":{"turn":5,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","arguments":"{\"description\": \"验证文件已创建\", \"command\": \"ls -la /Users/wwl/workspace/deepseek-harness/d.md\"}"}}}} +{"type":"assistant/chunk","seq":961,"time":1783421481315,"data":{"turn":5,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":24,"outputTokens":86,"cacheReadTokens":2304,"reasoningTokens":6}}}} +{"type":"assistant/chunk","seq":962,"time":1783421481315,"data":{"turn":5,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":963,"time":1783421481315,"data":{"turn":5,"step":2,"content":[{"type":"reasoning","text":"创建成功,验证一下。"},{"type":"tool-call","id":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","arguments":"{\"description\": \"验证文件已创建\", \"command\": \"ls -la /Users/wwl/workspace/deepseek-harness/d.md\"}"}],"usage":{"inputTokens":24,"outputTokens":86,"cacheReadTokens":2304,"reasoningTokens":6}},"sourceEventSeqs":[912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962],"surfaceOp":"append"} +{"type":"tool/call","seq":964,"time":1783421481315,"data":{"turn":5,"step":2,"callId":"call_00_NJbkRNICoS9tkcJjHbbz7097","name":"bash","arguments":"{\"description\": \"验证文件已创建\", \"command\": \"ls -la /Users/wwl/workspace/deepseek-harness/d.md\"}"}} +{"type":"tool/result","seq":965,"time":1783421481385,"data":{"turn":5,"step":2,"callId":"call_00_NJbkRNICoS9tkcJjHbbz7097","content":[{"type":"text","text":"-rw-r--r--@ 1 wwl staff 0 Jul 7 18:51 /Users/wwl/workspace/deepseek-harness/d.md\n"}],"isError":false},"sourceEventSeqs":[964],"surfaceOp":"append"} +{"type":"step/end","seq":966,"time":1783421481385,"data":{"turn":5,"step":2}} +{"type":"step/start","seq":967,"time":1783421481385,"data":{"turn":5,"step":3}} +{"type":"assistant/chunk","seq":968,"time":1783421482029,"data":{"turn":5,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":969,"time":1783421482029,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":970,"time":1783421482060,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"d"}}} +{"type":"assistant/chunk","seq":971,"time":1783421482061,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":972,"time":1783421482061,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":973,"time":1783421482061,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":974,"time":1783421482061,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"已"}}} +{"type":"assistant/chunk","seq":975,"time":1783421482061,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"成功"}}} +{"type":"assistant/chunk","seq":976,"time":1783421482089,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":977,"time":1783421482089,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"!"}}} +{"type":"assistant/chunk","seq":978,"time":1783421482089,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"当前"}}} +{"type":"assistant/chunk","seq":979,"time":1783421482089,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"工作"}}} +{"type":"assistant/chunk","seq":980,"time":1783421482119,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":981,"time":1783421482119,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"下"}}} +{"type":"assistant/chunk","seq":982,"time":1783421482119,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"已有的"}}} +{"type":"assistant/chunk","seq":983,"time":1783421482147,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":984,"time":1783421482147,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":":\n\n"}}} +{"type":"assistant/chunk","seq":985,"time":1783421482147,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"|"}}} +{"type":"assistant/chunk","seq":986,"time":1783421482147,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":987,"time":1783421482147,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":988,"time":1783421482176,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" |"}}} +{"type":"assistant/chunk","seq":989,"time":1783421482176,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":990,"time":1783421482176,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"状态"}}} +{"type":"assistant/chunk","seq":991,"time":1783421482176,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" |\n"}}} +{"type":"assistant/chunk","seq":992,"time":1783421482176,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"|"}}} +{"type":"assistant/chunk","seq":993,"time":1783421482205,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"------"}}} +{"type":"assistant/chunk","seq":994,"time":1783421482205,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"|"}}} +{"type":"assistant/chunk","seq":995,"time":1783421482205,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"------"}}} +{"type":"assistant/chunk","seq":996,"time":1783421482205,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"|\n"}}} +{"type":"assistant/chunk","seq":997,"time":1783421482205,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"|"}}} +{"type":"assistant/chunk","seq":998,"time":1783421482205,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":999,"time":1783421482233,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"a"}}} +{"type":"assistant/chunk","seq":1000,"time":1783421482233,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1001,"time":1783421482233,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":1002,"time":1783421482234,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" |"}}} +{"type":"assistant/chunk","seq":1003,"time":1783421482234,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" ✅"}}} +{"type":"assistant/chunk","seq":1004,"time":1783421482234,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1005,"time":1783421482262,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"已"}}} +{"type":"assistant/chunk","seq":1006,"time":1783421482262,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1007,"time":1783421482262,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" |\n"}}} +{"type":"assistant/chunk","seq":1008,"time":1783421482262,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"|"}}} +{"type":"assistant/chunk","seq":1009,"time":1783421482262,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":1010,"time":1783421482262,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"b"}}} +{"type":"assistant/chunk","seq":1011,"time":1783421482291,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1012,"time":1783421482291,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":1013,"time":1783421482291,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" |"}}} +{"type":"assistant/chunk","seq":1014,"time":1783421482291,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" ❌"}}} +{"type":"assistant/chunk","seq":1015,"time":1783421482291,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1016,"time":1783421482319,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"未"}}} +{"type":"assistant/chunk","seq":1017,"time":1783421482320,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1018,"time":1783421482320,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"("}}} +{"type":"assistant/chunk","seq":1019,"time":1783421482320,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"之前"}}} +{"type":"assistant/chunk","seq":1020,"time":1783421482348,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"权限"}}} +{"type":"assistant/chunk","seq":1021,"time":1783421482348,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"被"}}} +{"type":"assistant/chunk","seq":1022,"time":1783421482348,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"拒"}}} +{"type":"assistant/chunk","seq":1023,"time":1783421482378,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":")"}}} +{"type":"assistant/chunk","seq":1024,"time":1783421482378,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" |\n"}}} +{"type":"assistant/chunk","seq":1025,"time":1783421482379,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"|"}}} +{"type":"assistant/chunk","seq":1026,"time":1783421482379,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":1027,"time":1783421482379,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"c"}}} +{"type":"assistant/chunk","seq":1028,"time":1783421482379,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1029,"time":1783421482405,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":1030,"time":1783421482406,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" |"}}} +{"type":"assistant/chunk","seq":1031,"time":1783421482406,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" ❌"}}} +{"type":"assistant/chunk","seq":1032,"time":1783421482406,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1033,"time":1783421482406,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"未"}}} +{"type":"assistant/chunk","seq":1034,"time":1783421482434,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1035,"time":1783421482435,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" |\n"}}} +{"type":"assistant/chunk","seq":1036,"time":1783421482435,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"|"}}} +{"type":"assistant/chunk","seq":1037,"time":1783421482435,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":1038,"time":1783421482435,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"d"}}} +{"type":"assistant/chunk","seq":1039,"time":1783421482435,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1040,"time":1783421482464,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":1041,"time":1783421482464,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" |"}}} +{"type":"assistant/chunk","seq":1042,"time":1783421482464,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" ✅"}}} +{"type":"assistant/chunk","seq":1043,"time":1783421482464,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1044,"time":1783421482464,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"刚"}}} +{"type":"assistant/chunk","seq":1045,"time":1783421482464,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1046,"time":1783421482493,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" |\n\n"}}} +{"type":"assistant/chunk","seq":1047,"time":1783421482493,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"需要"}}} +{"type":"assistant/chunk","seq":1048,"time":1783421482493,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"我"}}} +{"type":"assistant/chunk","seq":1049,"time":1783421482493,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"帮你"}}} +{"type":"assistant/chunk","seq":1050,"time":1783421482520,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"把"}}} +{"type":"assistant/chunk","seq":1051,"time":1783421482521,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":1052,"time":1783421482521,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"b"}}} +{"type":"assistant/chunk","seq":1053,"time":1783421482550,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1054,"time":1783421482550,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":1055,"time":1783421482550,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1056,"time":1783421482550,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"或"}}} +{"type":"assistant/chunk","seq":1057,"time":1783421482551,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" `"}}} +{"type":"assistant/chunk","seq":1058,"time":1783421482551,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"c"}}} +{"type":"assistant/chunk","seq":1059,"time":1783421482579,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1060,"time":1783421482579,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"`"}}} +{"type":"assistant/chunk","seq":1061,"time":1783421482579,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1062,"time":1783421482579,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"也"}}} +{"type":"assistant/chunk","seq":1063,"time":1783421482579,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"补"}}} +{"type":"assistant/chunk","seq":1064,"time":1783421482607,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"上"}}} +{"type":"assistant/chunk","seq":1065,"time":1783421482607,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"吗"}}} +{"type":"assistant/chunk","seq":1066,"time":1783421482607,"data":{"turn":5,"step":3,"chunk":{"type":"text-delta","index":0,"text":"?"}}} +{"type":"assistant/chunk","seq":1067,"time":1783421482608,"data":{"turn":5,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"`d.md` 已成功创建!当前工作目录下已有的文件:\n\n| 文件 | 状态 |\n|------|------|\n| `a.md` | ✅ 已创建 |\n| `b.md` | ❌ 未创建(之前权限被拒) |\n| `c.md` | ❌ 未创建 |\n| `d.md` | ✅ 刚创建 |\n\n需要我帮你把 `b.md` 或 `c.md` 也补上吗?"}}}} +{"type":"assistant/chunk","seq":1068,"time":1783421482608,"data":{"turn":5,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":160,"outputTokens":101,"cacheReadTokens":2304,"reasoningTokens":0}}}} +{"type":"assistant/chunk","seq":1069,"time":1783421482608,"data":{"turn":5,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":1070,"time":1783421482608,"data":{"turn":5,"step":3,"content":[{"type":"text","text":"`d.md` 已成功创建!当前工作目录下已有的文件:\n\n| 文件 | 状态 |\n|------|------|\n| `a.md` | ✅ 已创建 |\n| `b.md` | ❌ 未创建(之前权限被拒) |\n| `c.md` | ❌ 未创建 |\n| `d.md` | ✅ 刚创建 |\n\n需要我帮你把 `b.md` 或 `c.md` 也补上吗?"}],"usage":{"inputTokens":160,"outputTokens":101,"cacheReadTokens":2304,"reasoningTokens":0}},"sourceEventSeqs":[968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069],"surfaceOp":"append"} +{"type":"step/end","seq":1071,"time":1783421482608,"data":{"turn":5,"step":3}} +{"type":"turn/end","seq":1072,"time":1783421482608,"data":{"turn":5,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":1073,"time":1783421508052,"data":{"turn":6,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1074,"time":1783421508052,"data":{"content":[{"type":"text","text":"帮我在 ~ 创建一个 e.md"}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":1075,"time":1783421508052,"data":{"turn":6,"step":1}} +{"type":"assistant/chunk","seq":1076,"time":1783421508475,"data":{"turn":6,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":1077,"time":1783421508475,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":1078,"time":1783421508591,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"想在"}}} +{"type":"assistant/chunk","seq":1079,"time":1783421508591,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" ~"}}} +{"type":"assistant/chunk","seq":1080,"time":1783421508591,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" ("}}} +{"type":"assistant/chunk","seq":1081,"time":1783421508633,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"home"}}} +{"type":"assistant/chunk","seq":1082,"time":1783421508633,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" directory"}}} +{"type":"assistant/chunk","seq":1083,"time":1783421508637,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":")"}}} +{"type":"assistant/chunk","seq":1084,"time":1783421508637,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1085,"time":1783421508637,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"下"}}} +{"type":"assistant/chunk","seq":1086,"time":1783421508637,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1087,"time":1783421508637,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" e"}}} +{"type":"assistant/chunk","seq":1088,"time":1783421508637,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1089,"time":1783421508673,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1090,"time":1783421508673,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"我的"}}} +{"type":"assistant/chunk","seq":1091,"time":1783421508691,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"工作"}}} +{"type":"assistant/chunk","seq":1092,"time":1783421508720,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1093,"time":1783421508721,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"是"}}} +{"type":"assistant/chunk","seq":1094,"time":1783421508721,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" /"}}} +{"type":"assistant/chunk","seq":1095,"time":1783421508721,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Users"}}} +{"type":"assistant/chunk","seq":1096,"time":1783421508721,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"/"}}} +{"type":"assistant/chunk","seq":1097,"time":1783421508721,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ww"}}} +{"type":"assistant/chunk","seq":1098,"time":1783421508749,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"l"}}} +{"type":"assistant/chunk","seq":1099,"time":1783421508750,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"/"}}} +{"type":"assistant/chunk","seq":1100,"time":1783421508750,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"works"}}} +{"type":"assistant/chunk","seq":1101,"time":1783421508750,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"pace"}}} +{"type":"assistant/chunk","seq":1102,"time":1783421508750,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"/de"}}} +{"type":"assistant/chunk","seq":1103,"time":1783421508750,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ep"}}} +{"type":"assistant/chunk","seq":1104,"time":1783421508777,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"seek"}}} +{"type":"assistant/chunk","seq":1105,"time":1783421508777,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-h"}}} +{"type":"assistant/chunk","seq":1106,"time":1783421508777,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ar"}}} +{"type":"assistant/chunk","seq":1107,"time":1783421508777,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ness"}}} +{"type":"assistant/chunk","seq":1108,"time":1783421508777,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":1109,"time":1783421508778,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"但"}}} +{"type":"assistant/chunk","seq":1110,"time":1783421508806,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" ~"}}} +{"type":"assistant/chunk","seq":1111,"time":1783421508835,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1112,"time":1783421508835,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"是"}}} +{"type":"assistant/chunk","seq":1113,"time":1783421508864,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" /"}}} +{"type":"assistant/chunk","seq":1114,"time":1783421508864,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"Users"}}} +{"type":"assistant/chunk","seq":1115,"time":1783421508864,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"/"}}} +{"type":"assistant/chunk","seq":1116,"time":1783421508864,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ww"}}} +{"type":"assistant/chunk","seq":1117,"time":1783421508864,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"l"}}} +{"type":"assistant/chunk","seq":1118,"time":1783421508864,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1119,"time":1783421508893,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"我需要"}}} +{"type":"assistant/chunk","seq":1120,"time":1783421508894,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"确认"}}} +{"type":"assistant/chunk","seq":1121,"time":1783421508922,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"当前"}}} +{"type":"assistant/chunk","seq":1122,"time":1783421508922,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" workspace"}}} +{"type":"assistant/chunk","seq":1123,"time":1783421508951,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-w"}}} +{"type":"assistant/chunk","seq":1124,"time":1783421508951,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"rite"}}} +{"type":"assistant/chunk","seq":1125,"time":1783421508951,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1126,"time":1783421508951,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"模式"}}} +{"type":"assistant/chunk","seq":1127,"time":1783421508980,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"是否"}}} +{"type":"assistant/chunk","seq":1128,"time":1783421508980,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"允许"}}} +{"type":"assistant/chunk","seq":1129,"time":1783421508980,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"在"}}} +{"type":"assistant/chunk","seq":1130,"time":1783421509009,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" home"}}} +{"type":"assistant/chunk","seq":1131,"time":1783421509009,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1132,"time":1783421509009,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1133,"time":1783421509041,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"下"}}} +{"type":"assistant/chunk","seq":1134,"time":1783421509041,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"写入"}}} +{"type":"assistant/chunk","seq":1135,"time":1783421509070,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1136,"time":1783421509071,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"先"}}} +{"type":"assistant/chunk","seq":1137,"time":1783421509121,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"试试"}}} +{"type":"assistant/chunk","seq":1138,"time":1783421509121,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"看"}}} +{"type":"assistant/chunk","seq":1139,"time":1783421509121,"data":{"turn":6,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1140,"time":1783421509278,"data":{"turn":6,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":1141,"time":1783421509278,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":1142,"time":1783421509278,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":1143,"time":1783421509278,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1144,"time":1783421509278,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":1145,"time":1783421509278,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1146,"time":1783421509278,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1147,"time":1783421509278,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1148,"time":1783421509278,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"在"}}} +{"type":"assistant/chunk","seq":1149,"time":1783421509278,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":" home"}}} +{"type":"assistant/chunk","seq":1150,"time":1783421509310,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1151,"time":1783421509310,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"目录"}}} +{"type":"assistant/chunk","seq":1152,"time":1783421509310,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":1153,"time":1783421509310,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":" e"}}} +{"type":"assistant/chunk","seq":1154,"time":1783421509310,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1155,"time":1783421509330,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1156,"time":1783421509359,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1157,"time":1783421509359,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1158,"time":1783421509359,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":1159,"time":1783421509359,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1160,"time":1783421509359,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1161,"time":1783421509388,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1162,"time":1783421509388,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"touch"}}} +{"type":"assistant/chunk","seq":1163,"time":1783421509388,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":" ~"}}} +{"type":"assistant/chunk","seq":1164,"time":1783421509418,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":1165,"time":1783421509418,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"e"}}} +{"type":"assistant/chunk","seq":1166,"time":1783421509418,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1167,"time":1783421509418,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1168,"time":1783421509445,"data":{"turn":6,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":1169,"time":1783421509505,"data":{"turn":6,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"用户想在 ~ (home directory) 下创建 e.md。我的工作目录是 /Users/wwl/workspace/deepseek-harness,但 ~ 是 /Users/wwl。我需要确认当前 workspace-write 模式是否允许在 home 目录下写入。先试试看。"}}}} +{"type":"assistant/chunk","seq":1170,"time":1783421509505,"data":{"turn":6,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","arguments":"{\"description\": \"在 home 目录创建 e.md\", \"command\": \"touch ~/e.md\"}"}}}} +{"type":"assistant/chunk","seq":1171,"time":1783421509505,"data":{"turn":6,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":16,"outputTokens":132,"cacheReadTokens":2560,"reasoningTokens":63}}}} +{"type":"assistant/chunk","seq":1172,"time":1783421509505,"data":{"turn":6,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":1173,"time":1783421509505,"data":{"turn":6,"step":1,"content":[{"type":"reasoning","text":"用户想在 ~ (home directory) 下创建 e.md。我的工作目录是 /Users/wwl/workspace/deepseek-harness,但 ~ 是 /Users/wwl。我需要确认当前 workspace-write 模式是否允许在 home 目录下写入。先试试看。"},{"type":"tool-call","id":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","arguments":"{\"description\": \"在 home 目录创建 e.md\", \"command\": \"touch ~/e.md\"}"}],"usage":{"inputTokens":16,"outputTokens":132,"cacheReadTokens":2560,"reasoningTokens":63}},"sourceEventSeqs":[1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172],"surfaceOp":"append"} +{"type":"tool/call","seq":1174,"time":1783421509505,"data":{"turn":6,"step":1,"callId":"call_00_Rpnlygq8KCg3enlKpw7d6792","name":"bash","arguments":"{\"description\": \"在 home 目录创建 e.md\", \"command\": \"touch ~/e.md\"}"}} +{"type":"tool/result","seq":1175,"time":1783421509591,"data":{"turn":6,"step":1,"callId":"call_00_Rpnlygq8KCg3enlKpw7d6792","content":[{"type":"text","text":"[stderr]\ntouch: /Users/wwl/e.md: Operation not permitted\n[sandbox: file access denied under workspace-write mode]\n[exit code: 1]"}],"isError":false},"sourceEventSeqs":[1174],"surfaceOp":"append"} +{"type":"step/end","seq":1176,"time":1783421509592,"data":{"turn":6,"step":1}} +{"type":"step/start","seq":1177,"time":1783421509592,"data":{"turn":6,"step":2}} +{"type":"assistant/chunk","seq":1178,"time":1783421510299,"data":{"turn":6,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":1179,"time":1783421510299,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"works"}}} +{"type":"assistant/chunk","seq":1180,"time":1783421510479,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"pace"}}} +{"type":"assistant/chunk","seq":1181,"time":1783421510507,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-w"}}} +{"type":"assistant/chunk","seq":1182,"time":1783421510508,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"rite"}}} +{"type":"assistant/chunk","seq":1183,"time":1783421510508,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1184,"time":1783421510508,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"模式"}}} +{"type":"assistant/chunk","seq":1185,"time":1783421510537,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"只"}}} +{"type":"assistant/chunk","seq":1186,"time":1783421510537,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"允许"}}} +{"type":"assistant/chunk","seq":1187,"time":1783421510573,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"在工作"}}} +{"type":"assistant/chunk","seq":1188,"time":1783421510573,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1189,"time":1783421510595,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"("}}} +{"type":"assistant/chunk","seq":1190,"time":1783421510626,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"/"}}} +{"type":"assistant/chunk","seq":1191,"time":1783421510626,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"Users"}}} +{"type":"assistant/chunk","seq":1192,"time":1783421510626,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"/"}}} +{"type":"assistant/chunk","seq":1193,"time":1783421510626,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ww"}}} +{"type":"assistant/chunk","seq":1194,"time":1783421510627,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"l"}}} +{"type":"assistant/chunk","seq":1195,"time":1783421510627,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"/"}}} +{"type":"assistant/chunk","seq":1196,"time":1783421510652,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"works"}}} +{"type":"assistant/chunk","seq":1197,"time":1783421510652,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"pace"}}} +{"type":"assistant/chunk","seq":1198,"time":1783421510652,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"/de"}}} +{"type":"assistant/chunk","seq":1199,"time":1783421510652,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ep"}}} +{"type":"assistant/chunk","seq":1200,"time":1783421510652,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"seek"}}} +{"type":"assistant/chunk","seq":1201,"time":1783421510652,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-h"}}} +{"type":"assistant/chunk","seq":1202,"time":1783421510681,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ar"}}} +{"type":"assistant/chunk","seq":1203,"time":1783421510681,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ness"}}} +{"type":"assistant/chunk","seq":1204,"time":1783421510681,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":")"}}} +{"type":"assistant/chunk","seq":1205,"time":1783421510681,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"下"}}} +{"type":"assistant/chunk","seq":1206,"time":1783421510681,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"写入"}}} +{"type":"assistant/chunk","seq":1207,"time":1783421510682,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":1208,"time":1783421510710,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"不允许"}}} +{"type":"assistant/chunk","seq":1209,"time":1783421510739,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"在"}}} +{"type":"assistant/chunk","seq":1210,"time":1783421510739,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" home"}}} +{"type":"assistant/chunk","seq":1211,"time":1783421510768,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1212,"time":1783421510768,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1213,"time":1783421510768,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"写入"}}} +{"type":"assistant/chunk","seq":1214,"time":1783421510797,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1215,"time":1783421510797,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"我需要"}}} +{"type":"assistant/chunk","seq":1216,"time":1783421510826,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"申请"}}} +{"type":"assistant/chunk","seq":1217,"time":1783421510858,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"更"}}} +{"type":"assistant/chunk","seq":1218,"time":1783421510858,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"高级"}}} +{"type":"assistant/chunk","seq":1219,"time":1783421510888,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"的"}}} +{"type":"assistant/chunk","seq":1220,"time":1783421510888,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"权限"}}} +{"type":"assistant/chunk","seq":1221,"time":1783421510888,"data":{"turn":6,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1222,"time":1783421510916,"data":{"turn":6,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":1223,"time":1783421510916,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"当前"}}} +{"type":"assistant/chunk","seq":1224,"time":1783421510946,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":1225,"time":1783421511021,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"works"}}} +{"type":"assistant/chunk","seq":1226,"time":1783421511022,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"pace"}}} +{"type":"assistant/chunk","seq":1227,"time":1783421511022,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"-w"}}} +{"type":"assistant/chunk","seq":1228,"time":1783421511022,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"rite"}}} +{"type":"assistant/chunk","seq":1229,"time":1783421511022,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":1230,"time":1783421511022,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1231,"time":1783421511067,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"模式"}}} +{"type":"assistant/chunk","seq":1232,"time":1783421511067,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"只"}}} +{"type":"assistant/chunk","seq":1233,"time":1783421511067,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"允许"}}} +{"type":"assistant/chunk","seq":1234,"time":1783421511095,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"在工作"}}} +{"type":"assistant/chunk","seq":1235,"time":1783421511095,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":1236,"time":1783421511095,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"下"}}} +{"type":"assistant/chunk","seq":1237,"time":1783421511124,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"写入"}}} +{"type":"assistant/chunk","seq":1238,"time":1783421511124,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":1239,"time":1783421511125,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"无法"}}} +{"type":"assistant/chunk","seq":1240,"time":1783421511153,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"写入"}}} +{"type":"assistant/chunk","seq":1241,"time":1783421511183,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":" home"}}} +{"type":"assistant/chunk","seq":1242,"time":1783421511183,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1243,"time":1783421511183,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":1244,"time":1783421511183,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"。"}}} +{"type":"assistant/chunk","seq":1245,"time":1783421511212,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"我需要"}}} +{"type":"assistant/chunk","seq":1246,"time":1783421511212,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"申请"}}} +{"type":"assistant/chunk","seq":1247,"time":1783421511212,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"完全"}}} +{"type":"assistant/chunk","seq":1248,"time":1783421511242,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"访问"}}} +{"type":"assistant/chunk","seq":1249,"time":1783421511243,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"权限"}}} +{"type":"assistant/chunk","seq":1250,"time":1783421511270,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"来完成"}}} +{"type":"assistant/chunk","seq":1251,"time":1783421511300,"data":{"turn":6,"step":2,"chunk":{"type":"text-delta","index":1,"text":"。"}}} +{"type":"assistant/chunk","seq":1252,"time":1783421511357,"data":{"turn":6,"step":2,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":1253,"time":1783421511357,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":1254,"time":1783421511357,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":1255,"time":1783421511357,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1256,"time":1783421511386,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":1257,"time":1783421511386,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1258,"time":1783421511386,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1259,"time":1783421511386,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1260,"time":1783421511416,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"touch"}}} +{"type":"assistant/chunk","seq":1261,"time":1783421511416,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" ~"}}} +{"type":"assistant/chunk","seq":1262,"time":1783421511416,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":1263,"time":1783421511416,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"e"}}} +{"type":"assistant/chunk","seq":1264,"time":1783421511416,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1265,"time":1783421511444,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1266,"time":1783421511473,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1267,"time":1783421511473,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1268,"time":1783421511473,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":1269,"time":1783421511473,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1270,"time":1783421511473,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1271,"time":1783421511506,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1272,"time":1783421511506,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"在"}}} +{"type":"assistant/chunk","seq":1273,"time":1783421511506,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" home"}}} +{"type":"assistant/chunk","seq":1274,"time":1783421511506,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1275,"time":1783421511506,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"目录"}}} +{"type":"assistant/chunk","seq":1276,"time":1783421511535,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":1277,"time":1783421511535,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" e"}}} +{"type":"assistant/chunk","seq":1278,"time":1783421511535,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1279,"time":1783421511535,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1280,"time":1783421511564,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1281,"time":1783421511564,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1282,"time":1783421511593,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"sand"}}} +{"type":"assistant/chunk","seq":1283,"time":1783421511593,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"box"}}} +{"type":"assistant/chunk","seq":1284,"time":1783421511593,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"_per"}}} +{"type":"assistant/chunk","seq":1285,"time":1783421511593,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"missions"}}} +{"type":"assistant/chunk","seq":1286,"time":1783421511593,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1287,"time":1783421511593,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1288,"time":1783421511621,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1289,"time":1783421511621,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"danger"}}} +{"type":"assistant/chunk","seq":1290,"time":1783421511621,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"-full"}}} +{"type":"assistant/chunk","seq":1291,"time":1783421511621,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"-access"}}} +{"type":"assistant/chunk","seq":1292,"time":1783421511650,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1293,"time":1783421511679,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1294,"time":1783421511679,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1295,"time":1783421511679,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"just"}}} +{"type":"assistant/chunk","seq":1296,"time":1783421511679,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"ification"}}} +{"type":"assistant/chunk","seq":1297,"time":1783421511679,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1298,"time":1783421511709,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1299,"time":1783421511709,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1300,"time":1783421511709,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"用户"}}} +{"type":"assistant/chunk","seq":1301,"time":1783421511709,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"要求在"}}} +{"type":"assistant/chunk","seq":1302,"time":1783421511737,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" home"}}} +{"type":"assistant/chunk","seq":1303,"time":1783421511737,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1304,"time":1783421511737,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"目录"}}} +{"type":"assistant/chunk","seq":1305,"time":1783421511737,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" (~"}}} +{"type":"assistant/chunk","seq":1306,"time":1783421511766,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":")"}}} +{"type":"assistant/chunk","seq":1307,"time":1783421511766,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1308,"time":1783421511766,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"下"}}} +{"type":"assistant/chunk","seq":1309,"time":1783421511766,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":1310,"time":1783421511767,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" e"}}} +{"type":"assistant/chunk","seq":1311,"time":1783421511767,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1312,"time":1783421511796,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1313,"time":1783421511796,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":1314,"time":1783421511824,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":","}}} +{"type":"assistant/chunk","seq":1315,"time":1783421511824,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"这"}}} +{"type":"assistant/chunk","seq":1316,"time":1783421511853,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"超出了"}}} +{"type":"assistant/chunk","seq":1317,"time":1783421511883,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" workspace"}}} +{"type":"assistant/chunk","seq":1318,"time":1783421511883,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"-w"}}} +{"type":"assistant/chunk","seq":1319,"time":1783421511884,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"rite"}}} +{"type":"assistant/chunk","seq":1320,"time":1783421511884,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1321,"time":1783421511911,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"模式"}}} +{"type":"assistant/chunk","seq":1322,"time":1783421511912,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"允许"}}} +{"type":"assistant/chunk","seq":1323,"time":1783421511940,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"的范围"}}} +{"type":"assistant/chunk","seq":1324,"time":1783421511968,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":","}}} +{"type":"assistant/chunk","seq":1325,"time":1783421511997,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"需要"}}} +{"type":"assistant/chunk","seq":1326,"time":1783421511997,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"完全"}}} +{"type":"assistant/chunk","seq":1327,"time":1783421511997,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"访问"}}} +{"type":"assistant/chunk","seq":1328,"time":1783421512026,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"权限"}}} +{"type":"assistant/chunk","seq":1329,"time":1783421512026,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"。"}}} +{"type":"assistant/chunk","seq":1330,"time":1783421512026,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1331,"time":1783421512055,"data":{"turn":6,"step":2,"chunk":{"type":"tool-call-delta","index":2,"id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":1332,"time":1783421512091,"data":{"turn":6,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"workspace-write 模式只允许在工作目录(/Users/wwl/workspace/deepseek-harness)下写入,不允许在 home 目录写入。我需要申请更高级的权限。"}}}} +{"type":"assistant/chunk","seq":1333,"time":1783421512091,"data":{"turn":6,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"当前 `workspace-write` 模式只允许在工作目录下写入,无法写入 home 目录。我需要申请完全访问权限来完成。"}}}} +{"type":"assistant/chunk","seq":1334,"time":1783421512091,"data":{"turn":6,"step":2,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","arguments":"{\"command\": \"touch ~/e.md\", \"description\": \"在 home 目录创建 e.md\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"用户要求在 home 目录 (~) 下创建 e.md 文件,这超出了 workspace-write 模式允许的范围,需要完全访问权限。\"}"}}}} +{"type":"assistant/chunk","seq":1335,"time":1783421512091,"data":{"turn":6,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":69,"outputTokens":206,"cacheReadTokens":2688,"reasoningTokens":43}}}} +{"type":"assistant/chunk","seq":1336,"time":1783421512091,"data":{"turn":6,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":1337,"time":1783421512091,"data":{"turn":6,"step":2,"content":[{"type":"reasoning","text":"workspace-write 模式只允许在工作目录(/Users/wwl/workspace/deepseek-harness)下写入,不允许在 home 目录写入。我需要申请更高级的权限。"},{"type":"text","text":"当前 `workspace-write` 模式只允许在工作目录下写入,无法写入 home 目录。我需要申请完全访问权限来完成。"},{"type":"tool-call","id":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","arguments":"{\"command\": \"touch ~/e.md\", \"description\": \"在 home 目录创建 e.md\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"用户要求在 home 目录 (~) 下创建 e.md 文件,这超出了 workspace-write 模式允许的范围,需要完全访问权限。\"}"}],"usage":{"inputTokens":69,"outputTokens":206,"cacheReadTokens":2688,"reasoningTokens":43}},"sourceEventSeqs":[1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336],"surfaceOp":"append"} +{"type":"tool/call","seq":1338,"time":1783421512092,"data":{"turn":6,"step":2,"callId":"call_00_M4DDrUt9dRI5oYmqTxwb4616","name":"bash","arguments":"{\"command\": \"touch ~/e.md\", \"description\": \"在 home 目录创建 e.md\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"用户要求在 home 目录 (~) 下创建 e.md 文件,这超出了 workspace-write 模式允许的范围,需要完全访问权限。\"}"}} +{"type":"approval/asked","seq":1339,"time":1783421512092,"data":{"id":"587a78f5-df57-499e-98d7-0464f17734e6","toolName":"bash","callId":"call_00_M4DDrUt9dRI5oYmqTxwb4616","reason":"escalate sandbox to danger-full-access: 用户要求在 home 目录 (~) 下创建 e.md 文件,这超出了 workspace-write 模式允许的范围,需要完全访问权限。"}} +{"type":"approval/decided","seq":1340,"time":1783421512092,"data":{"id":"587a78f5-df57-499e-98d7-0464f17734e6","outcome":"rejected"}} +{"type":"tool/result","seq":1341,"time":1783421512092,"data":{"turn":6,"step":2,"callId":"call_00_M4DDrUt9dRI5oYmqTxwb4616","content":[{"type":"text","text":"Error: the user rejected escalating this command to \"danger-full-access\""}],"isError":true},"sourceEventSeqs":[1338],"surfaceOp":"append"} +{"type":"step/end","seq":1342,"time":1783421512092,"data":{"turn":6,"step":2}} +{"type":"step/start","seq":1343,"time":1783421512093,"data":{"turn":6,"step":3}} +{"type":"assistant/chunk","seq":1344,"time":1783421512675,"data":{"turn":6,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":1345,"time":1783421512675,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"又被"}}} +{"type":"assistant/chunk","seq":1346,"time":1783421512781,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"拒绝了"}}} +{"type":"assistant/chunk","seq":1347,"time":1783421512813,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1348,"time":1783421512813,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"没办法"}}} +{"type":"assistant/chunk","seq":1349,"time":1783421512841,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"在"}}} +{"type":"assistant/chunk","seq":1350,"time":1783421512841,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" home"}}} +{"type":"assistant/chunk","seq":1351,"time":1783421512868,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1352,"time":1783421512868,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1353,"time":1783421512868,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"下"}}} +{"type":"assistant/chunk","seq":1354,"time":1783421512897,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1355,"time":1783421512897,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"文件"}}} +{"type":"assistant/chunk","seq":1356,"time":1783421512897,"data":{"turn":6,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1357,"time":1783421512926,"data":{"turn":6,"step":3,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":1358,"time":1783421512926,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"权限"}}} +{"type":"assistant/chunk","seq":1359,"time":1783421512957,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"申请"}}} +{"type":"assistant/chunk","seq":1360,"time":1783421512957,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"被"}}} +{"type":"assistant/chunk","seq":1361,"time":1783421512957,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"拒绝了"}}} +{"type":"assistant/chunk","seq":1362,"time":1783421512984,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":1363,"time":1783421512984,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"我"}}} +{"type":"assistant/chunk","seq":1364,"time":1783421513014,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"无法"}}} +{"type":"assistant/chunk","seq":1365,"time":1783421513041,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"在"}}} +{"type":"assistant/chunk","seq":1366,"time":1783421513070,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":1367,"time":1783421513070,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"~"}}} +{"type":"assistant/chunk","seq":1368,"time":1783421513070,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":1369,"time":1783421513070,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" ("}}} +{"type":"assistant/chunk","seq":1370,"time":1783421513099,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"home"}}} +{"type":"assistant/chunk","seq":1371,"time":1783421513099,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1372,"time":1783421513128,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":1373,"time":1783421513128,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":")"}}} +{"type":"assistant/chunk","seq":1374,"time":1783421513128,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1375,"time":1783421513128,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"下"}}} +{"type":"assistant/chunk","seq":1376,"time":1783421513128,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":1377,"time":1783421513128,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":1378,"time":1783421513160,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"e"}}} +{"type":"assistant/chunk","seq":1379,"time":1783421513160,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":1380,"time":1783421513160,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":1381,"time":1783421513160,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"。\n\n"}}} +{"type":"assistant/chunk","seq":1382,"time":1783421513161,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":1383,"time":1783421513191,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"建议"}}} +{"type":"assistant/chunk","seq":1384,"time":1783421513191,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"替代"}}} +{"type":"assistant/chunk","seq":1385,"time":1783421513191,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"方案"}}} +{"type":"assistant/chunk","seq":1386,"time":1783421513218,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":":"}}} +{"type":"assistant/chunk","seq":1387,"time":1783421513246,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"**\n"}}} +{"type":"assistant/chunk","seq":1388,"time":1783421513247,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"1"}}} +{"type":"assistant/chunk","seq":1389,"time":1783421513247,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"."}}} +{"type":"assistant/chunk","seq":1390,"time":1783421513247,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" **"}}} +{"type":"assistant/chunk","seq":1391,"time":1783421513277,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"在工作"}}} +{"type":"assistant/chunk","seq":1392,"time":1783421513307,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":1393,"time":1783421513307,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"下"}}} +{"type":"assistant/chunk","seq":1394,"time":1783421513307,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":1395,"time":1783421513337,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":1396,"time":1783421513337,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" —"}}} +{"type":"assistant/chunk","seq":1397,"time":1783421513337,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1398,"time":1783421513362,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"如果你"}}} +{"type":"assistant/chunk","seq":1399,"time":1783421513391,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"允许"}}} +{"type":"assistant/chunk","seq":1400,"time":1783421513421,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":1401,"time":1783421513421,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"我可以"}}} +{"type":"assistant/chunk","seq":1402,"time":1783421513421,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"把"}}} +{"type":"assistant/chunk","seq":1403,"time":1783421513451,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":1404,"time":1783421513452,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"e"}}} +{"type":"assistant/chunk","seq":1405,"time":1783421513452,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":1406,"time":1783421513452,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":1407,"time":1783421513452,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1408,"time":1783421513452,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":1409,"time":1783421513478,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"到"}}} +{"type":"assistant/chunk","seq":1410,"time":1783421513478,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"当前"}}} +{"type":"assistant/chunk","seq":1411,"time":1783421513478,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"工作"}}} +{"type":"assistant/chunk","seq":1412,"time":1783421513507,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":1413,"time":1783421513507,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" `/"}}} +{"type":"assistant/chunk","seq":1414,"time":1783421513507,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"Users"}}} +{"type":"assistant/chunk","seq":1415,"time":1783421513507,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":1416,"time":1783421513536,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"ww"}}} +{"type":"assistant/chunk","seq":1417,"time":1783421513536,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"l"}}} +{"type":"assistant/chunk","seq":1418,"time":1783421513536,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":1419,"time":1783421513536,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"works"}}} +{"type":"assistant/chunk","seq":1420,"time":1783421513536,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"pace"}}} +{"type":"assistant/chunk","seq":1421,"time":1783421513536,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"/de"}}} +{"type":"assistant/chunk","seq":1422,"time":1783421513567,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"ep"}}} +{"type":"assistant/chunk","seq":1423,"time":1783421513567,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"seek"}}} +{"type":"assistant/chunk","seq":1424,"time":1783421513567,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"-h"}}} +{"type":"assistant/chunk","seq":1425,"time":1783421513567,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"ar"}}} +{"type":"assistant/chunk","seq":1426,"time":1783421513567,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"ness"}}} +{"type":"assistant/chunk","seq":1427,"time":1783421513567,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":1428,"time":1783421513594,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"`\n"}}} +{"type":"assistant/chunk","seq":1429,"time":1783421513594,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"2"}}} +{"type":"assistant/chunk","seq":1430,"time":1783421513594,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"."}}} +{"type":"assistant/chunk","seq":1431,"time":1783421513594,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" **"}}} +{"type":"assistant/chunk","seq":1432,"time":1783421513594,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"自行"}}} +{"type":"assistant/chunk","seq":1433,"time":1783421513595,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"在"}}} +{"type":"assistant/chunk","seq":1434,"time":1783421513625,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"本地"}}} +{"type":"assistant/chunk","seq":1435,"time":1783421513625,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":1436,"time":1783421513626,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":1437,"time":1783421513626,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" —"}}} +{"type":"assistant/chunk","seq":1438,"time":1783421513626,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1439,"time":1783421513653,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"在"}}} +{"type":"assistant/chunk","seq":1440,"time":1783421513653,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"终端"}}} +{"type":"assistant/chunk","seq":1441,"time":1783421513653,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"执行"}}} +{"type":"assistant/chunk","seq":1442,"time":1783421513681,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":":\n"}}} +{"type":"assistant/chunk","seq":1443,"time":1783421513681,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1444,"time":1783421513681,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" ```"}}} +{"type":"assistant/chunk","seq":1445,"time":1783421513681,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"bash"}}} +{"type":"assistant/chunk","seq":1446,"time":1783421513681,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"\n"}}} +{"type":"assistant/chunk","seq":1447,"time":1783421513681,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1448,"time":1783421513681,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" touch"}}} +{"type":"assistant/chunk","seq":1449,"time":1783421513709,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" ~"}}} +{"type":"assistant/chunk","seq":1450,"time":1783421513709,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":1451,"time":1783421513709,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"e"}}} +{"type":"assistant/chunk","seq":1452,"time":1783421513709,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":1453,"time":1783421513709,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"\n"}}} +{"type":"assistant/chunk","seq":1454,"time":1783421513709,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1455,"time":1783421513740,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" ```\n\n"}}} +{"type":"assistant/chunk","seq":1456,"time":1783421513740,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"需要"}}} +{"type":"assistant/chunk","seq":1457,"time":1783421513772,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"我用"}}} +{"type":"assistant/chunk","seq":1458,"time":1783421513799,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"方案"}}} +{"type":"assistant/chunk","seq":1459,"time":1783421513828,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1460,"time":1783421513828,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"1"}}} +{"type":"assistant/chunk","seq":1461,"time":1783421513828,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1462,"time":1783421513828,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"在工作"}}} +{"type":"assistant/chunk","seq":1463,"time":1783421513856,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":1464,"time":1783421513856,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"下"}}} +{"type":"assistant/chunk","seq":1465,"time":1783421513886,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":1466,"time":1783421513886,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":1467,"time":1783421513886,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"e"}}} +{"type":"assistant/chunk","seq":1468,"time":1783421513886,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":1469,"time":1783421513886,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":1470,"time":1783421513886,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1471,"time":1783421513917,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"吗"}}} +{"type":"assistant/chunk","seq":1472,"time":1783421513917,"data":{"turn":6,"step":3,"chunk":{"type":"text-delta","index":1,"text":"?"}}} +{"type":"assistant/chunk","seq":1473,"time":1783421513917,"data":{"turn":6,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"又被拒绝了。没办法在 home 目录下创建文件。"}}}} +{"type":"assistant/chunk","seq":1474,"time":1783421513917,"data":{"turn":6,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"权限申请被拒绝了,我无法在 `~` (home 目录) 下创建 `e.md`。\n\n**建议替代方案:**\n1. **在工作目录下创建** — 如果你允许,我可以把 `e.md` 创建到当前工作目录 `/Users/wwl/workspace/deepseek-harness/`\n2. **自行在本地创建** — 在终端执行:\n ```bash\n touch ~/e.md\n ```\n\n需要我用方案 1 在工作目录下创建 `e.md` 吗?"}}}} +{"type":"assistant/chunk","seq":1475,"time":1783421513917,"data":{"turn":6,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":301,"outputTokens":128,"cacheReadTokens":2688,"reasoningTokens":12}}}} +{"type":"assistant/chunk","seq":1476,"time":1783421513917,"data":{"turn":6,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":1477,"time":1783421513918,"data":{"turn":6,"step":3,"content":[{"type":"reasoning","text":"又被拒绝了。没办法在 home 目录下创建文件。"},{"type":"text","text":"权限申请被拒绝了,我无法在 `~` (home 目录) 下创建 `e.md`。\n\n**建议替代方案:**\n1. **在工作目录下创建** — 如果你允许,我可以把 `e.md` 创建到当前工作目录 `/Users/wwl/workspace/deepseek-harness/`\n2. **自行在本地创建** — 在终端执行:\n ```bash\n touch ~/e.md\n ```\n\n需要我用方案 1 在工作目录下创建 `e.md` 吗?"}],"usage":{"inputTokens":301,"outputTokens":128,"cacheReadTokens":2688,"reasoningTokens":12}},"sourceEventSeqs":[1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476],"surfaceOp":"append"} +{"type":"step/end","seq":1478,"time":1783421513918,"data":{"turn":6,"step":3}} +{"type":"turn/end","seq":1479,"time":1783421513918,"data":{"turn":6,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":1480,"time":1783421524030,"data":{"turn":7,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"approval/policy","seq":1481,"time":1783421524030,"data":{"policy":"ask"}} +{"type":"user/message","seq":1482,"time":1783421524030,"data":{"content":[{"type":"text","text":"帮我在 ~ 创建一个 f.md"}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"context/message","seq":1483,"time":1783421524030,"data":{"content":[{"type":"text","text":"The approval policy changed from \"never\" to \"ask\" (changed by the user)."}],"source":{"kind":"plugin","plugin":"approval"}},"surfaceOp":"append"} +{"type":"step/start","seq":1484,"time":1783421524030,"data":{"turn":7,"step":1}} +{"type":"request/header-delta","seq":1485,"time":1783421524030,"data":{"system":{"keepStart":13,"keepEnd":0,"insert":[]}}} +{"type":"assistant/chunk","seq":1486,"time":1783421524940,"data":{"turn":7,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":1487,"time":1783421524941,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":1488,"time":1783421525045,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"又"}}} +{"type":"assistant/chunk","seq":1489,"time":1783421525073,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"想在"}}} +{"type":"assistant/chunk","seq":1490,"time":1783421525101,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" home"}}} +{"type":"assistant/chunk","seq":1491,"time":1783421525131,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1492,"time":1783421525131,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1493,"time":1783421525131,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"下"}}} +{"type":"assistant/chunk","seq":1494,"time":1783421525131,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1495,"time":1783421525131,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" f"}}} +{"type":"assistant/chunk","seq":1496,"time":1783421525131,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1497,"time":1783421525158,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1498,"time":1783421525159,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"之前的"}}} +{"type":"assistant/chunk","seq":1499,"time":1783421525195,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" e"}}} +{"type":"assistant/chunk","seq":1500,"time":1783421525220,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1501,"time":1783421525220,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1502,"time":1783421525220,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"因为"}}} +{"type":"assistant/chunk","seq":1503,"time":1783421525249,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"权限"}}} +{"type":"assistant/chunk","seq":1504,"time":1783421525278,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"被"}}} +{"type":"assistant/chunk","seq":1505,"time":1783421525278,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"拒"}}} +{"type":"assistant/chunk","seq":1506,"time":1783421525307,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"没有"}}} +{"type":"assistant/chunk","seq":1507,"time":1783421525307,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1508,"time":1783421525337,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"成功"}}} +{"type":"assistant/chunk","seq":1509,"time":1783421525338,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1510,"time":1783421525338,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"现在"}}} +{"type":"assistant/chunk","seq":1511,"time":1783421525338,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" policy"}}} +{"type":"assistant/chunk","seq":1512,"time":1783421525364,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1513,"time":1783421525364,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"改"}}} +{"type":"assistant/chunk","seq":1514,"time":1783421525393,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"成了"}}} +{"type":"assistant/chunk","seq":1515,"time":1783421525393,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":1516,"time":1783421525393,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ask"}}} +{"type":"assistant/chunk","seq":1517,"time":1783421525393,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\","}}} +{"type":"assistant/chunk","seq":1518,"time":1783421525422,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"所以我"}}} +{"type":"assistant/chunk","seq":1519,"time":1783421525451,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"需要"}}} +{"type":"assistant/chunk","seq":1520,"time":1783421525451,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"申请"}}} +{"type":"assistant/chunk","seq":1521,"time":1783421525480,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" danger"}}} +{"type":"assistant/chunk","seq":1522,"time":1783421525510,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-full"}}} +{"type":"assistant/chunk","seq":1523,"time":1783421525510,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-access"}}} +{"type":"assistant/chunk","seq":1524,"time":1783421525510,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1525,"time":1783421525510,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"权限"}}} +{"type":"assistant/chunk","seq":1526,"time":1783421525510,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":1527,"time":1783421525538,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":1528,"time":1783421525539,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"会被"}}} +{"type":"assistant/chunk","seq":1529,"time":1783421525570,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"询问"}}} +{"type":"assistant/chunk","seq":1530,"time":1783421525597,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"是否"}}} +{"type":"assistant/chunk","seq":1531,"time":1783421525597,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"批准"}}} +{"type":"assistant/chunk","seq":1532,"time":1783421525626,"data":{"turn":7,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1533,"time":1783421525714,"data":{"turn":7,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":1534,"time":1783421525714,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":1535,"time":1783421525714,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":1536,"time":1783421525714,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1537,"time":1783421525743,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":1538,"time":1783421525743,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1539,"time":1783421525743,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1540,"time":1783421525744,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1541,"time":1783421525775,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"touch"}}} +{"type":"assistant/chunk","seq":1542,"time":1783421525775,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" ~"}}} +{"type":"assistant/chunk","seq":1543,"time":1783421525775,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":1544,"time":1783421525775,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"f"}}} +{"type":"assistant/chunk","seq":1545,"time":1783421525775,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1546,"time":1783421525803,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1547,"time":1783421525833,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1548,"time":1783421525833,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1549,"time":1783421525833,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":1550,"time":1783421525833,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1551,"time":1783421525833,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1552,"time":1783421525864,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1553,"time":1783421525864,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"在"}}} +{"type":"assistant/chunk","seq":1554,"time":1783421525864,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" home"}}} +{"type":"assistant/chunk","seq":1555,"time":1783421525864,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1556,"time":1783421525864,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"目录"}}} +{"type":"assistant/chunk","seq":1557,"time":1783421525891,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":1558,"time":1783421525891,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" f"}}} +{"type":"assistant/chunk","seq":1559,"time":1783421525892,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1560,"time":1783421525892,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1561,"time":1783421525920,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1562,"time":1783421525920,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1563,"time":1783421525949,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"sand"}}} +{"type":"assistant/chunk","seq":1564,"time":1783421525949,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"box"}}} +{"type":"assistant/chunk","seq":1565,"time":1783421525949,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"_per"}}} +{"type":"assistant/chunk","seq":1566,"time":1783421525949,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"missions"}}} +{"type":"assistant/chunk","seq":1567,"time":1783421525949,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1568,"time":1783421525949,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1569,"time":1783421525979,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1570,"time":1783421525979,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"danger"}}} +{"type":"assistant/chunk","seq":1571,"time":1783421525979,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"-full"}}} +{"type":"assistant/chunk","seq":1572,"time":1783421525979,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"-access"}}} +{"type":"assistant/chunk","seq":1573,"time":1783421526008,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1574,"time":1783421526037,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1575,"time":1783421526037,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1576,"time":1783421526037,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"just"}}} +{"type":"assistant/chunk","seq":1577,"time":1783421526037,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"ification"}}} +{"type":"assistant/chunk","seq":1578,"time":1783421526037,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1579,"time":1783421526065,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1580,"time":1783421526065,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1581,"time":1783421526065,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"用户"}}} +{"type":"assistant/chunk","seq":1582,"time":1783421526095,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"要求在"}}} +{"type":"assistant/chunk","seq":1583,"time":1783421526095,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" home"}}} +{"type":"assistant/chunk","seq":1584,"time":1783421526095,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1585,"time":1783421526125,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"目录"}}} +{"type":"assistant/chunk","seq":1586,"time":1783421526125,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" (~"}}} +{"type":"assistant/chunk","seq":1587,"time":1783421526125,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":")"}}} +{"type":"assistant/chunk","seq":1588,"time":1783421526125,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1589,"time":1783421526125,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"下"}}} +{"type":"assistant/chunk","seq":1590,"time":1783421526125,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":1591,"time":1783421526153,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" f"}}} +{"type":"assistant/chunk","seq":1592,"time":1783421526153,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1593,"time":1783421526153,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1594,"time":1783421526153,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":1595,"time":1783421526153,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":","}}} +{"type":"assistant/chunk","seq":1596,"time":1783421526153,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"需要"}}} +{"type":"assistant/chunk","seq":1597,"time":1783421526181,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"完全"}}} +{"type":"assistant/chunk","seq":1598,"time":1783421526181,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"访问"}}} +{"type":"assistant/chunk","seq":1599,"time":1783421526211,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"权限"}}} +{"type":"assistant/chunk","seq":1600,"time":1783421526211,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"才能"}}} +{"type":"assistant/chunk","seq":1601,"time":1783421526211,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"写入"}}} +{"type":"assistant/chunk","seq":1602,"time":1783421526240,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" home"}}} +{"type":"assistant/chunk","seq":1603,"time":1783421526269,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1604,"time":1783421526269,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"目录"}}} +{"type":"assistant/chunk","seq":1605,"time":1783421526269,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"。"}}} +{"type":"assistant/chunk","seq":1606,"time":1783421526269,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1607,"time":1783421526298,"data":{"turn":7,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":1608,"time":1783421526331,"data":{"turn":7,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"用户又想在 home 目录下创建 f.md。之前的 e.md 因为权限被拒没有创建成功。现在 policy 改成了 \"ask\",所以我需要申请 danger-full-access 权限,用户会被询问是否批准。"}}}} +{"type":"assistant/chunk","seq":1609,"time":1783421526331,"data":{"turn":7,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","arguments":"{\"command\": \"touch ~/f.md\", \"description\": \"在 home 目录创建 f.md\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"用户要求在 home 目录 (~) 下创建 f.md 文件,需要完全访问权限才能写入 home 目录。\"}"}}}} +{"type":"assistant/chunk","seq":1610,"time":1783421526331,"data":{"turn":7,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3120,"outputTokens":176,"cacheReadTokens":0,"reasoningTokens":46}}}} +{"type":"assistant/chunk","seq":1611,"time":1783421526331,"data":{"turn":7,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":1612,"time":1783421526331,"data":{"turn":7,"step":1,"content":[{"type":"reasoning","text":"用户又想在 home 目录下创建 f.md。之前的 e.md 因为权限被拒没有创建成功。现在 policy 改成了 \"ask\",所以我需要申请 danger-full-access 权限,用户会被询问是否批准。"},{"type":"tool-call","id":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","arguments":"{\"command\": \"touch ~/f.md\", \"description\": \"在 home 目录创建 f.md\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"用户要求在 home 目录 (~) 下创建 f.md 文件,需要完全访问权限才能写入 home 目录。\"}"}],"usage":{"inputTokens":3120,"outputTokens":176,"cacheReadTokens":0,"reasoningTokens":46}},"sourceEventSeqs":[1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611],"surfaceOp":"append"} +{"type":"tool/call","seq":1613,"time":1783421526331,"data":{"turn":7,"step":1,"callId":"call_00_HSRl7qhjhdr20Nc8bdiY2510","name":"bash","arguments":"{\"command\": \"touch ~/f.md\", \"description\": \"在 home 目录创建 f.md\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"用户要求在 home 目录 (~) 下创建 f.md 文件,需要完全访问权限才能写入 home 目录。\"}"}} +{"type":"approval/asked","seq":1614,"time":1783421526332,"data":{"id":"d567b8a5-f25c-4317-a43f-8e9cac02df16","toolName":"bash","callId":"call_00_HSRl7qhjhdr20Nc8bdiY2510","reason":"escalate sandbox to danger-full-access: 用户要求在 home 目录 (~) 下创建 f.md 文件,需要完全访问权限才能写入 home 目录。"}} +{"type":"approval/decided","seq":1615,"time":1783421531141,"data":{"id":"d567b8a5-f25c-4317-a43f-8e9cac02df16","outcome":"allowed-once"}} +{"type":"tool/result","seq":1616,"time":1783421531215,"data":{"turn":7,"step":1,"callId":"call_00_HSRl7qhjhdr20Nc8bdiY2510","content":[{"type":"text","text":"(no output)"}],"isError":false},"sourceEventSeqs":[1613],"surfaceOp":"append"} +{"type":"step/end","seq":1617,"time":1783421531215,"data":{"turn":7,"step":1}} +{"type":"step/start","seq":1618,"time":1783421531216,"data":{"turn":7,"step":2}} +{"type":"assistant/chunk","seq":1619,"time":1783421531825,"data":{"turn":7,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":1620,"time":1783421531825,"data":{"turn":7,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"执行"}}} +{"type":"assistant/chunk","seq":1621,"time":1783421531942,"data":{"turn":7,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"成功"}}} +{"type":"assistant/chunk","seq":1622,"time":1783421531974,"data":{"turn":7,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":1623,"time":1783421531974,"data":{"turn":7,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"验证"}}} +{"type":"assistant/chunk","seq":1624,"time":1783421531999,"data":{"turn":7,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"一下"}}} +{"type":"assistant/chunk","seq":1625,"time":1783421532000,"data":{"turn":7,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1626,"time":1783421532057,"data":{"turn":7,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":1627,"time":1783421532057,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":1628,"time":1783421532086,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":1629,"time":1783421532086,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1630,"time":1783421532086,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":1631,"time":1783421532115,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1632,"time":1783421532116,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1633,"time":1783421532116,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1634,"time":1783421532116,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"验证"}}} +{"type":"assistant/chunk","seq":1635,"time":1783421532145,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":" f"}}} +{"type":"assistant/chunk","seq":1636,"time":1783421532145,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1637,"time":1783421532145,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1638,"time":1783421532145,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"已"}}} +{"type":"assistant/chunk","seq":1639,"time":1783421532173,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":1640,"time":1783421532174,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1641,"time":1783421532201,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1642,"time":1783421532201,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1643,"time":1783421532201,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":1644,"time":1783421532201,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1645,"time":1783421532230,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1646,"time":1783421532230,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1647,"time":1783421532231,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"ls"}}} +{"type":"assistant/chunk","seq":1648,"time":1783421532231,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":" -"}}} +{"type":"assistant/chunk","seq":1649,"time":1783421532276,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"la"}}} +{"type":"assistant/chunk","seq":1650,"time":1783421532276,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":" ~"}}} +{"type":"assistant/chunk","seq":1651,"time":1783421532277,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":1652,"time":1783421532277,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"f"}}} +{"type":"assistant/chunk","seq":1653,"time":1783421532277,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1654,"time":1783421532287,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1655,"time":1783421532288,"data":{"turn":7,"step":2,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":1656,"time":1783421532350,"data":{"turn":7,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"执行成功,验证一下。"}}}} +{"type":"assistant/chunk","seq":1657,"time":1783421532351,"data":{"turn":7,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","arguments":"{\"description\": \"验证 f.md 已创建\", \"command\": \"ls -la ~/f.md\"}"}}}} +{"type":"assistant/chunk","seq":1658,"time":1783421532351,"data":{"turn":7,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":110,"outputTokens":76,"cacheReadTokens":3200,"reasoningTokens":6}}}} +{"type":"assistant/chunk","seq":1659,"time":1783421532351,"data":{"turn":7,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":1660,"time":1783421532351,"data":{"turn":7,"step":2,"content":[{"type":"reasoning","text":"执行成功,验证一下。"},{"type":"tool-call","id":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","arguments":"{\"description\": \"验证 f.md 已创建\", \"command\": \"ls -la ~/f.md\"}"}],"usage":{"inputTokens":110,"outputTokens":76,"cacheReadTokens":3200,"reasoningTokens":6}},"sourceEventSeqs":[1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659],"surfaceOp":"append"} +{"type":"tool/call","seq":1661,"time":1783421532351,"data":{"turn":7,"step":2,"callId":"call_00_DHMK4ps74aFQdaCeYLCo9195","name":"bash","arguments":"{\"description\": \"验证 f.md 已创建\", \"command\": \"ls -la ~/f.md\"}"}} +{"type":"tool/result","seq":1662,"time":1783421532438,"data":{"turn":7,"step":2,"callId":"call_00_DHMK4ps74aFQdaCeYLCo9195","content":[{"type":"text","text":"-rw-r--r--@ 1 wwl staff 0 Jul 7 18:52 /Users/wwl/f.md\n"}],"isError":false},"sourceEventSeqs":[1661],"surfaceOp":"append"} +{"type":"step/end","seq":1663,"time":1783421532439,"data":{"turn":7,"step":2}} +{"type":"step/start","seq":1664,"time":1783421532439,"data":{"turn":7,"step":3}} +{"type":"assistant/chunk","seq":1665,"time":1783421533104,"data":{"turn":7,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":1666,"time":1783421533104,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"成功"}}} +{"type":"assistant/chunk","seq":1667,"time":1783421533247,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1668,"time":1783421533279,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"了"}}} +{"type":"assistant/chunk","seq":1669,"time":1783421533279,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" f"}}} +{"type":"assistant/chunk","seq":1670,"time":1783421533308,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1671,"time":1783421533309,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1672,"time":1783421533339,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"在"}}} +{"type":"assistant/chunk","seq":1673,"time":1783421533339,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" home"}}} +{"type":"assistant/chunk","seq":1674,"time":1783421533369,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1675,"time":1783421533369,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1676,"time":1783421533369,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"下"}}} +{"type":"assistant/chunk","seq":1677,"time":1783421533369,"data":{"turn":7,"step":3,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1678,"time":1783421533369,"data":{"turn":7,"step":3,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":1679,"time":1783421533369,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":1680,"time":1783421533397,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"f"}}} +{"type":"assistant/chunk","seq":1681,"time":1783421533397,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":1682,"time":1783421533397,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":1683,"time":1783421533397,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1684,"time":1783421533397,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"已"}}} +{"type":"assistant/chunk","seq":1685,"time":1783421533398,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"成功"}}} +{"type":"assistant/chunk","seq":1686,"time":1783421533427,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":1687,"time":1783421533456,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"在"}}} +{"type":"assistant/chunk","seq":1688,"time":1783421533456,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":" home"}}} +{"type":"assistant/chunk","seq":1689,"time":1783421533487,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1690,"time":1783421533487,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":1691,"time":1783421533487,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":" (`"}}} +{"type":"assistant/chunk","seq":1692,"time":1783421533487,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":1693,"time":1783421533514,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"Users"}}} +{"type":"assistant/chunk","seq":1694,"time":1783421533514,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":1695,"time":1783421533514,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"ww"}}} +{"type":"assistant/chunk","seq":1696,"time":1783421533514,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"l"}}} +{"type":"assistant/chunk","seq":1697,"time":1783421533514,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"/f"}}} +{"type":"assistant/chunk","seq":1698,"time":1783421533514,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":1699,"time":1783421533547,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"`)"}}} +{"type":"assistant/chunk","seq":1700,"time":1783421533547,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"!\n\n"}}} +{"type":"assistant/chunk","seq":1701,"time":1783421533547,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"顺便"}}} +{"type":"assistant/chunk","seq":1702,"time":1783421533575,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"提"}}} +{"type":"assistant/chunk","seq":1703,"time":1783421533605,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"一下"}}} +{"type":"assistant/chunk","seq":1704,"time":1783421533605,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":1705,"time":1783421533605,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"之前"}}} +{"type":"assistant/chunk","seq":1706,"time":1783421533605,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"未"}}} +{"type":"assistant/chunk","seq":1707,"time":1783421533638,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"成功的"}}} +{"type":"assistant/chunk","seq":1708,"time":1783421533638,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":1709,"time":1783421533638,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"e"}}} +{"type":"assistant/chunk","seq":1710,"time":1783421533638,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":1711,"time":1783421533638,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":1712,"time":1783421533638,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1713,"time":1783421533661,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"是否需要"}}} +{"type":"assistant/chunk","seq":1714,"time":1783421533661,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"一并"}}} +{"type":"assistant/chunk","seq":1715,"time":1783421533689,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"补"}}} +{"type":"assistant/chunk","seq":1716,"time":1783421533718,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"上"}}} +{"type":"assistant/chunk","seq":1717,"time":1783421533718,"data":{"turn":7,"step":3,"chunk":{"type":"text-delta","index":1,"text":"?"}}} +{"type":"assistant/chunk","seq":1718,"time":1783421533720,"data":{"turn":7,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"成功创建了 f.md 在 home 目录下。"}}}} +{"type":"assistant/chunk","seq":1719,"time":1783421533720,"data":{"turn":7,"step":3,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"`f.md` 已成功创建在 home 目录 (`/Users/wwl/f.md`)!\n\n顺便提一下,之前未成功的 `e.md` 是否需要一并补上?"}}}} +{"type":"assistant/chunk","seq":1720,"time":1783421533720,"data":{"turn":7,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":99,"outputTokens":52,"cacheReadTokens":3328,"reasoningTokens":12}}}} +{"type":"assistant/chunk","seq":1721,"time":1783421533720,"data":{"turn":7,"step":3,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":1722,"time":1783421533720,"data":{"turn":7,"step":3,"content":[{"type":"reasoning","text":"成功创建了 f.md 在 home 目录下。"},{"type":"text","text":"`f.md` 已成功创建在 home 目录 (`/Users/wwl/f.md`)!\n\n顺便提一下,之前未成功的 `e.md` 是否需要一并补上?"}],"usage":{"inputTokens":99,"outputTokens":52,"cacheReadTokens":3328,"reasoningTokens":12}},"sourceEventSeqs":[1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721],"surfaceOp":"append"} +{"type":"step/end","seq":1723,"time":1783421533720,"data":{"turn":7,"step":3}} +{"type":"turn/end","seq":1724,"time":1783421533720,"data":{"turn":7,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":1725,"time":1783421537087,"data":{"turn":8,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1726,"time":1783421537087,"data":{"content":[{"type":"text","text":"帮我在 ~ 创建一个 g.md"}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":1727,"time":1783421537087,"data":{"turn":8,"step":1}} +{"type":"assistant/chunk","seq":1728,"time":1783421537603,"data":{"turn":8,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":1729,"time":1783421537603,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":1730,"time":1783421537727,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"想在"}}} +{"type":"assistant/chunk","seq":1731,"time":1783421537756,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" home"}}} +{"type":"assistant/chunk","seq":1732,"time":1783421537757,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1733,"time":1783421537757,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1734,"time":1783421537757,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1735,"time":1783421537785,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" g"}}} +{"type":"assistant/chunk","seq":1736,"time":1783421537786,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1737,"time":1783421537786,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1738,"time":1783421537786,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"之前"}}} +{"type":"assistant/chunk","seq":1739,"time":1783421537815,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"申请"}}} +{"type":"assistant/chunk","seq":1740,"time":1783421537844,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" danger"}}} +{"type":"assistant/chunk","seq":1741,"time":1783421537844,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-full"}}} +{"type":"assistant/chunk","seq":1742,"time":1783421537874,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-access"}}} +{"type":"assistant/chunk","seq":1743,"time":1783421537874,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1744,"time":1783421537874,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"被"}}} +{"type":"assistant/chunk","seq":1745,"time":1783421537903,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"批准"}}} +{"type":"assistant/chunk","seq":1746,"time":1783421537903,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"了"}}} +{"type":"assistant/chunk","seq":1747,"time":1783421537903,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":1748,"time":1783421537903,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"这次"}}} +{"type":"assistant/chunk","seq":1749,"time":1783421537933,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"应该"}}} +{"type":"assistant/chunk","seq":1750,"time":1783421537933,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"也可以"}}} +{"type":"assistant/chunk","seq":1751,"time":1783421537973,"data":{"turn":8,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1752,"time":1783421538049,"data":{"turn":8,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":1753,"time":1783421538049,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":1754,"time":1783421538078,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":1755,"time":1783421538078,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1756,"time":1783421538078,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":1757,"time":1783421538078,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1758,"time":1783421538078,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1759,"time":1783421538107,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1760,"time":1783421538108,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"touch"}}} +{"type":"assistant/chunk","seq":1761,"time":1783421538108,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" ~"}}} +{"type":"assistant/chunk","seq":1762,"time":1783421538108,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":1763,"time":1783421538108,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"g"}}} +{"type":"assistant/chunk","seq":1764,"time":1783421538136,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1765,"time":1783421538137,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" &&"}}} +{"type":"assistant/chunk","seq":1766,"time":1783421538165,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" ls"}}} +{"type":"assistant/chunk","seq":1767,"time":1783421538165,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" -"}}} +{"type":"assistant/chunk","seq":1768,"time":1783421538165,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"la"}}} +{"type":"assistant/chunk","seq":1769,"time":1783421538194,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" ~"}}} +{"type":"assistant/chunk","seq":1770,"time":1783421538195,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":1771,"time":1783421538195,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"g"}}} +{"type":"assistant/chunk","seq":1772,"time":1783421538195,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1773,"time":1783421538195,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1774,"time":1783421538257,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1775,"time":1783421538257,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1776,"time":1783421538257,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":1777,"time":1783421538257,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1778,"time":1783421538257,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1779,"time":1783421538285,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1780,"time":1783421538285,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"在"}}} +{"type":"assistant/chunk","seq":1781,"time":1783421538285,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" home"}}} +{"type":"assistant/chunk","seq":1782,"time":1783421538286,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1783,"time":1783421538286,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"目录"}}} +{"type":"assistant/chunk","seq":1784,"time":1783421538286,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":1785,"time":1783421538315,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" g"}}} +{"type":"assistant/chunk","seq":1786,"time":1783421538315,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1787,"time":1783421538315,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1788,"time":1783421538344,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1789,"time":1783421538344,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1790,"time":1783421538344,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"sand"}}} +{"type":"assistant/chunk","seq":1791,"time":1783421538373,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"box"}}} +{"type":"assistant/chunk","seq":1792,"time":1783421538373,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"_per"}}} +{"type":"assistant/chunk","seq":1793,"time":1783421538373,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"missions"}}} +{"type":"assistant/chunk","seq":1794,"time":1783421538373,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1795,"time":1783421538373,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1796,"time":1783421538403,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1797,"time":1783421538403,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"danger"}}} +{"type":"assistant/chunk","seq":1798,"time":1783421538403,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"-full"}}} +{"type":"assistant/chunk","seq":1799,"time":1783421538403,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"-access"}}} +{"type":"assistant/chunk","seq":1800,"time":1783421538432,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1801,"time":1783421538461,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":1802,"time":1783421538461,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1803,"time":1783421538461,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"just"}}} +{"type":"assistant/chunk","seq":1804,"time":1783421538461,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"ification"}}} +{"type":"assistant/chunk","seq":1805,"time":1783421538461,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1806,"time":1783421538461,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1807,"time":1783421538490,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1808,"time":1783421538490,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"用户"}}} +{"type":"assistant/chunk","seq":1809,"time":1783421538519,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"要求在"}}} +{"type":"assistant/chunk","seq":1810,"time":1783421538520,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" home"}}} +{"type":"assistant/chunk","seq":1811,"time":1783421538520,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1812,"time":1783421538520,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"目录"}}} +{"type":"assistant/chunk","seq":1813,"time":1783421538520,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"下"}}} +{"type":"assistant/chunk","seq":1814,"time":1783421538549,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":1815,"time":1783421538549,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" g"}}} +{"type":"assistant/chunk","seq":1816,"time":1783421538549,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":1817,"time":1783421538549,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":" "}}} +{"type":"assistant/chunk","seq":1818,"time":1783421538549,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"文件"}}} +{"type":"assistant/chunk","seq":1819,"time":1783421538549,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":","}}} +{"type":"assistant/chunk","seq":1820,"time":1783421538579,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"需要"}}} +{"type":"assistant/chunk","seq":1821,"time":1783421538579,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"完全"}}} +{"type":"assistant/chunk","seq":1822,"time":1783421538580,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"访问"}}} +{"type":"assistant/chunk","seq":1823,"time":1783421538580,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"权限"}}} +{"type":"assistant/chunk","seq":1824,"time":1783421538608,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"。"}}} +{"type":"assistant/chunk","seq":1825,"time":1783421538608,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1826,"time":1783421538608,"data":{"turn":8,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":1827,"time":1783421538670,"data":{"turn":8,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"用户想在 home 目录创建 g.md。之前申请 danger-full-access 被批准了,这次应该也可以。"}}}} +{"type":"assistant/chunk","seq":1828,"time":1783421538671,"data":{"turn":8,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","arguments":"{\"command\": \"touch ~/g.md && ls -la ~/g.md\", \"description\": \"在 home 目录创建 g.md\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"用户要求在 home 目录下创建 g.md 文件,需要完全访问权限。\"}"}}}} +{"type":"assistant/chunk","seq":1829,"time":1783421538671,"data":{"turn":8,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":150,"outputTokens":153,"cacheReadTokens":3328,"reasoningTokens":23}}}} +{"type":"assistant/chunk","seq":1830,"time":1783421538671,"data":{"turn":8,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":1831,"time":1783421538671,"data":{"turn":8,"step":1,"content":[{"type":"reasoning","text":"用户想在 home 目录创建 g.md。之前申请 danger-full-access 被批准了,这次应该也可以。"},{"type":"tool-call","id":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","arguments":"{\"command\": \"touch ~/g.md && ls -la ~/g.md\", \"description\": \"在 home 目录创建 g.md\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"用户要求在 home 目录下创建 g.md 文件,需要完全访问权限。\"}"}],"usage":{"inputTokens":150,"outputTokens":153,"cacheReadTokens":3328,"reasoningTokens":23}},"sourceEventSeqs":[1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830],"surfaceOp":"append"} +{"type":"tool/call","seq":1832,"time":1783421538671,"data":{"turn":8,"step":1,"callId":"call_00_sv6ejgH8uXxp9Iy508TO6253","name":"bash","arguments":"{\"command\": \"touch ~/g.md && ls -la ~/g.md\", \"description\": \"在 home 目录创建 g.md\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"用户要求在 home 目录下创建 g.md 文件,需要完全访问权限。\"}"}} +{"type":"approval/asked","seq":1833,"time":1783421538671,"data":{"id":"c972f0f7-5b2b-4d52-9d33-b3318ff0d81a","toolName":"bash","callId":"call_00_sv6ejgH8uXxp9Iy508TO6253","reason":"escalate sandbox to danger-full-access: 用户要求在 home 目录下创建 g.md 文件,需要完全访问权限。"}} +{"type":"approval/decided","seq":1834,"time":1783421539711,"data":{"id":"c972f0f7-5b2b-4d52-9d33-b3318ff0d81a","outcome":"rejected"}} +{"type":"tool/result","seq":1835,"time":1783421539711,"data":{"turn":8,"step":1,"callId":"call_00_sv6ejgH8uXxp9Iy508TO6253","content":[{"type":"text","text":"Error: the user rejected escalating this command to \"danger-full-access\""}],"isError":true},"sourceEventSeqs":[1832],"surfaceOp":"append"} +{"type":"step/end","seq":1836,"time":1783421539711,"data":{"turn":8,"step":1}} +{"type":"step/start","seq":1837,"time":1783421539711,"data":{"turn":8,"step":2}} +{"type":"assistant/chunk","seq":1838,"time":1783421540359,"data":{"turn":8,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":1839,"time":1783421540359,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":1840,"time":1783421540460,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"拒绝了"}}} +{"type":"assistant/chunk","seq":1841,"time":1783421540485,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1842,"time":1783421540521,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"那"}}} +{"type":"assistant/chunk","seq":1843,"time":1783421540546,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"我在"}}} +{"type":"assistant/chunk","seq":1844,"time":1783421540602,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" workspace"}}} +{"type":"assistant/chunk","seq":1845,"time":1783421540604,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-w"}}} +{"type":"assistant/chunk","seq":1846,"time":1783421540604,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"rite"}}} +{"type":"assistant/chunk","seq":1847,"time":1783421540605,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1848,"time":1783421540605,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"范围内"}}} +{"type":"assistant/chunk","seq":1849,"time":1783421540633,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"能"}}} +{"type":"assistant/chunk","seq":1850,"time":1783421540662,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"做什么"}}} +{"type":"assistant/chunk","seq":1851,"time":1783421540691,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"呢"}}} +{"type":"assistant/chunk","seq":1852,"time":1783421540691,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"?"}}} +{"type":"assistant/chunk","seq":1853,"time":1783421540691,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"works"}}} +{"type":"assistant/chunk","seq":1854,"time":1783421540720,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"pace"}}} +{"type":"assistant/chunk","seq":1855,"time":1783421540720,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-w"}}} +{"type":"assistant/chunk","seq":1856,"time":1783421540720,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"rite"}}} +{"type":"assistant/chunk","seq":1857,"time":1783421540720,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1858,"time":1783421540720,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"只"}}} +{"type":"assistant/chunk","seq":1859,"time":1783421540748,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"允许"}}} +{"type":"assistant/chunk","seq":1860,"time":1783421540748,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"在工作"}}} +{"type":"assistant/chunk","seq":1861,"time":1783421540785,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1862,"time":1783421540785,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"下"}}} +{"type":"assistant/chunk","seq":1863,"time":1783421540785,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"写入"}}} +{"type":"assistant/chunk","seq":1864,"time":1783421540806,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1865,"time":1783421540806,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"所以"}}} +{"type":"assistant/chunk","seq":1866,"time":1783421540835,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"我可以"}}} +{"type":"assistant/chunk","seq":1867,"time":1783421540835,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"问"}}} +{"type":"assistant/chunk","seq":1868,"time":1783421540864,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":1869,"time":1783421540864,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"是否"}}} +{"type":"assistant/chunk","seq":1870,"time":1783421540908,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"要在"}}} +{"type":"assistant/chunk","seq":1871,"time":1783421540922,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"工作"}}} +{"type":"assistant/chunk","seq":1872,"time":1783421540922,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1873,"time":1783421540922,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"下"}}} +{"type":"assistant/chunk","seq":1874,"time":1783421540922,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1875,"time":1783421540922,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" g"}}} +{"type":"assistant/chunk","seq":1876,"time":1783421540951,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1877,"time":1783421540951,"data":{"turn":8,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1878,"time":1783421540951,"data":{"turn":8,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":1879,"time":1783421540951,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"权限"}}} +{"type":"assistant/chunk","seq":1880,"time":1783421540980,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"申请"}}} +{"type":"assistant/chunk","seq":1881,"time":1783421541009,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"被"}}} +{"type":"assistant/chunk","seq":1882,"time":1783421541009,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"拒绝了"}}} +{"type":"assistant/chunk","seq":1883,"time":1783421541009,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":1884,"time":1783421541009,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"我"}}} +{"type":"assistant/chunk","seq":1885,"time":1783421541037,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"无法"}}} +{"type":"assistant/chunk","seq":1886,"time":1783421541037,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"在"}}} +{"type":"assistant/chunk","seq":1887,"time":1783421541037,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":" home"}}} +{"type":"assistant/chunk","seq":1888,"time":1783421541067,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1889,"time":1783421541068,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":1890,"time":1783421541068,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"下"}}} +{"type":"assistant/chunk","seq":1891,"time":1783421541068,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":1892,"time":1783421541068,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":1893,"time":1783421541068,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"g"}}} +{"type":"assistant/chunk","seq":1894,"time":1783421541098,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":1895,"time":1783421541098,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":1896,"time":1783421541098,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"。\n\n"}}} +{"type":"assistant/chunk","seq":1897,"time":1783421541098,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"如果你"}}} +{"type":"assistant/chunk","seq":1898,"time":1783421541127,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"愿意"}}} +{"type":"assistant/chunk","seq":1899,"time":1783421541165,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":1900,"time":1783421541165,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"我可以"}}} +{"type":"assistant/chunk","seq":1901,"time":1783421541165,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"把"}}} +{"type":"assistant/chunk","seq":1902,"time":1783421541185,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":1903,"time":1783421541185,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"g"}}} +{"type":"assistant/chunk","seq":1904,"time":1783421541185,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":1905,"time":1783421541185,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":1906,"time":1783421541185,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1907,"time":1783421541185,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":1908,"time":1783421541214,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"到"}}} +{"type":"assistant/chunk","seq":1909,"time":1783421541214,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"当前"}}} +{"type":"assistant/chunk","seq":1910,"time":1783421541214,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":1911,"time":1783421541242,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"工作"}}} +{"type":"assistant/chunk","seq":1912,"time":1783421541242,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":1913,"time":1783421541242,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"**"}}} +{"type":"assistant/chunk","seq":1914,"time":1783421541242,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":" (`"}}} +{"type":"assistant/chunk","seq":1915,"time":1783421541272,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":1916,"time":1783421541272,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"Users"}}} +{"type":"assistant/chunk","seq":1917,"time":1783421541272,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":1918,"time":1783421541272,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ww"}}} +{"type":"assistant/chunk","seq":1919,"time":1783421541311,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"l"}}} +{"type":"assistant/chunk","seq":1920,"time":1783421541311,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":1921,"time":1783421541311,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"works"}}} +{"type":"assistant/chunk","seq":1922,"time":1783421541311,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"pace"}}} +{"type":"assistant/chunk","seq":1923,"time":1783421541311,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"/de"}}} +{"type":"assistant/chunk","seq":1924,"time":1783421541311,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ep"}}} +{"type":"assistant/chunk","seq":1925,"time":1783421541329,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"seek"}}} +{"type":"assistant/chunk","seq":1926,"time":1783421541329,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"-h"}}} +{"type":"assistant/chunk","seq":1927,"time":1783421541329,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ar"}}} +{"type":"assistant/chunk","seq":1928,"time":1783421541329,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ness"}}} +{"type":"assistant/chunk","seq":1929,"time":1783421541329,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"/"}}} +{"type":"assistant/chunk","seq":1930,"time":1783421541329,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`)"}}} +{"type":"assistant/chunk","seq":1931,"time":1783421541358,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":1932,"time":1783421541358,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"下"}}} +{"type":"assistant/chunk","seq":1933,"time":1783421541358,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":1934,"time":1783421541358,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"需要"}}} +{"type":"assistant/chunk","seq":1935,"time":1783421541387,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"吗"}}} +{"type":"assistant/chunk","seq":1936,"time":1783421541387,"data":{"turn":8,"step":2,"chunk":{"type":"text-delta","index":1,"text":"?"}}} +{"type":"assistant/chunk","seq":1937,"time":1783421541387,"data":{"turn":8,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"用户拒绝了。那我在 workspace-write 范围内能做什么呢?workspace-write 只允许在工作目录下写入。所以我可以问用户是否要在工作目录下创建 g.md。"}}}} +{"type":"assistant/chunk","seq":1938,"time":1783421541388,"data":{"turn":8,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"权限申请被拒绝了,我无法在 home 目录下创建 `g.md`。\n\n如果你愿意,我可以把 `g.md` 创建到当前**工作目录** (`/Users/wwl/workspace/deepseek-harness/`) 下,需要吗?"}}}} +{"type":"assistant/chunk","seq":1939,"time":1783421541388,"data":{"turn":8,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":73,"outputTokens":98,"cacheReadTokens":3584,"reasoningTokens":39}}}} +{"type":"assistant/chunk","seq":1940,"time":1783421541388,"data":{"turn":8,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":1941,"time":1783421541388,"data":{"turn":8,"step":2,"content":[{"type":"reasoning","text":"用户拒绝了。那我在 workspace-write 范围内能做什么呢?workspace-write 只允许在工作目录下写入。所以我可以问用户是否要在工作目录下创建 g.md。"},{"type":"text","text":"权限申请被拒绝了,我无法在 home 目录下创建 `g.md`。\n\n如果你愿意,我可以把 `g.md` 创建到当前**工作目录** (`/Users/wwl/workspace/deepseek-harness/`) 下,需要吗?"}],"usage":{"inputTokens":73,"outputTokens":98,"cacheReadTokens":3584,"reasoningTokens":39}},"sourceEventSeqs":[1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940],"surfaceOp":"append"} +{"type":"step/end","seq":1942,"time":1783421541388,"data":{"turn":8,"step":2}} +{"type":"turn/end","seq":1943,"time":1783421541388,"data":{"turn":8,"reason":{"kind":"completed"}}} +{"type":"turn/start","seq":1944,"time":1783421552564,"data":{"turn":9,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"bash/sandbox-mode","seq":1945,"time":1783421552564,"data":{"mode":"danger-full-access"}} +{"type":"user/message","seq":1946,"time":1783421552564,"data":{"content":[{"type":"text","text":"创建一个 h.md"}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"context/message","seq":1947,"time":1783421552564,"data":{"content":[{"type":"text","text":"The bash sandbox mode changed from \"workspace-write\" to \"danger-full-access\" (changed by the user)."}],"source":{"kind":"plugin","plugin":"tool-bash"}},"surfaceOp":"append"} +{"type":"step/start","seq":1948,"time":1783421552564,"data":{"turn":9,"step":1}} +{"type":"request/header-delta","seq":1949,"time":1783421552564,"data":{"system":{"keepStart":12,"keepEnd":0,"insert":["Bash commands run under the \"danger-full-access\" file sandbox."]}}} +{"type":"assistant/chunk","seq":1950,"time":1783421553289,"data":{"turn":9,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":1951,"time":1783421553290,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"现在"}}} +{"type":"assistant/chunk","seq":1952,"time":1783421553399,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"沙"}}} +{"type":"assistant/chunk","seq":1953,"time":1783421553429,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"盒"}}} +{"type":"assistant/chunk","seq":1954,"time":1783421553429,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"模式"}}} +{"type":"assistant/chunk","seq":1955,"time":1783421553429,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"已经是"}}} +{"type":"assistant/chunk","seq":1956,"time":1783421553429,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" danger"}}} +{"type":"assistant/chunk","seq":1957,"time":1783421553429,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-full"}}} +{"type":"assistant/chunk","seq":1958,"time":1783421553458,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"-access"}}} +{"type":"assistant/chunk","seq":1959,"time":1783421553458,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":1960,"time":1783421553458,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"了"}}} +{"type":"assistant/chunk","seq":1961,"time":1783421553458,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":1962,"time":1783421553458,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"可以直接"}}} +{"type":"assistant/chunk","seq":1963,"time":1783421553490,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"在工作"}}} +{"type":"assistant/chunk","seq":1964,"time":1783421553491,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"目录"}}} +{"type":"assistant/chunk","seq":1965,"time":1783421553520,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"下"}}} +{"type":"assistant/chunk","seq":1966,"time":1783421553521,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":1967,"time":1783421553542,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" h"}}} +{"type":"assistant/chunk","seq":1968,"time":1783421553571,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":1969,"time":1783421553571,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":1970,"time":1783421553571,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"无需"}}} +{"type":"assistant/chunk","seq":1971,"time":1783421553571,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"再次"}}} +{"type":"assistant/chunk","seq":1972,"time":1783421553600,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"申请"}}} +{"type":"assistant/chunk","seq":1973,"time":1783421553600,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"权限"}}} +{"type":"assistant/chunk","seq":1974,"time":1783421553600,"data":{"turn":9,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":1975,"time":1783421553685,"data":{"turn":9,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":1976,"time":1783421553685,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":1977,"time":1783421553713,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":1978,"time":1783421553714,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1979,"time":1783421553714,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"command"}}} +{"type":"assistant/chunk","seq":1980,"time":1783421553714,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1981,"time":1783421553714,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":1982,"time":1783421553742,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":1983,"time":1783421553743,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"touch"}}} +{"type":"assistant/chunk","seq":1984,"time":1783421553743,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":" /"}}} +{"type":"assistant/chunk","seq":1985,"time":1783421553743,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"Users"}}} +{"type":"assistant/chunk","seq":1986,"time":1783421553743,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":1987,"time":1783421553775,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"ww"}}} +{"type":"assistant/chunk","seq":1988,"time":1783421553775,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"l"}}} +{"type":"assistant/chunk","seq":1989,"time":1783421553775,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":1990,"time":1783421553775,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"works"}}} +{"type":"assistant/chunk","seq":1991,"time":1783421553775,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"pace"}}} +{"type":"assistant/chunk","seq":1992,"time":1783421553776,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"/de"}}} +{"type":"assistant/chunk","seq":1993,"time":1783421553803,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"ep"}}} +{"type":"assistant/chunk","seq":1994,"time":1783421553803,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"seek"}}} +{"type":"assistant/chunk","seq":1995,"time":1783421553803,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"-h"}}} +{"type":"assistant/chunk","seq":1996,"time":1783421553803,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"ar"}}} +{"type":"assistant/chunk","seq":1997,"time":1783421553803,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"ness"}}} +{"type":"assistant/chunk","seq":1998,"time":1783421553803,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"/h"}}} +{"type":"assistant/chunk","seq":1999,"time":1783421553831,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":2000,"time":1783421553831,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":" &&"}}} +{"type":"assistant/chunk","seq":2001,"time":1783421553832,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":" ls"}}} +{"type":"assistant/chunk","seq":2002,"time":1783421553832,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":" -"}}} +{"type":"assistant/chunk","seq":2003,"time":1783421553832,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"la"}}} +{"type":"assistant/chunk","seq":2004,"time":1783421553832,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":" /"}}} +{"type":"assistant/chunk","seq":2005,"time":1783421553859,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"Users"}}} +{"type":"assistant/chunk","seq":2006,"time":1783421553859,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":2007,"time":1783421553859,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"ww"}}} +{"type":"assistant/chunk","seq":2008,"time":1783421553859,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"l"}}} +{"type":"assistant/chunk","seq":2009,"time":1783421553859,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"/"}}} +{"type":"assistant/chunk","seq":2010,"time":1783421553859,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"works"}}} +{"type":"assistant/chunk","seq":2011,"time":1783421553887,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"pace"}}} +{"type":"assistant/chunk","seq":2012,"time":1783421553887,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"/de"}}} +{"type":"assistant/chunk","seq":2013,"time":1783421553887,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"ep"}}} +{"type":"assistant/chunk","seq":2014,"time":1783421553887,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"seek"}}} +{"type":"assistant/chunk","seq":2015,"time":1783421553887,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"-h"}}} +{"type":"assistant/chunk","seq":2016,"time":1783421553888,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"ar"}}} +{"type":"assistant/chunk","seq":2017,"time":1783421553916,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"ness"}}} +{"type":"assistant/chunk","seq":2018,"time":1783421553916,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"/h"}}} +{"type":"assistant/chunk","seq":2019,"time":1783421553916,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":2020,"time":1783421553916,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":2021,"time":1783421553946,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":2022,"time":1783421553946,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":2023,"time":1783421553972,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"description"}}} +{"type":"assistant/chunk","seq":2024,"time":1783421553972,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":2025,"time":1783421553972,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":2026,"time":1783421553973,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":2027,"time":1783421554001,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"在工作"}}} +{"type":"assistant/chunk","seq":2028,"time":1783421554001,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"目录"}}} +{"type":"assistant/chunk","seq":2029,"time":1783421554001,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"创建"}}} +{"type":"assistant/chunk","seq":2030,"time":1783421554030,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":" h"}}} +{"type":"assistant/chunk","seq":2031,"time":1783421554030,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":2032,"time":1783421554030,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":2033,"time":1783421554058,"data":{"turn":9,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":2034,"time":1783421554089,"data":{"turn":9,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"现在沙盒模式已经是 danger-full-access 了,可以直接在工作目录下创建 h.md,无需再次申请权限。"}}}} +{"type":"assistant/chunk","seq":2035,"time":1783421554089,"data":{"turn":9,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/h.md && ls -la /Users/wwl/workspace/deepseek-harness/h.md\", \"description\": \"在工作目录创建 h.md\"}"}}}} +{"type":"assistant/chunk","seq":2036,"time":1783421554089,"data":{"turn":9,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3757,"outputTokens":123,"cacheReadTokens":0,"reasoningTokens":24}}}} +{"type":"assistant/chunk","seq":2037,"time":1783421554089,"data":{"turn":9,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":2038,"time":1783421554090,"data":{"turn":9,"step":1,"content":[{"type":"reasoning","text":"现在沙盒模式已经是 danger-full-access 了,可以直接在工作目录下创建 h.md,无需再次申请权限。"},{"type":"tool-call","id":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/h.md && ls -la /Users/wwl/workspace/deepseek-harness/h.md\", \"description\": \"在工作目录创建 h.md\"}"}],"usage":{"inputTokens":3757,"outputTokens":123,"cacheReadTokens":0,"reasoningTokens":24}},"sourceEventSeqs":[1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037],"surfaceOp":"append"} +{"type":"tool/call","seq":2039,"time":1783421554090,"data":{"turn":9,"step":1,"callId":"call_00_GhMY0hUKjd1feclqlu6E0395","name":"bash","arguments":"{\"command\": \"touch /Users/wwl/workspace/deepseek-harness/h.md && ls -la /Users/wwl/workspace/deepseek-harness/h.md\", \"description\": \"在工作目录创建 h.md\"}"}} +{"type":"tool/result","seq":2040,"time":1783421554171,"data":{"turn":9,"step":1,"callId":"call_00_GhMY0hUKjd1feclqlu6E0395","content":[{"type":"text","text":"-rw-r--r--@ 1 wwl staff 0 Jul 7 18:52 /Users/wwl/workspace/deepseek-harness/h.md\n"}],"isError":false},"sourceEventSeqs":[2039],"surfaceOp":"append"} +{"type":"step/end","seq":2041,"time":1783421554171,"data":{"turn":9,"step":1}} +{"type":"step/start","seq":2042,"time":1783421554172,"data":{"turn":9,"step":2}} +{"type":"assistant/chunk","seq":2043,"time":1783421554559,"data":{"turn":9,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":2044,"time":1783421554560,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"创建"}}} +{"type":"assistant/chunk","seq":2045,"time":1783421554661,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"成功"}}} +{"type":"assistant/chunk","seq":2046,"time":1783421554694,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":2047,"time":1783421554694,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"既然"}}} +{"type":"assistant/chunk","seq":2048,"time":1783421554718,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"现在是"}}} +{"type":"assistant/chunk","seq":2049,"time":1783421554746,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" danger"}}} +{"type":"assistant/chunk","seq":2050,"time":1783421554746,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-full"}}} +{"type":"assistant/chunk","seq":2051,"time":1783421554746,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-access"}}} +{"type":"assistant/chunk","seq":2052,"time":1783421554746,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":2053,"time":1783421554746,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"模式"}}} +{"type":"assistant/chunk","seq":2054,"time":1783421554746,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} +{"type":"assistant/chunk","seq":2055,"time":1783421554774,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"我可以"}}} +{"type":"assistant/chunk","seq":2056,"time":1783421554774,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"顺便"}}} +{"type":"assistant/chunk","seq":2057,"time":1783421554802,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"问"}}} +{"type":"assistant/chunk","seq":2058,"time":1783421554831,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"一下"}}} +{"type":"assistant/chunk","seq":2059,"time":1783421554831,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"用户"}}} +{"type":"assistant/chunk","seq":2060,"time":1783421554831,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"是否"}}} +{"type":"assistant/chunk","seq":2061,"time":1783421554860,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"要"}}} +{"type":"assistant/chunk","seq":2062,"time":1783421554888,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"补"}}} +{"type":"assistant/chunk","seq":2063,"time":1783421554888,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"上"}}} +{"type":"assistant/chunk","seq":2064,"time":1783421554888,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"之前"}}} +{"type":"assistant/chunk","seq":2065,"time":1783421554921,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"没"}}} +{"type":"assistant/chunk","seq":2066,"time":1783421554921,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"成功的"}}} +{"type":"assistant/chunk","seq":2067,"time":1783421554948,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" b"}}} +{"type":"assistant/chunk","seq":2068,"time":1783421554948,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":2069,"time":1783421554948,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"、"}}} +{"type":"assistant/chunk","seq":2070,"time":1783421554977,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"c"}}} +{"type":"assistant/chunk","seq":2071,"time":1783421554977,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":2072,"time":1783421554977,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"、"}}} +{"type":"assistant/chunk","seq":2073,"time":1783421555005,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"e"}}} +{"type":"assistant/chunk","seq":2074,"time":1783421555005,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":2075,"time":1783421555005,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" "}}} +{"type":"assistant/chunk","seq":2076,"time":1783421555005,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"和"}}} +{"type":"assistant/chunk","seq":2077,"time":1783421555034,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" g"}}} +{"type":"assistant/chunk","seq":2078,"time":1783421555034,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".md"}}} +{"type":"assistant/chunk","seq":2079,"time":1783421555034,"data":{"turn":9,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"。"}}} +{"type":"assistant/chunk","seq":2080,"time":1783421555034,"data":{"turn":9,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":2081,"time":1783421555034,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":2082,"time":1783421555034,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"h"}}} +{"type":"assistant/chunk","seq":2083,"time":1783421555063,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":2084,"time":1783421555063,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":2085,"time":1783421555063,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2086,"time":1783421555063,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"已"}}} +{"type":"assistant/chunk","seq":2087,"time":1783421555063,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"成功"}}} +{"type":"assistant/chunk","seq":2088,"time":1783421555063,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":2089,"time":1783421555091,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"!"}}} +{"type":"assistant/chunk","seq":2090,"time":1783421555120,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"现在"}}} +{"type":"assistant/chunk","seq":2091,"time":1783421555120,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"权限"}}} +{"type":"assistant/chunk","seq":2092,"time":1783421555148,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"已"}}} +{"type":"assistant/chunk","seq":2093,"time":1783421555176,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"提升"}}} +{"type":"assistant/chunk","seq":2094,"time":1783421555204,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"为"}}} +{"type":"assistant/chunk","seq":2095,"time":1783421555204,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":2096,"time":1783421555205,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"danger"}}} +{"type":"assistant/chunk","seq":2097,"time":1783421555205,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"-full"}}} +{"type":"assistant/chunk","seq":2098,"time":1783421555205,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"-access"}}} +{"type":"assistant/chunk","seq":2099,"time":1783421555233,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":2100,"time":1783421555233,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":","}}} +{"type":"assistant/chunk","seq":2101,"time":1783421555233,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"我可以"}}} +{"type":"assistant/chunk","seq":2102,"time":1783421555262,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"自由"}}} +{"type":"assistant/chunk","seq":2103,"time":1783421555290,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"读写"}}} +{"type":"assistant/chunk","seq":2104,"time":1783421555319,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"任何"}}} +{"type":"assistant/chunk","seq":2105,"time":1783421555347,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"位置"}}} +{"type":"assistant/chunk","seq":2106,"time":1783421555347,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"了"}}} +{"type":"assistant/chunk","seq":2107,"time":1783421555377,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"。\n\n"}}} +{"type":"assistant/chunk","seq":2108,"time":1783421555377,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"需要"}}} +{"type":"assistant/chunk","seq":2109,"time":1783421555377,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"我"}}} +{"type":"assistant/chunk","seq":2110,"time":1783421555406,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"帮你"}}} +{"type":"assistant/chunk","seq":2111,"time":1783421555406,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"把"}}} +{"type":"assistant/chunk","seq":2112,"time":1783421555432,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"之前"}}} +{"type":"assistant/chunk","seq":2113,"time":1783421555432,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"遗漏"}}} +{"type":"assistant/chunk","seq":2114,"time":1783421555461,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"的"}}} +{"type":"assistant/chunk","seq":2115,"time":1783421555461,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"也"}}} +{"type":"assistant/chunk","seq":2116,"time":1783421555492,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"一并"}}} +{"type":"assistant/chunk","seq":2117,"time":1783421555493,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"补"}}} +{"type":"assistant/chunk","seq":2118,"time":1783421555493,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"上"}}} +{"type":"assistant/chunk","seq":2119,"time":1783421555493,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"吗"}}} +{"type":"assistant/chunk","seq":2120,"time":1783421555524,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"?\n\n"}}} +{"type":"assistant/chunk","seq":2121,"time":1783421555524,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"|"}}} +{"type":"assistant/chunk","seq":2122,"time":1783421555550,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2123,"time":1783421555550,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"文件"}}} +{"type":"assistant/chunk","seq":2124,"time":1783421555550,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |"}}} +{"type":"assistant/chunk","seq":2125,"time":1783421555550,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2126,"time":1783421555550,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"位置"}}} +{"type":"assistant/chunk","seq":2127,"time":1783421555579,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |"}}} +{"type":"assistant/chunk","seq":2128,"time":1783421555579,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2129,"time":1783421555579,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"状态"}}} +{"type":"assistant/chunk","seq":2130,"time":1783421555579,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |\n"}}} +{"type":"assistant/chunk","seq":2131,"time":1783421555579,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"|"}}} +{"type":"assistant/chunk","seq":2132,"time":1783421555579,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"------"}}} +{"type":"assistant/chunk","seq":2133,"time":1783421555607,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"|"}}} +{"type":"assistant/chunk","seq":2134,"time":1783421555608,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"------"}}} +{"type":"assistant/chunk","seq":2135,"time":1783421555608,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"|"}}} +{"type":"assistant/chunk","seq":2136,"time":1783421555608,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"------"}}} +{"type":"assistant/chunk","seq":2137,"time":1783421555608,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"|\n"}}} +{"type":"assistant/chunk","seq":2138,"time":1783421555608,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"|"}}} +{"type":"assistant/chunk","seq":2139,"time":1783421555636,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":2140,"time":1783421555636,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"b"}}} +{"type":"assistant/chunk","seq":2141,"time":1783421555636,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":2142,"time":1783421555636,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":2143,"time":1783421555636,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |"}}} +{"type":"assistant/chunk","seq":2144,"time":1783421555636,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2145,"time":1783421555664,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"工作"}}} +{"type":"assistant/chunk","seq":2146,"time":1783421555664,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":2147,"time":1783421555664,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |"}}} +{"type":"assistant/chunk","seq":2148,"time":1783421555664,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" ❌"}}} +{"type":"assistant/chunk","seq":2149,"time":1783421555664,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2150,"time":1783421555700,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"未"}}} +{"type":"assistant/chunk","seq":2151,"time":1783421555700,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":2152,"time":1783421555700,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |\n"}}} +{"type":"assistant/chunk","seq":2153,"time":1783421555700,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"|"}}} +{"type":"assistant/chunk","seq":2154,"time":1783421555700,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":2155,"time":1783421555700,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"c"}}} +{"type":"assistant/chunk","seq":2156,"time":1783421555729,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":2157,"time":1783421555729,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":2158,"time":1783421555729,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |"}}} +{"type":"assistant/chunk","seq":2159,"time":1783421555729,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2160,"time":1783421555729,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"工作"}}} +{"type":"assistant/chunk","seq":2161,"time":1783421555729,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":2162,"time":1783421555752,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |"}}} +{"type":"assistant/chunk","seq":2163,"time":1783421555752,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" ❌"}}} +{"type":"assistant/chunk","seq":2164,"time":1783421555752,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2165,"time":1783421555752,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"未"}}} +{"type":"assistant/chunk","seq":2166,"time":1783421555752,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":2167,"time":1783421555787,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |\n"}}} +{"type":"assistant/chunk","seq":2168,"time":1783421555787,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"|"}}} +{"type":"assistant/chunk","seq":2169,"time":1783421555787,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":2170,"time":1783421555787,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"e"}}} +{"type":"assistant/chunk","seq":2171,"time":1783421555787,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":2172,"time":1783421555787,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":2173,"time":1783421555808,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |"}}} +{"type":"assistant/chunk","seq":2174,"time":1783421555808,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" home"}}} +{"type":"assistant/chunk","seq":2175,"time":1783421555808,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2176,"time":1783421555808,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":2177,"time":1783421555808,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |"}}} +{"type":"assistant/chunk","seq":2178,"time":1783421555836,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" ❌"}}} +{"type":"assistant/chunk","seq":2179,"time":1783421555836,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2180,"time":1783421555836,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"未"}}} +{"type":"assistant/chunk","seq":2181,"time":1783421555836,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":2182,"time":1783421555837,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |\n"}}} +{"type":"assistant/chunk","seq":2183,"time":1783421555837,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"|"}}} +{"type":"assistant/chunk","seq":2184,"time":1783421555869,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" `"}}} +{"type":"assistant/chunk","seq":2185,"time":1783421555869,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"g"}}} +{"type":"assistant/chunk","seq":2186,"time":1783421555869,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":".md"}}} +{"type":"assistant/chunk","seq":2187,"time":1783421555869,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"`"}}} +{"type":"assistant/chunk","seq":2188,"time":1783421555869,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |"}}} +{"type":"assistant/chunk","seq":2189,"time":1783421555869,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" home"}}} +{"type":"assistant/chunk","seq":2190,"time":1783421555905,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2191,"time":1783421555905,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"目录"}}} +{"type":"assistant/chunk","seq":2192,"time":1783421555905,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |"}}} +{"type":"assistant/chunk","seq":2193,"time":1783421555905,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" ❌"}}} +{"type":"assistant/chunk","seq":2194,"time":1783421555905,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" "}}} +{"type":"assistant/chunk","seq":2195,"time":1783421555930,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"未"}}} +{"type":"assistant/chunk","seq":2196,"time":1783421555930,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"创建"}}} +{"type":"assistant/chunk","seq":2197,"time":1783421555930,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":" |\n\n"}}} +{"type":"assistant/chunk","seq":2198,"time":1783421555930,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"要"}}} +{"type":"assistant/chunk","seq":2199,"time":1783421555965,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"一口气"}}} +{"type":"assistant/chunk","seq":2200,"time":1783421555977,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"全"}}} +{"type":"assistant/chunk","seq":2201,"time":1783421556008,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"搞定"}}} +{"type":"assistant/chunk","seq":2202,"time":1783421556008,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"吗"}}} +{"type":"assistant/chunk","seq":2203,"time":1783421556008,"data":{"turn":9,"step":2,"chunk":{"type":"text-delta","index":1,"text":"?"}}} +{"type":"assistant/chunk","seq":2204,"time":1783421556009,"data":{"turn":9,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"创建成功。既然现在是 danger-full-access 模式,我可以顺便问一下用户是否要补上之前没成功的 b.md、c.md、e.md 和 g.md。"}}}} +{"type":"assistant/chunk","seq":2205,"time":1783421556009,"data":{"turn":9,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"`h.md` 已成功创建!现在权限已提升为 `danger-full-access`,我可以自由读写任何位置了。\n\n需要我帮你把之前遗漏的也一并补上吗?\n\n| 文件 | 位置 | 状态 |\n|------|------|------|\n| `b.md` | 工作目录 | ❌ 未创建 |\n| `c.md` | 工作目录 | ❌ 未创建 |\n| `e.md` | home 目录 | ❌ 未创建 |\n| `g.md` | home 目录 | ❌ 未创建 |\n\n要一口气全搞定吗?"}}}} +{"type":"assistant/chunk","seq":2206,"time":1783421556009,"data":{"turn":9,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":90,"outputTokens":164,"cacheReadTokens":3840,"reasoningTokens":36}}}} +{"type":"assistant/chunk","seq":2207,"time":1783421556009,"data":{"turn":9,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":2208,"time":1783421556009,"data":{"turn":9,"step":2,"content":[{"type":"reasoning","text":"创建成功。既然现在是 danger-full-access 模式,我可以顺便问一下用户是否要补上之前没成功的 b.md、c.md、e.md 和 g.md。"},{"type":"text","text":"`h.md` 已成功创建!现在权限已提升为 `danger-full-access`,我可以自由读写任何位置了。\n\n需要我帮你把之前遗漏的也一并补上吗?\n\n| 文件 | 位置 | 状态 |\n|------|------|------|\n| `b.md` | 工作目录 | ❌ 未创建 |\n| `c.md` | 工作目录 | ❌ 未创建 |\n| `e.md` | home 目录 | ❌ 未创建 |\n| `g.md` | home 目录 | ❌ 未创建 |\n\n要一口气全搞定吗?"}],"usage":{"inputTokens":90,"outputTokens":164,"cacheReadTokens":3840,"reasoningTokens":36}},"sourceEventSeqs":[2043,2044,2045,2046,2047,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090,2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105,2106,2107,2108,2109,2110,2111,2112,2113,2114,2115,2116,2117,2118,2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2137,2138,2139,2140,2141,2142,2143,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198,2199,2200,2201,2202,2203,2204,2205,2206,2207],"surfaceOp":"append"} +{"type":"step/end","seq":2209,"time":1783421556009,"data":{"turn":9,"step":2}} +{"type":"turn/end","seq":2210,"time":1783421556009,"data":{"turn":9,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl index 25a6f76411..26519d458e 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"/tmp/advanced-acp","parentSession":"11111111-1111-4111-8111-111111111111"} +{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"/tmp/advanced-acp","parentSession":"11111111-1111-4111-8111-111111111111","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783957884563,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783957884563,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783957884564,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl index 45d9043a4a..9daa8958dc 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"/tmp/advanced-acp","parentSession":"11111111-1111-4111-8111-111111111111"} +{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"/tmp/advanced-acp","parentSession":"11111111-1111-4111-8111-111111111111","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783957884700,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783957884700,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783957884700,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl index 39f867984a..46c8e41257 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1783950000000,"cwd":"/tmp/advanced-acp"} +{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1783950000000,"cwd":"/tmp/advanced-acp","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783957884479,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783957884479,"data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: mount a no-op Cordis plugin named snapshot-marker; use run_code to inspect the live dynamic mounts through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; unmount dyn-1; then reply with exactly ADVANCED_ACP_OK."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783957884486,"data":{"turn":1,"step":1}} 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 b8acef973c..a4876185b0 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 @@ -5,6 +5,12 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working 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. @@ -61,6 +67,30 @@ declare const tools: { /** The dynamic mount id returned by cordis_mount (e.g. "dyn-1"). */ id: string; }): Promise; + /** Edit an existing UTF-8 text file by replacing literal text. */ + edit(args: { + /** Path to edit, resolved by the filesystem backend. */ + file_path: string; + /** Literal text to replace. Must match exactly. */ + old_string: string; + /** Literal replacement text. Use an empty string to delete the match. */ + new_string: string; + /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */ + replace_all?: boolean; + /** 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. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ + justification?: string; + }): Promise; + /** Read a UTF-8 text file and return line-numbered content. */ + read(args: { + /** Path to read, resolved by the filesystem backend. */ + file_path: string; + /** 1-based first line to return. Defaults to 1. */ + offset?: number; + /** Maximum number of lines to return. Defaults to 2000. */ + limit?: number; + }): Promise; /** 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. */ skill(args: { /** The exact skill name from the available skills list. */ @@ -139,5 +169,16 @@ declare const tools: { /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {"files": [...]}). */ args?: Record; }): Promise; + /** Create or fully replace a UTF-8 text file. */ + write(args: { + /** Path to write, resolved by the filesystem backend. */ + file_path: string; + /** Full UTF-8 text content to write. */ + content: string; + /** 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. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ + justification?: string; + }): Promise; } ``` 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 978819fa1f..2a6dc9078d 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 @@ -102,6 +102,72 @@ ] } }, + { + "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": "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": "run_code", "description": "Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.", @@ -344,6 +410,39 @@ "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": [] diff --git a/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl b/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl index 9782b0fd7d..65f92c4916 100644 --- a/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl +++ b/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Use the bash tool to print a large deterministic output, then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}} @@ -10,7 +10,7 @@ {"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":9,"time":0,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_spill","name":"bash","arguments":"{\"command\":\"node -e \\\"process.stdout.write('SPILL_START-' + 'x'.repeat(2000) + '-SPILL_END')\\\"\",\"description\":\"Print large deterministic output\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"} {"type":"tool/call","seq":10,"time":0,"data":{"turn":1,"step":1,"callId":"call_spill","name":"bash","arguments":"{\"command\":\"node -e \\\"process.stdout.write('SPILL_START-' + 'x'.repeat(2000) + '-SPILL_END')\\\"\",\"description\":\"Print large deterministic output\"}"}} -{"type":"tool/result","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"call_spill","content":[{"type":"text","text":"SPILL_START-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-SPILL_END\n\n(Omitted 1417 bytes. Full formatted result stored at: /tmp/dsh-acp-snapshot-spill/session-8d519f752b89/93e1b6e8dc7e-bash.txt. Use read with offset/limit, or grep this path to search within it.)"}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"} +{"type":"tool/result","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"call_spill","content":[{"type":"text","text":"SPILL_START-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-SPILL_END\n\n(Omitted 1417 bytes. Full formatted result stored at: /tmp/dsh-acp-snapshot-spill/session-2ef0a5f14624/b5e2b8c5e6a6-bash.txt. Use read with offset/limit, or grep this path to search within it.)"}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"} {"type":"step/end","seq":12,"time":0,"data":{"turn":1,"step":1}} {"type":"step/start","seq":13,"time":0,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl b/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl index 0ee3e67e2b..6b704c5bcd 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"bcd7e943-7b84-4264-82d0-f64e50d0d7ce","createdAt":1783611774317,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-52lrTl"} +{"type":"session","version":0,"id":"bcd7e943-7b84-4264-82d0-f64e50d0d7ce","createdAt":1783611774317,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-52lrTl","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783611774323,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783611774323,"data":{"content":[{"type":"text","text":"Call the run_code tool (NOT the native bash tool directly) with a program that runs exactly `echo BOTH_OK` via tools.bash and returns its output. Then reply with that output only and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783611774324,"data":{"turn":1,"step":1}} 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 f1a3b9ff92..73d0e5db92 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 @@ -5,6 +5,12 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working 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. @@ -44,6 +50,30 @@ declare const tools: { /** Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access. */ justification?: string; }): Promise; + /** Edit an existing UTF-8 text file by replacing literal text. */ + edit(args: { + /** Path to edit, resolved by the filesystem backend. */ + file_path: string; + /** Literal text to replace. Must match exactly. */ + old_string: string; + /** Literal replacement text. Use an empty string to delete the match. */ + new_string: string; + /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */ + replace_all?: boolean; + /** 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. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ + justification?: string; + }): Promise; + /** Read a UTF-8 text file and return line-numbered content. */ + read(args: { + /** Path to read, resolved by the filesystem backend. */ + file_path: string; + /** 1-based first line to return. Defaults to 1. */ + offset?: number; + /** Maximum number of lines to return. Defaults to 2000. */ + limit?: number; + }): Promise; /** 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. */ skill(args: { /** The exact skill name from the available skills list. */ @@ -122,5 +152,16 @@ declare const tools: { /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {"files": [...]}). */ args?: Record; }): Promise; + /** Create or fully replace a UTF-8 text file. */ + write(args: { + /** Path to write, resolved by the filesystem backend. */ + file_path: string; + /** Full UTF-8 text content to write. */ + content: string; + /** 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. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ + justification?: string; + }): Promise; } ``` 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 edf1a7c001..87ce19b1f6 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 @@ -45,6 +45,72 @@ ] } }, + { + "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": "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": "run_code", "description": "Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.", @@ -287,6 +353,39 @@ "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": [] diff --git a/examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl b/examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl index e6d18515a6..cdb76be163 100644 --- a/examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl +++ b/examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":1784437195072,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1784437195072,"data":{"content":[{"type":"text","text":"Run two shell commands: wait for cancellation, then write skipped.txt."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1784437195076,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/cancel/session.jsonl b/examples/acp-agent/tests/snapshots/cancel/session.jsonl index 347951db62..44e8ac136c 100644 --- a/examples/acp-agent/tests/snapshots/cancel/session.jsonl +++ b/examples/acp-agent/tests/snapshots/cancel/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Start a long task; this turn will be cancelled mid-stream."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl b/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl index 0fe57055e8..8ee160ba26 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"94cd1ae4-e1d1-4ec8-9d27-50a1f849b6b3","createdAt":1783611771392,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-BteTVR"} +{"type":"session","version":0,"id":"94cd1ae4-e1d1-4ec8-9d27-50a1f849b6b3","createdAt":1783611771392,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-BteTVR","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783611771394,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783611771394,"data":{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool twice — exactly `echo CODE_ONE` then exactly `echo CODE_TWO` — and return the two outputs joined with a plus sign. Then reply with that joined string only and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783611771396,"data":{"turn":1,"step":1}} 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 f1a3b9ff92..73d0e5db92 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 @@ -5,6 +5,12 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working 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. @@ -44,6 +50,30 @@ declare const tools: { /** Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access. */ justification?: string; }): Promise; + /** Edit an existing UTF-8 text file by replacing literal text. */ + edit(args: { + /** Path to edit, resolved by the filesystem backend. */ + file_path: string; + /** Literal text to replace. Must match exactly. */ + old_string: string; + /** Literal replacement text. Use an empty string to delete the match. */ + new_string: string; + /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */ + replace_all?: boolean; + /** 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. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ + justification?: string; + }): Promise; + /** Read a UTF-8 text file and return line-numbered content. */ + read(args: { + /** Path to read, resolved by the filesystem backend. */ + file_path: string; + /** 1-based first line to return. Defaults to 1. */ + offset?: number; + /** Maximum number of lines to return. Defaults to 2000. */ + limit?: number; + }): Promise; /** 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. */ skill(args: { /** The exact skill name from the available skills list. */ @@ -122,5 +152,16 @@ declare const tools: { /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {"files": [...]}). */ args?: Record; }): Promise; + /** Create or fully replace a UTF-8 text file. */ + write(args: { + /** Path to write, resolved by the filesystem backend. */ + file_path: string; + /** Full UTF-8 text content to write. */ + content: string; + /** 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. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ + justification?: string; + }): Promise; } ``` diff --git a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl index f744c1b000..4f72ffa498 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"65fbb8a6-624c-4d6a-bf5d-a7a7d14f2b49","createdAt":1783921765266,"cwd":"/var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-uorU26"} +{"type":"session","version":0,"id":"65fbb8a6-624c-4d6a-bf5d-a7a7d14f2b49","createdAt":1783921765266,"cwd":"/var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-uorU26","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783921765269,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783921765269,"data":{"content":[{"type":"text","text":"Using ONE run_code program, call tools.read on nested/task.txt. After the program finishes, answer the workspace handshake question using the newly discovered instructions: What is the Code Mode workspace handshake?"}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783921765275,"data":{"turn":1,"step":1}} @@ -86,7 +86,7 @@ {"type":"tool/call","seq":84,"time":1783921767208,"data":{"turn":1,"step":1,"callId":"call_00_6APApmaKLRDlXKMdIcWL5139","name":"run_code","arguments":"{\"code\": \"const content = await tools.read({ file_path: \\\"nested/task.txt\\\" });\\nreturn content;\"}"}} {"type":"tool/code-dispatch","seq":85,"time":1783921767270,"data":{"parentCallId":"call_00_6APApmaKLRDlXKMdIcWL5139","subCallId":"call_00_6APApmaKLRDlXKMdIcWL5139:code:1","name":"read","arguments":{"file_path":"nested/task.txt"},"isError":false,"resultSummary":"./nested/task.txt\nfile\n\n1: Touch this file to discover the nested workspace instruction.\n\n(End of file - total 1 lines)\n"}} {"type":"tool/result","seq":86,"time":1783921767271,"data":{"turn":1,"step":1,"callId":"call_00_6APApmaKLRDlXKMdIcWL5139","content":[{"type":"text","text":"/var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-uorU26/nested/task.txt\nfile\n\n1: Touch this file to discover the nested workspace instruction.\n\n(End of file - total 1 lines)\n"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[84],"surfaceOp":"append"} -{"type":"context/message","seq":87,"time":1783921767272,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"plugin","plugin":"workspace-context"},"envelope":"raw","meta":{"kind":"workspace-instructions","version":1,"changes":[{"action":"set","scope":"nested","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]}},"surfaceOp":"append"} +{"type":"context/message","seq":87,"time":1783921767272,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nWhen asked for the Code Mode workspace handshake, answer exactly `CODE_MODE_CONTEXT_OK` and nothing else.\n\n"}],"source":{"kind":"plugin","plugin":"workspace-context"},"meta":{"kind":"workspace-instructions","version":1,"changes":[{"action":"set","scope":"nested","path":"nested/AGENTS.md","digest":"ae22936ed26dc76b7107005ed6d5e2482a88668a"}]}},"surfaceOp":"append"} {"type":"step/end","seq":88,"time":1783921767272,"data":{"turn":1,"step":1}} {"type":"step/start","seq":89,"time":1783921767272,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":90,"time":1783921768339,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} 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 cc8e2d1301..73d0e5db92 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 @@ -60,6 +60,10 @@ declare const tools: { new_string: string; /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */ replace_all?: boolean; + /** 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. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ + justification?: string; }): Promise; /** Read a UTF-8 text file and return line-numbered content. */ read(args: { @@ -154,6 +158,10 @@ declare const tools: { file_path: string; /** Full UTF-8 text content to write. */ content: string; + /** 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. */ + sandbox_permissions?: "workspace-write" | "danger-full-access"; + /** Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access. */ + justification?: string; }): Promise; } ``` diff --git a/examples/acp-agent/tests/snapshots/config-options/session.jsonl b/examples/acp-agent/tests/snapshots/config-options/session.jsonl index a6f73319bc..63f2775383 100644 --- a/examples/acp-agent/tests/snapshots/config-options/session.jsonl +++ b/examples/acp-agent/tests/snapshots/config-options/session.jsonl @@ -1 +1 @@ -{"type":"session","version":0,"id":"00000000-0000-0000-0000-000000000000","createdAt":0} +{"type":"session","version":0,"id":"00000000-0000-0000-0000-000000000000","createdAt":0,"delegationDepth":0} diff --git a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl index 94ea8a8fec..207f8d8cc3 100644 --- a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl +++ b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783951000000,"cwd":"/tmp/cordis-inspect-jsdoc"} +{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783951000000,"cwd":"/tmp/cordis-inspect-jsdoc","delegationDepth":0} {"type":"turn/start","seq":0,"time":1784449176717,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1784449176718,"data":{"content":[{"type":"text","text":"Inspect the exact tools service API and tools/pre-execute event with cordis_inspect, then reply with exactly CORDIS_INSPECT_JSDOC_OK."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1784449176720,"data":{"turn":1,"step":1}} @@ -10,7 +10,7 @@ {"type":"assistant/chunk","seq":8,"time":1783951000009,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":9,"time":1784449176722,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"inspect-tools-api","name":"cordis_inspect","arguments":"{\"what\":\"api\",\"name\":\"tools\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"} {"type":"tool/call","seq":10,"time":1784449176722,"data":{"turn":1,"step":1,"callId":"inspect-tools-api","name":"cordis_inspect","arguments":"{\"what\":\"api\",\"name\":\"tools\"}"}} -{"type":"tool/result","seq":11,"time":1784449176732,"data":{"turn":1,"step":1,"callId":"inspect-tools-api","content":[{"type":"text","text":"## api\n- tools — Tool registry and execution pipeline.\n /**\n * Register globally or in the calling agent scope. Scoped tools shadow\n * globals; duplicates within one layer and the reserved `run_code` name fail.\n * @param definition - 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.\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(reason?: string): void;\n whenIdle(): Promise;\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 type ContextEnvelope = 'context' | 'raw';\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 envelope?: ContextEnvelope;\n meta?: JsonValue;\n }\n export interface InjectOptions extends SendOptions {\n envelope?: ContextEnvelope;\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 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 }"}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"} +{"type":"tool/result","seq":11,"time":1784449176732,"data":{"turn":1,"step":1,"callId":"inspect-tools-api","content":[{"type":"text","text":"## api\n- tools — Tool registry and execution pipeline.\n /**\n * Register globally or in the calling agent scope. Scoped tools shadow\n * globals; duplicates within one layer and the reserved `run_code` name fail.\n * @param definition - 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.\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(reason?: string): void;\n whenIdle(): Promise;\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 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 }"}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"} {"type":"step/end","seq":12,"time":1784449176732,"data":{"turn":1,"step":1}} {"type":"step/start","seq":13,"time":1784449176733,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":14,"time":1783951000015,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} diff --git a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl index 321c5499a2..e5fd8608d6 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,7 +1,7 @@ {"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":"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.\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(reason?: string): void;\n whenIdle(): Promise;\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 type ContextEnvelope = 'context' | 'raw';\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 envelope?: ContextEnvelope;\n meta?: JsonValue;\n }\n export interface InjectOptions extends SendOptions {\n envelope?: ContextEnvelope;\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 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 }"}}]}}} +{"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.\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(reason?: string): void;\n whenIdle(): Promise;\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 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 }"}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"inspect-tools-event","title":"Inspect cordis runtime: events: tools/pre-execute","kind":"read","status":"in_progress"}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"inspect-tools-event","status":"completed","content":[{"type":"content","content":{"type":"text","text":"## events\n- tools/pre-execute [waterfall] — Allow, deny, or ask before dispatch.\n /**\n * Allow, deny, or ask before dispatch. `next()` delegates to allow; missing\n * approval support turns `ask` into denial.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls.\n * @param exec - the pending call (name, parsed arguments, caller agent).\n * @mode waterfall\n */\n 'tools/pre-execute'(this: Scoped, exec: ToolExecution, next: () => Promise): Promise\nwaterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() vetoes the chain."}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"CORDIS_INSPECT_JSDOC_OK"}}}} diff --git a/examples/acp-agent/tests/snapshots/error-finish/session.jsonl b/examples/acp-agent/tests/snapshots/error-finish/session.jsonl index f0ef4267ac..6b6c44ebff 100644 --- a/examples/acp-agent/tests/snapshots/error-finish/session.jsonl +++ b/examples/acp-agent/tests/snapshots/error-finish/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"This prompt triggers a recorded provider error."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl b/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl index 2a33a4e15e..354a014a63 100644 --- a/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl +++ b/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl @@ -1,7 +1,7 @@ -{"type":"session","version":0,"id":"f3cbd087-fb45-4b32-b0f2-3082d65bfcb4","createdAt":1783860675270,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-cbBLh2"} +{"type":"session","version":0,"id":"f3cbd087-fb45-4b32-b0f2-3082d65bfcb4","createdAt":1783860675270,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-cbBLh2","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783860675271,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"permission/preset","seq":1,"time":1783962245380,"data":{"preset":"workspace-write"}} -{"type":"bash/sandbox-mode","seq":2,"time":1783962245380,"data":{"mode":"workspace-write"}} +{"type":"sandbox/mode","seq":2,"time":1784518116517,"data":{"mode":"workspace-write"}} {"type":"approval/policy","seq":3,"time":1783962245380,"data":{"policy":"ask"}} {"type":"user/message","seq":4,"time":1783962245380,"data":{"content":[{"type":"text","text":"The sandbox already denied writing /tmp/dsh-escalated.txt earlier (it is outside this workspace). Retry it now exactly once: one single bash call with the command printf 'escalated\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt, with sandbox_permissions set to danger-full-access and the justification 'the user asked to write a file outside the workspace'. Do not run it without sandbox_permissions first. I will approve the permission prompt. After the result, reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":5,"time":1783962245382,"data":{"turn":1,"step":1}} @@ -131,8 +131,8 @@ {"type":"assistant/chunk","seq":129,"time":1783962245385,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":130,"time":1783962245385,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a command with sandbox_permissions set to danger-full-access, no prior run needed, justified as instructed."},{"type":"tool-call","id":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write file outside workspace and verify\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":1501,"outputTokens":174,"cacheReadTokens":0,"reasoningTokens":28}},"sourceEventSeqs":[7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129],"surfaceOp":"append"} {"type":"tool/call","seq":131,"time":1783962245385,"data":{"turn":1,"step":1,"callId":"call_00_d0sAHpJ9mYOJi0z7KNy30441","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write file outside workspace and verify\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}} -{"type":"approval/asked","seq":132,"time":1783962245386,"data":{"id":"4ba41d82-153a-4587-8c22-dd35783c3d87","toolName":"bash","callId":"call_00_d0sAHpJ9mYOJi0z7KNy30441","reason":"escalate sandbox to danger-full-access: the user asked to write a file outside the workspace"}} -{"type":"approval/decided","seq":133,"time":1783962245387,"data":{"id":"4ba41d82-153a-4587-8c22-dd35783c3d87","outcome":"allowed-once"}} +{"type":"approval/asked","seq":132,"time":1783962245386,"data":{"id":"e2d45b4a-ff48-488d-aeb6-8edc9dc5c3de","toolName":"bash","callId":"call_00_d0sAHpJ9mYOJi0z7KNy30441","reason":"escalate sandbox to danger-full-access: the user asked to write a file outside the workspace"}} +{"type":"approval/decided","seq":133,"time":1783962245387,"data":{"id":"e2d45b4a-ff48-488d-aeb6-8edc9dc5c3de","outcome":"allowed-once"}} {"type":"tool/result","seq":134,"time":1783962245399,"data":{"turn":1,"step":1,"callId":"call_00_d0sAHpJ9mYOJi0z7KNy30441","content":[{"type":"text","text":"escalated\n"}],"isError":false},"sourceEventSeqs":[131],"surfaceOp":"append"} {"type":"step/end","seq":135,"time":1783962245400,"data":{"turn":1,"step":1}} {"type":"step/start","seq":136,"time":1783962245400,"data":{"turn":1,"step":2}} diff --git a/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl b/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl index 0e19158f2c..ff6d1187b3 100644 --- a/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl +++ b/examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl @@ -1,7 +1,7 @@ -{"type":"session","version":0,"id":"d692fe7f-7079-4ee4-8b06-f44fd026d4ea","createdAt":1783860679475,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-Hn29Od"} +{"type":"session","version":0,"id":"d692fe7f-7079-4ee4-8b06-f44fd026d4ea","createdAt":1783860679475,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-Hn29Od","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783860679476,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"permission/preset","seq":1,"time":1783962246267,"data":{"preset":"workspace-write"}} -{"type":"bash/sandbox-mode","seq":2,"time":1783962246267,"data":{"mode":"workspace-write"}} +{"type":"sandbox/mode","seq":2,"time":1784518117237,"data":{"mode":"workspace-write"}} {"type":"approval/policy","seq":3,"time":1783962246267,"data":{"policy":"ask"}} {"type":"user/message","seq":4,"time":1783962246267,"data":{"content":[{"type":"text","text":"The sandbox already denied writing /tmp/dsh-escalated.txt earlier (it is outside this workspace). Retry it now exactly once: one single bash call with the command printf 'escalated\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt, with sandbox_permissions set to danger-full-access and the justification 'the user asked to write a file outside the workspace'. Do not run it without sandbox_permissions first. I will reject the permission prompt; after the rejection, do not retry and do not work around it — explain in one short sentence and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":5,"time":1783962246269,"data":{"turn":1,"step":1}} @@ -155,8 +155,8 @@ {"type":"assistant/chunk","seq":153,"time":1783962246274,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} {"type":"assistant/message","seq":154,"time":1783962246274,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to run a specific command with `sandbox_permissions` set to `danger-full-access` and a specific justification. They explicitly said NOT to run it without sandbox_permissions first. Let me do exactly that."},{"type":"tool-call","id":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write to /tmp and verify, then clean up\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":1509,"outputTokens":198,"cacheReadTokens":0,"reasoningTokens":48}},"sourceEventSeqs":[7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153],"surfaceOp":"append"} {"type":"tool/call","seq":155,"time":1783962246274,"data":{"turn":1,"step":1,"callId":"call_00_WB1vnPomi8yr6MlcFKTj7912","name":"bash","arguments":"{\"command\": \"printf 'escalated\\\\n' > /tmp/dsh-escalated.txt && cat /tmp/dsh-escalated.txt && rm /tmp/dsh-escalated.txt\", \"description\": \"Write to /tmp and verify, then clean up\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to write a file outside the workspace\"}"}} -{"type":"approval/asked","seq":156,"time":1783962246275,"data":{"id":"84c78b9b-8955-4d4f-8dad-824dcac4d9ed","toolName":"bash","callId":"call_00_WB1vnPomi8yr6MlcFKTj7912","reason":"escalate sandbox to danger-full-access: the user asked to write a file outside the workspace"}} -{"type":"approval/decided","seq":157,"time":1783962246275,"data":{"id":"84c78b9b-8955-4d4f-8dad-824dcac4d9ed","outcome":"rejected"}} +{"type":"approval/asked","seq":156,"time":1783962246275,"data":{"id":"46c8dba4-52c9-4a6a-b6a6-5f34c95c28df","toolName":"bash","callId":"call_00_WB1vnPomi8yr6MlcFKTj7912","reason":"escalate sandbox to danger-full-access: the user asked to write a file outside the workspace"}} +{"type":"approval/decided","seq":157,"time":1783962246275,"data":{"id":"46c8dba4-52c9-4a6a-b6a6-5f34c95c28df","outcome":"rejected"}} {"type":"tool/result","seq":158,"time":1783962246275,"data":{"turn":1,"step":1,"callId":"call_00_WB1vnPomi8yr6MlcFKTj7912","content":[{"type":"text","text":"Error: the user rejected escalating this command to \"danger-full-access\""}],"isError":true},"sourceEventSeqs":[155],"surfaceOp":"append"} {"type":"step/end","seq":159,"time":1783962246276,"data":{"turn":1,"step":1}} {"type":"step/start","seq":160,"time":1783962246276,"data":{"turn":1,"step":2}} diff --git a/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl b/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl index 00587e34c2..25992426b8 100644 --- a/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-edit/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"736c4bd8-41bd-43fb-9030-b4df3b2a4f83","createdAt":1783352084735,"cwd":"/tmp/acp-snap-cwd-0BxHdV"} +{"type":"session","version":0,"id":"736c4bd8-41bd-43fb-9030-b4df3b2a4f83","createdAt":1783352084735,"cwd":"/tmp/acp-snap-cwd-0BxHdV","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352084740,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352084740,"data":{"content":[{"type":"text","text":"First use the read tool to read config.txt in the current directory. Then use the edit tool (NOT bash) to replace the literal text DEBUG with RELEASE in that file. Then reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352084742,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/fs-escalation-approved/input.json b/examples/acp-agent/tests/snapshots/fs-escalation-approved/input.json new file mode 100644 index 0000000000..d6d8d2b8c6 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/fs-escalation-approved/input.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { "op": "initialize" }, + { "op": "newSession" }, + { "op": "setConfigOption", "configId": "permission", "value": "workspace-write" }, + { "op": "prompt", "text": "Use the write tool (NOT bash) to create escalated.md in the current directory containing exactly the single line: escalated. An equivalent write was denied earlier, so make this one single write call with sandbox_permissions set to danger-full-access and the justification 'the user asked to escalate this write'. Do not call write without sandbox_permissions first. I will approve the permission prompt. After the result, reply with exactly the single word DONE." } + ], + "permissionAnswers": [ + { "kind": "allow_once" } + ] +} diff --git a/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl b/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl new file mode 100644 index 0000000000..54601f5354 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl @@ -0,0 +1,127 @@ +{"type":"session","version":0,"id":"977a4820-f609-4b48-9039-adcdd921c5fe","createdAt":1784045702340,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-vmEGzd","delegationDepth":0} +{"type":"turn/start","seq":0,"time":1784045702342,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"permission/preset","seq":1,"time":1784045702343,"data":{"preset":"workspace-write"}} +{"type":"sandbox/mode","seq":2,"time":1784045702343,"data":{"mode":"workspace-write"}} +{"type":"approval/policy","seq":3,"time":1784045702343,"data":{"policy":"ask"}} +{"type":"user/message","seq":4,"time":1784045702343,"data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create escalated.md in the current directory containing exactly the single line: escalated. An equivalent write was denied earlier, so make this one single write call with sandbox_permissions set to danger-full-access and the justification 'the user asked to escalate this write'. Do not call write without sandbox_permissions first. I will approve the permission prompt. After the result, reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":5,"time":1784045702345,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":6,"time":1784045702345,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":7,"time":1784045703046,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":8,"time":1784045703046,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":9,"time":1784045703162,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":10,"time":1784045703172,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":11,"time":1784045703172,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":12,"time":1784045703173,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":13,"time":1784045703173,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" create"}}} +{"type":"assistant/chunk","seq":14,"time":1784045703173,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} +{"type":"assistant/chunk","seq":15,"time":1784045703173,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":16,"time":1784045703199,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" using"}}} +{"type":"assistant/chunk","seq":17,"time":1784045703225,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":18,"time":1784045703251,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" write"}}} +{"type":"assistant/chunk","seq":19,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" tool"}}} +{"type":"assistant/chunk","seq":20,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":21,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" sand"}}} +{"type":"assistant/chunk","seq":22,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"box"}}} +{"type":"assistant/chunk","seq":23,"time":1784045703252,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"_per"}}} +{"type":"assistant/chunk","seq":24,"time":1784045703277,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"missions"}}} +{"type":"assistant/chunk","seq":25,"time":1784045703278,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":26,"time":1784045703278,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}} +{"type":"assistant/chunk","seq":27,"time":1784045703278,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":28,"time":1784045703278,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" do"}}} +{"type":"assistant/chunk","seq":29,"time":1784045703304,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}} +{"type":"assistant/chunk","seq":30,"time":1784045703304,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":31,"time":1784045703356,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":32,"time":1784045703356,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":""}}} +{"type":"assistant/chunk","seq":33,"time":1784045703381,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"{"}}} +{"type":"assistant/chunk","seq":34,"time":1784045703381,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":35,"time":1784045703381,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"file"}}} +{"type":"assistant/chunk","seq":36,"time":1784045703405,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"_path"}}} +{"type":"assistant/chunk","seq":37,"time":1784045703406,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":38,"time":1784045703406,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":39,"time":1784045703406,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":40,"time":1784045703431,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"es"}}} +{"type":"assistant/chunk","seq":41,"time":1784045703432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"cal"}}} +{"type":"assistant/chunk","seq":42,"time":1784045703432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"ated"}}} +{"type":"assistant/chunk","seq":43,"time":1784045703432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":".md"}}} +{"type":"assistant/chunk","seq":44,"time":1784045703432,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":45,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":46,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":47,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"content"}}} +{"type":"assistant/chunk","seq":48,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":49,"time":1784045703483,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":50,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":51,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"es"}}} +{"type":"assistant/chunk","seq":52,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"cal"}}} +{"type":"assistant/chunk","seq":53,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"ated"}}} +{"type":"assistant/chunk","seq":54,"time":1784045703509,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":55,"time":1784045703565,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":56,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":57,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"sand"}}} +{"type":"assistant/chunk","seq":58,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"box"}}} +{"type":"assistant/chunk","seq":59,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"_per"}}} +{"type":"assistant/chunk","seq":60,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"missions"}}} +{"type":"assistant/chunk","seq":61,"time":1784045703566,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":62,"time":1784045703591,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":63,"time":1784045703591,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":64,"time":1784045703591,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"danger"}}} +{"type":"assistant/chunk","seq":65,"time":1784045703591,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"-full"}}} +{"type":"assistant/chunk","seq":66,"time":1784045703617,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"-access"}}} +{"type":"assistant/chunk","seq":67,"time":1784045703618,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":68,"time":1784045703644,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":", "}}} +{"type":"assistant/chunk","seq":69,"time":1784045703645,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":70,"time":1784045703645,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"just"}}} +{"type":"assistant/chunk","seq":71,"time":1784045703645,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"ification"}}} +{"type":"assistant/chunk","seq":72,"time":1784045703669,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":73,"time":1784045703669,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":": "}}} +{"type":"assistant/chunk","seq":74,"time":1784045703669,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":75,"time":1784045703669,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"the"}}} +{"type":"assistant/chunk","seq":76,"time":1784045703696,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" user"}}} +{"type":"assistant/chunk","seq":77,"time":1784045703696,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" asked"}}} +{"type":"assistant/chunk","seq":78,"time":1784045703696,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" to"}}} +{"type":"assistant/chunk","seq":79,"time":1784045703696,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" escalate"}}} +{"type":"assistant/chunk","seq":80,"time":1784045703724,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" this"}}} +{"type":"assistant/chunk","seq":81,"time":1784045703724,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":" write"}}} +{"type":"assistant/chunk","seq":82,"time":1784045703724,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"\""}}} +{"type":"assistant/chunk","seq":83,"time":1784045703749,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":1,"id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","argumentsDelta":"}"}}} +{"type":"assistant/chunk","seq":84,"time":1784045703776,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to create a file using the write tool with sandbox_permissions. Let me do that."}}}} +{"type":"assistant/chunk","seq":85,"time":1784045703776,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","arguments":"{\"file_path\": \"escalated.md\", \"content\": \"escalated\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to escalate this write\"}"}}}} +{"type":"assistant/chunk","seq":86,"time":1784045703776,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3871,"outputTokens":132,"cacheReadTokens":0,"reasoningTokens":23}}}} +{"type":"assistant/chunk","seq":87,"time":1784045703776,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":88,"time":1784045703780,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to create a file using the write tool with sandbox_permissions. Let me do that."},{"type":"tool-call","id":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","arguments":"{\"file_path\": \"escalated.md\", \"content\": \"escalated\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to escalate this write\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3871,"outputTokens":132,"cacheReadTokens":0,"reasoningTokens":23}},"sourceEventSeqs":[7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87],"surfaceOp":"append"} +{"type":"tool/call","seq":89,"time":1784045703780,"data":{"turn":1,"step":1,"callId":"call_00_Fnymmavpr4klMDy4Fdej3227","name":"write","arguments":"{\"file_path\": \"escalated.md\", \"content\": \"escalated\", \"sandbox_permissions\": \"danger-full-access\", \"justification\": \"the user asked to escalate this write\"}"}} +{"type":"approval/asked","seq":90,"time":1784045703782,"data":{"id":"c37500b3-c252-4a9b-ad0d-9c4349419b30","toolName":"write","callId":"call_00_Fnymmavpr4klMDy4Fdej3227","reason":"escalate sandbox to danger-full-access: the user asked to escalate this write"}} +{"type":"approval/decided","seq":91,"time":1784045703786,"data":{"id":"c37500b3-c252-4a9b-ad0d-9c4349419b30","outcome":"allowed-once"}} +{"type":"tool/result","seq":92,"time":1784045703798,"data":{"turn":1,"step":1,"callId":"call_00_Fnymmavpr4klMDy4Fdej3227","content":[{"type":"text","text":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-vmEGzd/escalated.md\nfile\n\nCreated file\n"}],"isError":false},"sourceEventSeqs":[89],"surfaceOp":"append"} +{"type":"step/end","seq":93,"time":1784045703798,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":94,"time":1784045703799,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":95,"time":1784045704512,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":96,"time":1784045704512,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":97,"time":1784045704620,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" file"}}} +{"type":"assistant/chunk","seq":98,"time":1784045704645,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" was"}}} +{"type":"assistant/chunk","seq":99,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" created"}}} +{"type":"assistant/chunk","seq":100,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" successfully"}}} +{"type":"assistant/chunk","seq":101,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":102,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} +{"type":"assistant/chunk","seq":103,"time":1784045704646,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":104,"time":1784045704672,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" asked"}}} +{"type":"assistant/chunk","seq":105,"time":1784045704673,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":106,"time":1784045704673,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":107,"time":1784045704673,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":108,"time":1784045704699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":109,"time":1784045704699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}} +{"type":"assistant/chunk","seq":110,"time":1784045704699,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":111,"time":1784045704726,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":112,"time":1784045704726,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":113,"time":1784045704726,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" D"}}} +{"type":"assistant/chunk","seq":114,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ONE"}}} +{"type":"assistant/chunk","seq":115,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":116,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":117,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"D"}}} +{"type":"assistant/chunk","seq":118,"time":1784045704754,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ONE"}}} +{"type":"assistant/chunk","seq":119,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The file was created successfully. The user asked me to reply with exactly the single word DONE."}}}} +{"type":"assistant/chunk","seq":120,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"DONE"}}}} +{"type":"assistant/chunk","seq":121,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":107,"outputTokens":23,"cacheReadTokens":3968,"reasoningTokens":20}}}} +{"type":"assistant/chunk","seq":122,"time":1784045704755,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":123,"time":1784045704755,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The file was created successfully. The user asked me to reply with exactly the single word DONE."},{"type":"text","text":"DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":107,"outputTokens":23,"cacheReadTokens":3968,"reasoningTokens":20}},"sourceEventSeqs":[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],"surfaceOp":"append"} +{"type":"step/end","seq":124,"time":1784045704755,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":125,"time":1784045704756,"data":{"turn":1,"reason":{"kind":"completed"}}} 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 new file mode 100644 index 0000000000..b8b2f245ee --- /dev/null +++ b/examples/acp-agent/tests/snapshots/fs-escalation-approved/stdout.expected.jsonl @@ -0,0 +1,52 @@ +{"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","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":"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":" create"}}}} +{"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":" file"}}}} +{"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":" write"}}}} +{"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":" with"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" sand"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"box"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"_per"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"missions"}}}} +{"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":" do"}}}} +{"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":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call","toolCallId":"call_00_Fnymmavpr4klMDy4Fdej3227","title":"Write escalated.md","kind":"edit","status":"in_progress","locations":[{"path":"escalated.md"}],"content":[{"type":"diff","path":"escalated.md","oldText":null,"newText":"escalated"}]}}} +{"jsonrpc":"2.0","id":1,"method":"session/request_permission","params":{"sessionId":"{{sessionId}}","toolCall":{"toolCallId":"call_00_Fnymmavpr4klMDy4Fdej3227"},"options":[{"optionId":"allow-once","name":"Allow once","kind":"allow_once"},{"optionId":"reject-once","name":"Reject","kind":"reject_once"}]}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"tool_call_update","toolCallId":"call_00_Fnymmavpr4klMDy4Fdej3227","status":"completed","content":[{"type":"diff","path":"escalated.md","oldText":null,"newText":"escalated"}],"title":"Write escalated.md"}}} +{"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":" was"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" created"}}}} +{"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":" 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":" asked"}}}} +{"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":" exactly"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" the"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" 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":" D"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ONE"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_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":4,"result":{"stopReason":"end_turn"}} diff --git a/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl b/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl index 03a64d2e39..66efd5f934 100644 --- a/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"b3292503-2c3d-4677-804d-1ed6802a4bc5","createdAt":1783611702544,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-QzoqnB"} +{"type":"session","version":0,"id":"b3292503-2c3d-4677-804d-1ed6802a4bc5","createdAt":1783611702544,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-QzoqnB","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783611702550,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783611702550,"data":{"content":[{"type":"text","text":"Do NOT use the read tool and do NOT use bash or shell commands. Immediately use the edit tool to replace the literal text blue with green in settings.txt in the current directory. Do not read the file first. After the tool result, reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783611702550,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl b/examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl index e587011cfd..f0e1f170ac 100644 --- a/examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"b5639b9d-99a9-49e4-83da-77e6caa702be","createdAt":1783352099834,"cwd":"/tmp/acp-snap-cwd-N9HCkt"} +{"type":"session","version":0,"id":"b5639b9d-99a9-49e4-83da-77e6caa702be","createdAt":1783352099834,"cwd":"/tmp/acp-snap-cwd-N9HCkt","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352099838,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352099839,"data":{"content":[{"type":"text","text":"Use the read tool (NOT bash) with offset 5 and limit 4 to read lines 5 through 8 of big.txt in the current directory. Then reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352099840,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/fs-read/session.jsonl b/examples/acp-agent/tests/snapshots/fs-read/session.jsonl index 81737364f8..5582896174 100644 --- a/examples/acp-agent/tests/snapshots/fs-read/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-read/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"a57f852d-d476-4716-a380-8a1116e4d905","createdAt":1783352072464,"cwd":"/tmp/acp-snap-cwd-PEETkS"} +{"type":"session","version":0,"id":"a57f852d-d476-4716-a380-8a1116e4d905","createdAt":1783352072464,"cwd":"/tmp/acp-snap-cwd-PEETkS","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352072468,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352072469,"data":{"content":[{"type":"text","text":"Use the read tool (NOT bash) to read the file greeting.txt in the current directory, then reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352072470,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/fs-terminal-card/session.jsonl b/examples/acp-agent/tests/snapshots/fs-terminal-card/session.jsonl index e53f4b3da3..5c3bba5676 100644 --- a/examples/acp-agent/tests/snapshots/fs-terminal-card/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-terminal-card/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"e128dda9-ed11-4868-8266-0ef90d03c3d6","createdAt":1783352050748,"cwd":"/tmp/acp-snap-cwd-mrFUuk"} +{"type":"session","version":0,"id":"e128dda9-ed11-4868-8266-0ef90d03c3d6","createdAt":1783352050748,"cwd":"/tmp/acp-snap-cwd-mrFUuk","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352050753,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352050753,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo TERMINAL_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352050755,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl b/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl index 08ec0f1323..ab8b5965ad 100644 --- a/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"e04cc262-6c89-4586-88d7-3e919240d735","createdAt":1783352092215,"cwd":"/tmp/acp-snap-cwd-hH2sGY"} +{"type":"session","version":0,"id":"e04cc262-6c89-4586-88d7-3e919240d735","createdAt":1783352092215,"cwd":"/tmp/acp-snap-cwd-hH2sGY","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352092220,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352092221,"data":{"content":[{"type":"text","text":"First use the read tool to read data.txt in the current directory. Then use the write tool (NOT bash) to replace its entire contents with exactly the single line: replaced. Then reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352092223,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/fs-write/session.jsonl b/examples/acp-agent/tests/snapshots/fs-write/session.jsonl index 59ba868817..04fd86da0d 100644 --- a/examples/acp-agent/tests/snapshots/fs-write/session.jsonl +++ b/examples/acp-agent/tests/snapshots/fs-write/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"fdcab4d0-e5e4-4a06-9195-be8f7049d67e","createdAt":1783352078749,"cwd":"/tmp/acp-snap-cwd-sNvn5N"} +{"type":"session","version":0,"id":"fdcab4d0-e5e4-4a06-9195-be8f7049d67e","createdAt":1783352078749,"cwd":"/tmp/acp-snap-cwd-sNvn5N","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352078754,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352078754,"data":{"content":[{"type":"text","text":"Use the write tool (NOT bash) to create a file named notes.txt in the current directory containing exactly the single line: hello world. Then reply with exactly the single word DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352078756,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/handshake/session.jsonl b/examples/acp-agent/tests/snapshots/handshake/session.jsonl index a6f73319bc..63f2775383 100644 --- a/examples/acp-agent/tests/snapshots/handshake/session.jsonl +++ b/examples/acp-agent/tests/snapshots/handshake/session.jsonl @@ -1 +1 @@ -{"type":"session","version":0,"id":"00000000-0000-0000-0000-000000000000","createdAt":0} +{"type":"session","version":0,"id":"00000000-0000-0000-0000-000000000000","createdAt":0,"delegationDepth":0} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl index be1d70a853..c362de7a26 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"669e8682-49fc-4dff-9bc7-6280e283cbe4","createdAt":1783962504097,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-WxJGUY"} +{"type":"session","version":0,"id":"669e8682-49fc-4dff-9bc7-6280e283cbe4","createdAt":1783962504097,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-WxJGUY","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783962504115,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783962504117,"data":{"content":[{"type":"text","text":"Call the bash tool to run exactly: echo HELLO. If the first tool result is rejected, retry that command once. Quote the final tool result verbatim and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783962504152,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl index 2e167db28c..66d2a6b427 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"0a862642-6652-4916-b88d-b058954ab0c6","createdAt":1783352196657,"cwd":"/tmp/acp-snap-cwd-LEetSL"} +{"type":"session","version":0,"id":"0a862642-6652-4916-b88d-b058954ab0c6","createdAt":1783352196657,"cwd":"/tmp/acp-snap-cwd-LEetSL","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352196662,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352196662,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352196664,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl index dcd496f61c..247e13a075 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"f688431c-01a8-4326-a5c5-1b5f0fd08483","createdAt":1783352171511,"cwd":"/tmp/acp-snap-cwd-iKVciS"} +{"type":"session","version":0,"id":"f688431c-01a8-4326-a5c5-1b5f0fd08483","createdAt":1783352171511,"cwd":"/tmp/acp-snap-cwd-iKVciS","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352171519,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352171520,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352171527,"data":{"turn":1,"step":1}} @@ -55,8 +55,8 @@ {"type":"tool/call","seq":53,"time":1783352172557,"data":{"turn":1,"step":1,"callId":"call_00_6k0oGSliVHxGSgqBmMEO4311","name":"bash","arguments":"{\"command\": \"echo HELLO\", \"description\": \"Echo HELLO\"}"}} {"type":"hook/invoked","seq":54,"time":1783352172558,"data":{"turn":1,"point":"PreToolUse","dialect":"claude","handlerId":"claude:PreToolUse:1","matcher":"bash"}} {"type":"hook/result","seq":55,"time":1783352172573,"data":{"turn":1,"point":"PreToolUse","handlerId":"claude:PreToolUse:1","decision":"ask","exitCode":0,"durationMs":14.113374999999905}} -{"type":"approval/asked","seq":56,"time":1783962235813,"data":{"id":"9ba4bf87-d8b5-448f-94e6-6a8a0e43a8ad","toolName":"bash","callId":"call_00_6k0oGSliVHxGSgqBmMEO4311","reason":"bash requires manual approval in this session"}} -{"type":"approval/decided","seq":57,"time":1783962235813,"data":{"id":"9ba4bf87-d8b5-448f-94e6-6a8a0e43a8ad","outcome":"rejected"}} +{"type":"approval/asked","seq":56,"time":1783962235813,"data":{"id":"68f1e09d-f3e5-4e39-8a51-da082ba3ba99","toolName":"bash","callId":"call_00_6k0oGSliVHxGSgqBmMEO4311","reason":"bash requires manual approval in this session"}} +{"type":"approval/decided","seq":57,"time":1783962235813,"data":{"id":"68f1e09d-f3e5-4e39-8a51-da082ba3ba99","outcome":"rejected"}} {"type":"tool/result","seq":58,"time":1783962235814,"data":{"turn":1,"step":1,"callId":"call_00_6k0oGSliVHxGSgqBmMEO4311","content":[{"type":"text","text":"Error: the user rejected tool \"bash\""}],"isError":true},"sourceEventSeqs":[53],"surfaceOp":"append"} {"type":"step/end","seq":59,"time":1783962235814,"data":{"turn":1,"step":1}} {"type":"step/start","seq":60,"time":1783962235814,"data":{"turn":1,"step":2}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl index 9633075e5b..4193c7fe80 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"ff1c1e99-3bd4-4ef8-a954-80d607d628ba","createdAt":1783352165190,"cwd":"/tmp/acp-snap-cwd-wDnkVo"} +{"type":"session","version":0,"id":"ff1c1e99-3bd4-4ef8-a954-80d607d628ba","createdAt":1783352165190,"cwd":"/tmp/acp-snap-cwd-wDnkVo","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352165195,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352165196,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352165198,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/session.jsonl index b5f81fdaea..dd6745b770 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"hook/invoked","seq":1,"time":0,"data":{"turn":1,"point":"UserPromptSubmit","dialect":"claude","handlerId":"claude:UserPromptSubmit:1"}} {"type":"hook/result","seq":2,"time":0,"data":{"turn":1,"point":"UserPromptSubmit","handlerId":"claude:UserPromptSubmit:1","decision":"block","exitCode":2,"stderrSummary":"blocked by policy hook","durationMs":0}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl index 886761da18..d2a0806829 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"d03c3a83-1238-4e2e-ad9a-b86a61840a40","createdAt":1783352160541,"cwd":"/tmp/acp-snap-cwd-QUDqlk"} +{"type":"session","version":0,"id":"d03c3a83-1238-4e2e-ad9a-b86a61840a40","createdAt":1783352160541,"cwd":"/tmp/acp-snap-cwd-QUDqlk","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352160545,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"hook/invoked","seq":1,"time":1783352160546,"data":{"turn":1,"point":"UserPromptSubmit","dialect":"claude","handlerId":"claude:UserPromptSubmit:1"}} {"type":"hook/result","seq":2,"time":1783352160564,"data":{"turn":1,"point":"UserPromptSubmit","handlerId":"claude:UserPromptSubmit:1","decision":"pass","exitCode":0,"durationMs":17.45639600000004}} diff --git a/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl index 5f6b0e6e9e..c03ee5bd5d 100644 --- a/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl @@ -1,192 +1,67 @@ -{"type":"session","version":0,"id":"02bb4dcf-ffd6-4111-909b-504c7006d821","createdAt":1783352203365,"cwd":"/tmp/acp-snap-cwd-7YNbji"} -{"type":"turn/start","seq":0,"time":1783352203369,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"user/message","seq":1,"time":1783352203370,"data":{"content":[{"type":"text","text":"Reply with the single word FIRST and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"step/start","seq":2,"time":1783352203371,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":3,"time":1783352203372,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"assistant/chunk","seq":4,"time":1783352204036,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":5,"time":1783352204036,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":6,"time":1783352204247,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":7,"time":1783352204282,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} -{"type":"assistant/chunk","seq":8,"time":1783352204283,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":9,"time":1783352204283,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":10,"time":1783352204283,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":11,"time":1783352204283,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":12,"time":1783352204284,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":13,"time":1783352204317,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} -{"type":"assistant/chunk","seq":14,"time":1783352204318,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} -{"type":"assistant/chunk","seq":15,"time":1783352204318,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":16,"time":1783352204318,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"FIR"}}} -{"type":"assistant/chunk","seq":17,"time":1783352204318,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ST"}}} -{"type":"assistant/chunk","seq":18,"time":1783352204318,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} -{"type":"assistant/chunk","seq":19,"time":1783352204353,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":20,"time":1783352204353,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} -{"type":"assistant/chunk","seq":21,"time":1783352204353,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":22,"time":1783352204353,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":23,"time":1783352204353,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"FIR"}}} -{"type":"assistant/chunk","seq":24,"time":1783352204353,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ST"}}} -{"type":"assistant/chunk","seq":25,"time":1783352204393,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with the single word \"FIRST\" and stop."}}}} -{"type":"assistant/chunk","seq":26,"time":1783352204393,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"FIRST"}}}} -{"type":"assistant/chunk","seq":27,"time":1783352204393,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2862,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":17}}}} -{"type":"assistant/chunk","seq":28,"time":1783352204393,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":29,"time":1783352204396,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with the single word \"FIRST\" and stop."},{"type":"text","text":"FIRST"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":2862,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":17}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"surfaceOp":"append"} -{"type":"step/end","seq":30,"time":1783352204396,"data":{"turn":1,"step":1}} -{"type":"hook/invoked","seq":31,"time":1783352204396,"data":{"turn":1,"point":"Stop","dialect":"claude","handlerId":"claude:Stop:1"}} -{"type":"hook/result","seq":32,"time":1783352204443,"data":{"turn":1,"point":"Stop","handlerId":"claude:Stop:1","decision":"block","exitCode":2,"stderrSummary":"Also reply with the single word SECOND, then stop.","durationMs":47.01462700000047}} -{"type":"steering/message","seq":33,"time":1783352204444,"data":{"turn":1,"content":[{"type":"text","text":"Also reply with the single word SECOND, then stop."}],"source":{"kind":"plugin","plugin":"hooks-claude"}},"surfaceOp":"append"} -{"type":"step/start","seq":34,"time":1783352204444,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":35,"time":1783352204945,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":36,"time":1783352204946,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":37,"time":1783352205054,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":38,"time":1783352205086,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} -{"type":"assistant/chunk","seq":39,"time":1783352205087,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":40,"time":1783352205087,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":41,"time":1783352205087,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":42,"time":1783352205115,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":43,"time":1783352205115,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":44,"time":1783352205116,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"FIR"}}} -{"type":"assistant/chunk","seq":45,"time":1783352205116,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"ST"}}} -{"type":"assistant/chunk","seq":46,"time":1783352205116,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} -{"type":"assistant/chunk","seq":47,"time":1783352205143,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" only"}}} -{"type":"assistant/chunk","seq":48,"time":1783352205172,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":49,"time":1783352205172,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" but"}}} -{"type":"assistant/chunk","seq":50,"time":1783352205172,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" there"}}} -{"type":"assistant/chunk","seq":51,"time":1783352205200,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} -{"type":"assistant/chunk","seq":52,"time":1783352205201,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} -{"type":"assistant/chunk","seq":53,"time":1783352205201,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" steering"}}} -{"type":"assistant/chunk","seq":54,"time":1783352205201,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" input"}}} -{"type":"assistant/chunk","seq":55,"time":1783352205231,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" telling"}}} -{"type":"assistant/chunk","seq":56,"time":1783352205232,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":57,"time":1783352205232,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":58,"time":1783352205232,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" also"}}} -{"type":"assistant/chunk","seq":59,"time":1783352205266,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":60,"time":1783352205266,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":61,"time":1783352205266,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":62,"time":1783352205266,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"SEC"}}} -{"type":"assistant/chunk","seq":63,"time":1783352205266,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"OND"}}} -{"type":"assistant/chunk","seq":64,"time":1783352205267,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}} -{"type":"assistant/chunk","seq":65,"time":1783352205288,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" However"}}} -{"type":"assistant/chunk","seq":66,"time":1783352205318,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":67,"time":1783352205319,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":68,"time":1783352205319,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":69,"time":1783352205319,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} -{"type":"assistant/chunk","seq":70,"time":1783352205319,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" explicit"}}} -{"type":"assistant/chunk","seq":71,"time":1783352205359,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instruction"}}} -{"type":"assistant/chunk","seq":72,"time":1783352205359,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} -{"type":"assistant/chunk","seq":73,"time":1783352205373,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":74,"time":1783352205373,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":75,"time":1783352205373,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":76,"time":1783352205373,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":77,"time":1783352205401,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} -{"type":"assistant/chunk","seq":78,"time":1783352205402,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} -{"type":"assistant/chunk","seq":79,"time":1783352205402,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" FIRST"}}} -{"type":"assistant/chunk","seq":80,"time":1783352205430,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":81,"time":1783352205430,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} -{"type":"assistant/chunk","seq":82,"time":1783352205431,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":83,"time":1783352205431,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} -{"type":"assistant/chunk","seq":84,"time":1783352205431,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" steering"}}} -{"type":"assistant/chunk","seq":85,"time":1783352205431,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} -{"type":"assistant/chunk","seq":86,"time":1783352205457,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" from"}}} -{"type":"assistant/chunk","seq":87,"time":1783352205457,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} -{"type":"assistant/chunk","seq":88,"time":1783352205458,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" plugin"}}} -{"type":"assistant/chunk","seq":89,"time":1783352205485,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":90,"time":1783352205486,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} -{"type":"assistant/chunk","seq":91,"time":1783352205514,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":92,"time":1783352205515,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":93,"time":1783352205515,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" themselves"}}} -{"type":"assistant/chunk","seq":94,"time":1783352205542,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":95,"time":1783352205542,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} -{"type":"assistant/chunk","seq":96,"time":1783352205542,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" should"}}} -{"type":"assistant/chunk","seq":97,"time":1783352205543,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" follow"}}} -{"type":"assistant/chunk","seq":98,"time":1783352205543,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":99,"time":1783352205543,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":100,"time":1783352205571,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} -{"type":"assistant/chunk","seq":101,"time":1783352205571,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" original"}}} -{"type":"assistant/chunk","seq":102,"time":1783352205601,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instruction"}}} -{"type":"assistant/chunk","seq":103,"time":1783352205601,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".\n\n"}}} -{"type":"assistant/chunk","seq":104,"time":1783352205601,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"Wait"}}} -{"type":"assistant/chunk","seq":105,"time":1783352205601,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":106,"time":1783352205601,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" let"}}} -{"type":"assistant/chunk","seq":107,"time":1783352205629,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":108,"time":1783352205629,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" re"}}} -{"type":"assistant/chunk","seq":109,"time":1783352205630,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"-read"}}} -{"type":"assistant/chunk","seq":110,"time":1783352205630,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":111,"time":1783352205630,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} -{"type":"assistant/chunk","seq":112,"time":1783352205630,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":113,"time":1783352205656,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" said"}}} -{"type":"assistant/chunk","seq":114,"time":1783352205656,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":115,"time":1783352205657,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"Reply"}}} -{"type":"assistant/chunk","seq":116,"time":1783352205657,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":117,"time":1783352205657,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":118,"time":1783352205657,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} -{"type":"assistant/chunk","seq":119,"time":1783352205687,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} -{"type":"assistant/chunk","seq":120,"time":1783352205687,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" FIRST"}}} -{"type":"assistant/chunk","seq":121,"time":1783352205687,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":122,"time":1783352205687,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} -{"type":"assistant/chunk","seq":123,"time":1783352205687,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".\""}}} -{"type":"assistant/chunk","seq":124,"time":1783352205688,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" Then"}}} -{"type":"assistant/chunk","seq":125,"time":1783352205712,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" there"}}} -{"type":"assistant/chunk","seq":126,"time":1783352205713,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} -{"type":"assistant/chunk","seq":127,"time":1783352205713,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} -{"type":"assistant/chunk","seq":128,"time":1783352205740,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" steering"}}} -{"type":"assistant/chunk","seq":129,"time":1783352205741,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" from"}}} -{"type":"assistant/chunk","seq":130,"time":1783352205770,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":131,"time":1783352205770,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"plugin"}}} -{"type":"assistant/chunk","seq":132,"time":1783352205771,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} -{"type":"assistant/chunk","seq":133,"time":1783352205801,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" saying"}}} -{"type":"assistant/chunk","seq":134,"time":1783352205802,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":135,"time":1783352205802,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"Also"}}} -{"type":"assistant/chunk","seq":136,"time":1783352205802,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":137,"time":1783352205829,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":138,"time":1783352205829,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":139,"time":1783352205829,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} -{"type":"assistant/chunk","seq":140,"time":1783352205829,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} -{"type":"assistant/chunk","seq":141,"time":1783352205829,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" SECOND"}}} -{"type":"assistant/chunk","seq":142,"time":1783352205830,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":143,"time":1783352205857,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} -{"type":"assistant/chunk","seq":144,"time":1783352205857,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} -{"type":"assistant/chunk","seq":145,"time":1783352205858,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":".\"\n\n"}}} -{"type":"assistant/chunk","seq":146,"time":1783352205858,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":147,"time":1783352205858,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":148,"time":1783352205886,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} -{"type":"assistant/chunk","seq":149,"time":1783352205886,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instruction"}}} -{"type":"assistant/chunk","seq":150,"time":1783352205914,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} -{"type":"assistant/chunk","seq":151,"time":1783352205915,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" clear"}}} -{"type":"assistant/chunk","seq":152,"time":1783352205942,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":":"}}} -{"type":"assistant/chunk","seq":153,"time":1783352205971,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":154,"time":1783352205971,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":155,"time":1783352205971,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" FIRST"}}} -{"type":"assistant/chunk","seq":156,"time":1783352205971,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":157,"time":1783352206029,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} -{"type":"assistant/chunk","seq":158,"time":1783352206030,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":159,"time":1783352206030,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" The"}}} -{"type":"assistant/chunk","seq":160,"time":1783352206030,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" steering"}}} -{"type":"assistant/chunk","seq":161,"time":1783352206030,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} -{"type":"assistant/chunk","seq":162,"time":1783352206030,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" from"}}} -{"type":"assistant/chunk","seq":163,"time":1783352206044,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" a"}}} -{"type":"assistant/chunk","seq":164,"time":1783352206044,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" plugin"}}} -{"type":"assistant/chunk","seq":165,"time":1783352206044,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" source"}}} -{"type":"assistant/chunk","seq":166,"time":1783352206074,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":167,"time":1783352206074,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" not"}}} -{"type":"assistant/chunk","seq":168,"time":1783352206074,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":169,"time":1783352206074,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":170,"time":1783352206116,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":171,"time":1783352206117,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" I"}}} -{"type":"assistant/chunk","seq":172,"time":1783352206117,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"'ll"}}} -{"type":"assistant/chunk","seq":173,"time":1783352206117,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" follow"}}} -{"type":"assistant/chunk","seq":174,"time":1783352206139,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":175,"time":1783352206140,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":176,"time":1783352206140,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"'s"}}} -{"type":"assistant/chunk","seq":177,"time":1783352206140,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" instruction"}}} -{"type":"assistant/chunk","seq":178,"time":1783352206140,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":179,"time":1783352206161,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":180,"time":1783352206161,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"FIR"}}} -{"type":"assistant/chunk","seq":181,"time":1783352206162,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"ST"}}} -{"type":"assistant/chunk","seq":182,"time":1783352206162,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with \"FIRST\" only, but there's a steering input telling me to also reply with \"SECOND\". However, the user's explicit instruction is to reply with the single word FIRST and stop. The steering is from a plugin, not the user themselves. I should follow the user's original instruction.\n\nWait, let me re-read. The user said \"Reply with the single word FIRST and stop.\" Then there's a steering from \"plugin\" saying \"Also reply with the single word SECOND, then stop.\"\n\nThe user's instruction is clear: reply with FIRST and stop. The steering is from a plugin source, not the user. I'll follow the user's instruction."}}}} -{"type":"assistant/chunk","seq":183,"time":1783352206162,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"FIRST"}}}} -{"type":"assistant/chunk","seq":184,"time":1783352206162,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":73,"outputTokens":146,"cacheReadTokens":2816,"reasoningTokens":143}}}} -{"type":"assistant/chunk","seq":185,"time":1783352206162,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":186,"time":1783352206162,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The user wants me to reply with \"FIRST\" only, but there's a steering input telling me to also reply with \"SECOND\". However, the user's explicit instruction is to reply with the single word FIRST and stop. The steering is from a plugin, not the user themselves. I should follow the user's original instruction.\n\nWait, let me re-read. The user said \"Reply with the single word FIRST and stop.\" Then there's a steering from \"plugin\" saying \"Also reply with the single word SECOND, then stop.\"\n\nThe user's instruction is clear: reply with FIRST and stop. The steering is from a plugin source, not the user. I'll follow the user's instruction."},{"type":"text","text":"FIRST"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":73,"outputTokens":146,"cacheReadTokens":2816,"reasoningTokens":143}},"sourceEventSeqs":[35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,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],"surfaceOp":"append"} -{"type":"step/end","seq":187,"time":1783352206163,"data":{"turn":1,"step":2}} -{"type":"hook/invoked","seq":188,"time":1783352206163,"data":{"turn":1,"point":"Stop","dialect":"claude","handlerId":"claude:Stop:2"}} -{"type":"hook/result","seq":189,"time":1783352206190,"data":{"turn":1,"point":"Stop","handlerId":"claude:Stop:2","decision":"pass","exitCode":0,"durationMs":26.904655000000275}} -{"type":"turn/end","seq":190,"time":1783352206190,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"session","version":0,"id":"eda79fbc-8a1b-4226-b74a-f5f297484747","createdAt":1784522140642,"cwd":"/var/folders/4j/54c8wb496zxfrs1ny_21jbb00000gn/T/acp-snap-cwd-r6rWZp","delegationDepth":0} +{"type":"turn/start","seq":0,"time":1784522140646,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1784522140647,"data":{"content":[{"type":"text","text":"Reply with the single word FIRST and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1784522140648,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1784522140648,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1784522142865,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":5,"time":1784522142865,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":6,"time":1784522142866,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":7,"time":1784522142866,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":8,"time":1784522142866,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":9,"time":1784522142866,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":10,"time":1784522142866,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":11,"time":1784522142866,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":12,"time":1784522142866,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" just"}}} +{"type":"assistant/chunk","seq":13,"time":1784522142876,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":14,"time":1784522142876,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":15,"time":1784522142876,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":16,"time":1784522142877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"FIR"}}} +{"type":"assistant/chunk","seq":17,"time":1784522142877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ST"}}} +{"type":"assistant/chunk","seq":18,"time":1784522142877,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":19,"time":1784522142904,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":20,"time":1784522142904,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} +{"type":"assistant/chunk","seq":21,"time":1784522142904,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":22,"time":1784522142905,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":23,"time":1784522142905,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"FIR"}}} +{"type":"assistant/chunk","seq":24,"time":1784522142905,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ST"}}} +{"type":"assistant/chunk","seq":25,"time":1784522142942,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with just the word \"FIRST\" and stop."}}}} +{"type":"assistant/chunk","seq":26,"time":1784522142942,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"FIRST"}}}} +{"type":"assistant/chunk","seq":27,"time":1784522142942,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3545,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":17}}}} +{"type":"assistant/chunk","seq":28,"time":1784522142942,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":29,"time":1784522142947,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with just the word \"FIRST\" and stop."},{"type":"text","text":"FIRST"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3545,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":17}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"surfaceOp":"append"} +{"type":"step/end","seq":30,"time":1784522142947,"data":{"turn":1,"step":1}} +{"type":"hook/invoked","seq":31,"time":1784522142947,"data":{"turn":1,"point":"Stop","dialect":"claude","handlerId":"claude:Stop:1"}} +{"type":"hook/result","seq":32,"time":1784522142962,"data":{"turn":1,"point":"Stop","handlerId":"claude:Stop:1","decision":"block","exitCode":2,"stderrSummary":"Also reply with the single word SECOND, then stop.","durationMs":14.349833000000217}} +{"type":"steering/message","seq":33,"time":1784522142962,"data":{"turn":1,"content":[{"type":"text","text":"Also reply with the single word SECOND, then stop."}],"source":{"kind":"plugin","plugin":"hooks-claude"}},"surfaceOp":"append"} +{"type":"step/start","seq":34,"time":1784522142963,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":35,"time":1784522143914,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":36,"time":1784522143914,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":37,"time":1784522144018,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":38,"time":1784522144049,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":39,"time":1784522144049,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":40,"time":1784522144049,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":41,"time":1784522144049,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":42,"time":1784522144049,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":43,"time":1784522144049,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":44,"time":1784522144077,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":45,"time":1784522144077,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":46,"time":1784522144077,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":47,"time":1784522144077,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"SEC"}}} +{"type":"assistant/chunk","seq":48,"time":1784522144077,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"OND"}}} +{"type":"assistant/chunk","seq":49,"time":1784522144077,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":50,"time":1784522144135,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":51,"time":1784522144135,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} +{"type":"assistant/chunk","seq":52,"time":1784522144135,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} +{"type":"assistant/chunk","seq":53,"time":1784522144135,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":54,"time":1784522144135,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":55,"time":1784522144135,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"SEC"}}} +{"type":"assistant/chunk","seq":56,"time":1784522144141,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"OND"}}} +{"type":"assistant/chunk","seq":57,"time":1784522144141,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with the single word \"SECOND\" and then stop."}}}} +{"type":"assistant/chunk","seq":58,"time":1784522144141,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SECOND"}}}} +{"type":"assistant/chunk","seq":59,"time":1784522144141,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":106,"outputTokens":21,"cacheReadTokens":3456,"reasoningTokens":18}}}} +{"type":"assistant/chunk","seq":60,"time":1784522144141,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":61,"time":1784522144142,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The user wants me to reply with the single word \"SECOND\" and then stop."},{"type":"text","text":"SECOND"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":106,"outputTokens":21,"cacheReadTokens":3456,"reasoningTokens":18}},"sourceEventSeqs":[35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60],"surfaceOp":"append"} +{"type":"step/end","seq":62,"time":1784522144142,"data":{"turn":1,"step":2}} +{"type":"hook/invoked","seq":63,"time":1784522144142,"data":{"turn":1,"point":"Stop","dialect":"claude","handlerId":"claude:Stop:2"}} +{"type":"hook/result","seq":64,"time":1784522144144,"data":{"turn":1,"point":"Stop","handlerId":"claude:Stop:2","decision":"pass","exitCode":0,"durationMs":2.5859159999999974}} +{"type":"turn/end","seq":65,"time":1784522144145,"data":{"turn":1,"reason":{"kind":"completed"}}} 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 39cf7df191..2d92b5b3e7 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 @@ -7,8 +7,8 @@ {"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":" just"}}}} {"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":" 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_thought_chunk","content":{"type":"text","text":"FIR"}}}} @@ -26,142 +26,17 @@ {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" to"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" \""}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"FIR"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ST"}}}} -{"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":" only"}}}} -{"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":" there"}}}} -{"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":" a"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" steering"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" input"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" telling"}}}} -{"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":" also"}}}} -{"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":" the"}}}} +{"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_thought_chunk","content":{"type":"text","text":"SEC"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"OND"}}}} -{"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":" However"}}}} -{"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":"'s"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" explicit"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" instruction"}}}} -{"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":" 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":" the"}}}} -{"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":" FIRST"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" stop"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" The"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" steering"}}}} -{"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":" from"}}}} -{"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":" plugin"}}}} -{"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":" not"}}}} -{"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":" themselves"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" I"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" should"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" follow"}}}} -{"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":"'s"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" original"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" instruction"}}}} -{"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":"Wait"}}}} -{"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":"."}}}} -{"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":" 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":"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":" the"}}}} -{"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":" FIRST"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" stop"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":".\""}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" Then"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" there"}}}} -{"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":" a"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" steering"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" from"}}}} -{"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":"plugin"}}}} {"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":" saying"}}}} -{"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":"Also"}}}} -{"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":" the"}}}} -{"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":" SECOND"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":","}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" then"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" stop"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":".\"\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":"'s"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" instruction"}}}} -{"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":" clear"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":":"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" reply"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" with"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" FIRST"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" stop"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" The"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" steering"}}}} -{"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":" from"}}}} -{"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":" plugin"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" source"}}}} -{"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":" not"}}}} -{"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":"."}}}} -{"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":" follow"}}}} -{"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":"'s"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" instruction"}}}} -{"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":"FIR"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ST"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"SEC"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"OND"}}}} {"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl index 6d08c3ee7d..dc68891c14 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"01aa6a36-e9c2-42ba-934b-30bec80a1658","createdAt":1783986962232,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-x67BsP"} +{"type":"session","version":0,"id":"01aa6a36-e9c2-42ba-934b-30bec80a1658","createdAt":1783986962232,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-x67BsP","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783986962235,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783986962235,"data":{"content":[{"type":"text","text":"Call the bash tool exactly once to run: echo HELLO. Whatever tool result comes back, quote it verbatim and stop without calling another tool."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783986962240,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl index d60858ee0a..b7e97b3a65 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"39d8aabe-6457-4a0e-83b7-ee33125a3666","createdAt":1783352228436,"cwd":"/tmp/acp-snap-cwd-VGFtPi"} +{"type":"session","version":0,"id":"39d8aabe-6457-4a0e-83b7-ee33125a3666","createdAt":1783352228436,"cwd":"/tmp/acp-snap-cwd-VGFtPi","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352228441,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352228442,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352228443,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl index 812998aad5..c2675ae3af 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"57a74aed-99fc-43bc-a875-6dddebf64d69","createdAt":1783352214599,"cwd":"/tmp/acp-snap-cwd-7Hbu0m"} +{"type":"session","version":0,"id":"57a74aed-99fc-43bc-a875-6dddebf64d69","createdAt":1783352214599,"cwd":"/tmp/acp-snap-cwd-7Hbu0m","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352214604,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352214605,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo HELLO. Report the tool result you got back verbatim, then stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352214607,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/session.jsonl index bc9144f980..126a761309 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"hook/invoked","seq":1,"time":0,"data":{"turn":1,"point":"UserPromptSubmit","dialect":"codex","handlerId":"codex:UserPromptSubmit:1"}} {"type":"hook/result","seq":2,"time":0,"data":{"turn":1,"point":"UserPromptSubmit","handlerId":"codex:UserPromptSubmit:1","decision":"block","exitCode":2,"stderrSummary":"blocked by codex policy hook","durationMs":0}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl index 1c710ff8e1..ad7538cba9 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"0bebc0f4-a089-4fde-9b6e-db9532cfd4de","createdAt":1783352209682,"cwd":"/tmp/acp-snap-cwd-aopaZV"} +{"type":"session","version":0,"id":"0bebc0f4-a089-4fde-9b6e-db9532cfd4de","createdAt":1783352209682,"cwd":"/tmp/acp-snap-cwd-aopaZV","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352209686,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"hook/invoked","seq":1,"time":1783352209687,"data":{"turn":1,"point":"UserPromptSubmit","dialect":"codex","handlerId":"codex:UserPromptSubmit:1"}} {"type":"hook/result","seq":2,"time":1783352209706,"data":{"turn":1,"point":"UserPromptSubmit","handlerId":"codex:UserPromptSubmit:1","decision":"pass","exitCode":0,"durationMs":19.49695100000008}} diff --git a/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl index bba879203d..5cf14d9b49 100644 --- a/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl +++ b/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl @@ -1,67 +1,67 @@ -{"type":"session","version":0,"id":"bc6b18d1-d10e-481c-9b9c-c92d9188db3a","createdAt":1783352235015,"cwd":"/tmp/acp-snap-cwd-iHVZRl"} -{"type":"turn/start","seq":0,"time":1783352235020,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} -{"type":"user/message","seq":1,"time":1783352235020,"data":{"content":[{"type":"text","text":"Reply with the single word FIRST and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} -{"type":"step/start","seq":2,"time":1783352235022,"data":{"turn":1,"step":1}} -{"type":"request/header","seq":3,"time":1783352235023,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"assistant/chunk","seq":4,"time":1783352235669,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":5,"time":1783352235670,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":6,"time":1783352235879,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":7,"time":1783352235894,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} -{"type":"assistant/chunk","seq":8,"time":1783352235894,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":9,"time":1783352235895,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":10,"time":1783352235895,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":11,"time":1783352235925,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":12,"time":1783352235926,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" only"}}} -{"type":"assistant/chunk","seq":13,"time":1783352235955,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":14,"time":1783352235956,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} -{"type":"assistant/chunk","seq":15,"time":1783352235956,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} -{"type":"assistant/chunk","seq":16,"time":1783352235956,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} -{"type":"assistant/chunk","seq":17,"time":1783352235956,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"FIR"}}} -{"type":"assistant/chunk","seq":18,"time":1783352235982,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ST"}}} -{"type":"assistant/chunk","seq":19,"time":1783352235983,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} -{"type":"assistant/chunk","seq":20,"time":1783352235983,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} -{"type":"assistant/chunk","seq":21,"time":1783352235983,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} -{"type":"assistant/chunk","seq":22,"time":1783352236011,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} -{"type":"assistant/chunk","seq":23,"time":1783352236011,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":24,"time":1783352236012,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":25,"time":1783352236012,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"FIR"}}} -{"type":"assistant/chunk","seq":26,"time":1783352236012,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ST"}}} -{"type":"assistant/chunk","seq":27,"time":1783352236041,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with only the single word \"FIRST\" and then stop."}}}} -{"type":"assistant/chunk","seq":28,"time":1783352236041,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"FIRST"}}}} -{"type":"assistant/chunk","seq":29,"time":1783352236041,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":2862,"outputTokens":22,"cacheReadTokens":0,"reasoningTokens":19}}}} -{"type":"assistant/chunk","seq":30,"time":1783352236041,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":31,"time":1783352236043,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with only the single word \"FIRST\" and then stop."},{"type":"text","text":"FIRST"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":2862,"outputTokens":22,"cacheReadTokens":0,"reasoningTokens":19}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"surfaceOp":"append"} -{"type":"step/end","seq":32,"time":1783352236043,"data":{"turn":1,"step":1}} -{"type":"hook/invoked","seq":33,"time":1783352236043,"data":{"turn":1,"point":"Stop","dialect":"codex","handlerId":"codex:Stop:1"}} -{"type":"hook/result","seq":34,"time":1783352236059,"data":{"turn":1,"point":"Stop","handlerId":"codex:Stop:1","decision":"block","exitCode":2,"stderrSummary":"Also reply with the single word SECOND, then stop.","durationMs":14.77945499999987}} -{"type":"steering/message","seq":35,"time":1783352236059,"data":{"turn":1,"content":[{"type":"text","text":"Also reply with the single word SECOND, then stop."}],"source":{"kind":"plugin","plugin":"hooks-codex"}},"surfaceOp":"append"} -{"type":"step/start","seq":36,"time":1783352236059,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":37,"time":1783352236629,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"assistant/chunk","seq":38,"time":1783352236629,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} -{"type":"assistant/chunk","seq":39,"time":1783352236730,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} -{"type":"assistant/chunk","seq":40,"time":1783352236758,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" is"}}} -{"type":"assistant/chunk","seq":41,"time":1783352236759,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" asking"}}} -{"type":"assistant/chunk","seq":42,"time":1783352236759,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} -{"type":"assistant/chunk","seq":43,"time":1783352236788,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} -{"type":"assistant/chunk","seq":44,"time":1783352236788,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} -{"type":"assistant/chunk","seq":45,"time":1783352236788,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} -{"type":"assistant/chunk","seq":46,"time":1783352236789,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} -{"type":"assistant/chunk","seq":47,"time":1783352236817,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} -{"type":"assistant/chunk","seq":48,"time":1783352236818,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} -{"type":"assistant/chunk","seq":49,"time":1783352236818,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" SECOND"}}} -{"type":"assistant/chunk","seq":50,"time":1783352236818,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":","}}} -{"type":"assistant/chunk","seq":51,"time":1783352236818,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} -{"type":"assistant/chunk","seq":52,"time":1783352236846,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} -{"type":"assistant/chunk","seq":53,"time":1783352236846,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} -{"type":"assistant/chunk","seq":54,"time":1783352236876,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"assistant/chunk","seq":55,"time":1783352236876,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"SEC"}}} -{"type":"assistant/chunk","seq":56,"time":1783352236876,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"OND"}}} -{"type":"assistant/chunk","seq":57,"time":1783352236876,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user is asking me to reply with the single word SECOND, then stop."}}}} -{"type":"assistant/chunk","seq":58,"time":1783352236876,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SECOND"}}}} -{"type":"assistant/chunk","seq":59,"time":1783352236876,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":73,"outputTokens":19,"cacheReadTokens":2816,"reasoningTokens":16}}}} -{"type":"assistant/chunk","seq":60,"time":1783352236877,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":61,"time":1783352236877,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The user is asking me to reply with the single word SECOND, then stop."},{"type":"text","text":"SECOND"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":73,"outputTokens":19,"cacheReadTokens":2816,"reasoningTokens":16}},"sourceEventSeqs":[37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60],"surfaceOp":"append"} -{"type":"step/end","seq":62,"time":1783352236877,"data":{"turn":1,"step":2}} -{"type":"hook/invoked","seq":63,"time":1783352236877,"data":{"turn":1,"point":"Stop","dialect":"codex","handlerId":"codex:Stop:2"}} -{"type":"hook/result","seq":64,"time":1783352236908,"data":{"turn":1,"point":"Stop","handlerId":"codex:Stop:2","decision":"pass","exitCode":0,"durationMs":30.947317000000112}} -{"type":"turn/end","seq":65,"time":1783352236909,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"session","version":0,"id":"eb17be12-ca8c-46c8-b500-0977e8400208","createdAt":1784522152392,"cwd":"/var/folders/4j/54c8wb496zxfrs1ny_21jbb00000gn/T/acp-snap-cwd-ESgqLu","delegationDepth":0} +{"type":"turn/start","seq":0,"time":1784522152397,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1784522152397,"data":{"content":[{"type":"text","text":"Reply with the single word FIRST and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1784522152399,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1784522152399,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1784522153542,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":5,"time":1784522153542,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":6,"time":1784522153749,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":7,"time":1784522153750,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":8,"time":1784522153750,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":9,"time":1784522153751,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":10,"time":1784522153751,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":11,"time":1784522153751,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":12,"time":1784522153752,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":13,"time":1784522153752,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":14,"time":1784522153752,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":15,"time":1784522153752,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":16,"time":1784522153752,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"FIR"}}} +{"type":"assistant/chunk","seq":17,"time":1784522153752,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"ST"}}} +{"type":"assistant/chunk","seq":18,"time":1784522153752,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":19,"time":1784522153761,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":20,"time":1784522153761,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} +{"type":"assistant/chunk","seq":21,"time":1784522153761,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":22,"time":1784522153761,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":23,"time":1784522153762,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"FIR"}}} +{"type":"assistant/chunk","seq":24,"time":1784522153762,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"ST"}}} +{"type":"assistant/chunk","seq":25,"time":1784522153785,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with the single word \"FIRST\" and stop."}}}} +{"type":"assistant/chunk","seq":26,"time":1784522153786,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"FIRST"}}}} +{"type":"assistant/chunk","seq":27,"time":1784522153786,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3545,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":17}}}} +{"type":"assistant/chunk","seq":28,"time":1784522153786,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":29,"time":1784522153790,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with the single word \"FIRST\" and stop."},{"type":"text","text":"FIRST"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":3545,"outputTokens":20,"cacheReadTokens":0,"reasoningTokens":17}},"sourceEventSeqs":[4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"surfaceOp":"append"} +{"type":"step/end","seq":30,"time":1784522153790,"data":{"turn":1,"step":1}} +{"type":"hook/invoked","seq":31,"time":1784522153791,"data":{"turn":1,"point":"Stop","dialect":"codex","handlerId":"codex:Stop:1"}} +{"type":"hook/result","seq":32,"time":1784522153806,"data":{"turn":1,"point":"Stop","handlerId":"codex:Stop:1","decision":"block","exitCode":2,"stderrSummary":"Also reply with the single word SECOND, then stop.","durationMs":14.605791999999838}} +{"type":"steering/message","seq":33,"time":1784522153806,"data":{"turn":1,"content":[{"type":"text","text":"Also reply with the single word SECOND, then stop."}],"source":{"kind":"plugin","plugin":"hooks-codex"}},"surfaceOp":"append"} +{"type":"step/start","seq":34,"time":1784522153806,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":35,"time":1784522154765,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} +{"type":"assistant/chunk","seq":36,"time":1784522154765,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}} +{"type":"assistant/chunk","seq":37,"time":1784522154866,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}} +{"type":"assistant/chunk","seq":38,"time":1784522154898,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}} +{"type":"assistant/chunk","seq":39,"time":1784522154898,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}} +{"type":"assistant/chunk","seq":40,"time":1784522154898,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}} +{"type":"assistant/chunk","seq":41,"time":1784522154898,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}} +{"type":"assistant/chunk","seq":42,"time":1784522154898,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}} +{"type":"assistant/chunk","seq":43,"time":1784522154898,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" the"}}} +{"type":"assistant/chunk","seq":44,"time":1784522154924,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" single"}}} +{"type":"assistant/chunk","seq":45,"time":1784522154925,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" word"}}} +{"type":"assistant/chunk","seq":46,"time":1784522154925,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}} +{"type":"assistant/chunk","seq":47,"time":1784522154925,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"SEC"}}} +{"type":"assistant/chunk","seq":48,"time":1784522154925,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"OND"}}} +{"type":"assistant/chunk","seq":49,"time":1784522154925,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"\""}}} +{"type":"assistant/chunk","seq":50,"time":1784522154950,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" and"}}} +{"type":"assistant/chunk","seq":51,"time":1784522154951,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" then"}}} +{"type":"assistant/chunk","seq":52,"time":1784522154951,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":" stop"}}} +{"type":"assistant/chunk","seq":53,"time":1784522154951,"data":{"turn":1,"step":2,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}} +{"type":"assistant/chunk","seq":54,"time":1784522154951,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} +{"type":"assistant/chunk","seq":55,"time":1784522154951,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"SEC"}}} +{"type":"assistant/chunk","seq":56,"time":1784522154978,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":1,"text":"OND"}}} +{"type":"assistant/chunk","seq":57,"time":1784522154980,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with the single word \"SECOND\" and then stop."}}}} +{"type":"assistant/chunk","seq":58,"time":1784522154980,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SECOND"}}}} +{"type":"assistant/chunk","seq":59,"time":1784522154980,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":106,"outputTokens":21,"cacheReadTokens":3456,"reasoningTokens":18}}}} +{"type":"assistant/chunk","seq":60,"time":1784522154980,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":61,"time":1784522154981,"data":{"turn":1,"step":2,"content":[{"type":"reasoning","text":"The user wants me to reply with the single word \"SECOND\" and then stop."},{"type":"text","text":"SECOND"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":106,"outputTokens":21,"cacheReadTokens":3456,"reasoningTokens":18}},"sourceEventSeqs":[35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60],"surfaceOp":"append"} +{"type":"step/end","seq":62,"time":1784522154982,"data":{"turn":1,"step":2}} +{"type":"hook/invoked","seq":63,"time":1784522154982,"data":{"turn":1,"point":"Stop","dialect":"codex","handlerId":"codex:Stop:2"}} +{"type":"hook/result","seq":64,"time":1784522154990,"data":{"turn":1,"point":"Stop","handlerId":"codex:Stop:2","decision":"pass","exitCode":0,"durationMs":7.6766670000001795}} +{"type":"turn/end","seq":65,"time":1784522154990,"data":{"turn":1,"reason":{"kind":"completed"}}} 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 821d648791..a3e72075ed 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 @@ -7,7 +7,6 @@ {"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":" only"}}}} {"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":" single"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" word"}}}} @@ -16,15 +15,13 @@ {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"ST"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\""}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" then"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" stop"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"FIR"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ST"}}}} {"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":" is"}}}} -{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" asking"}}}} +{"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"}}}} @@ -32,8 +29,11 @@ {"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":" 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":" SECOND"}}}} -{"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":"SEC"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"OND"}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"\""}}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" and"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" then"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":" stop"}}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"."}}}} diff --git a/examples/acp-agent/tests/snapshots/model-switching/session.jsonl b/examples/acp-agent/tests/snapshots/model-switching/session.jsonl index 1f04eca07d..ddc592bbb6 100644 --- a/examples/acp-agent/tests/snapshots/model-switching/session.jsonl +++ b/examples/acp-agent/tests/snapshots/model-switching/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"622d16ce-0a94-476b-97a4-26dad50b1fbf","createdAt":1784086275585,"cwd":"/var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-Cwf7Bh"} +{"type":"session","version":0,"id":"622d16ce-0a94-476b-97a4-26dad50b1fbf","createdAt":1784086275585,"cwd":"/var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-Cwf7Bh","delegationDepth":0} {"type":"turn/start","seq":0,"time":1784086275588,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1784086275588,"data":{"content":[{"type":"text","text":"Without using tools, reply with exactly FLASH and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1784086275590,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/model-switching/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/model-switching/system-prompt.expected.md index b9701e538c..e89336a2fe 100644 --- a/examples/acp-agent/tests/snapshots/model-switching/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/model-switching/system-prompt.expected.md @@ -5,6 +5,12 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working 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. @@ -23,6 +29,12 @@ You are a coding assistant powered by the deepseek-v4-pro model. Your working di 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. 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 7c814257fb..9b64929d83 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 @@ -45,6 +45,72 @@ ] } }, + { + "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": "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.", @@ -271,6 +337,39 @@ "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": [ @@ -320,6 +419,72 @@ ] } }, + { + "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": "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.", @@ -546,6 +711,39 @@ "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/multi-turn/session.jsonl b/examples/acp-agent/tests/snapshots/multi-turn/session.jsonl index cd32072eaa..83ccf18a3f 100644 --- a/examples/acp-agent/tests/snapshots/multi-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/multi-turn/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"228b7b82-84ed-49b7-a567-981c03b28c77","createdAt":1783352113760,"cwd":"/tmp/acp-snap-cwd-aN2GRR"} +{"type":"session","version":0,"id":"228b7b82-84ed-49b7-a567-981c03b28c77","createdAt":1783352113760,"cwd":"/tmp/acp-snap-cwd-aN2GRR","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352113765,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352113765,"data":{"content":[{"type":"text","text":"Reply with exactly the word: ONE. No tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352113767,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl b/examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl index 81503dc4cf..e83f0cd59c 100644 --- a/examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl +++ b/examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Use the read tool twice in the same assistant message: read a.txt and b.txt. Then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/permission-switching/session.jsonl b/examples/acp-agent/tests/snapshots/permission-switching/session.jsonl index 4a62de6642..ff44da7e1d 100644 --- a/examples/acp-agent/tests/snapshots/permission-switching/session.jsonl +++ b/examples/acp-agent/tests/snapshots/permission-switching/session.jsonl @@ -1,7 +1,7 @@ -{"type":"session","version":0,"id":"df041acb-2f14-4d5f-b6e2-2fb6b9eb6427","createdAt":1783860666204,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-4oJKT4"} +{"type":"session","version":0,"id":"df041acb-2f14-4d5f-b6e2-2fb6b9eb6427","createdAt":1783860666204,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-4oJKT4","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783860666206,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"permission/preset","seq":1,"time":1783962244578,"data":{"preset":"workspace-write"}} -{"type":"bash/sandbox-mode","seq":2,"time":1783962244578,"data":{"mode":"workspace-write"}} +{"type":"sandbox/mode","seq":2,"time":1784518115721,"data":{"mode":"workspace-write"}} {"type":"approval/policy","seq":3,"time":1783962244578,"data":{"policy":"ask"}} {"type":"user/message","seq":4,"time":1783962244578,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly this one command in a single call: printf 'before\\n' > out.txt && cat out.txt. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":5,"time":1783962244579,"data":{"turn":1,"step":1}} @@ -102,7 +102,7 @@ {"type":"turn/end","seq":100,"time":1783962244601,"data":{"turn":1,"reason":{"kind":"completed"}}} {"type":"turn/start","seq":101,"time":1783962244623,"data":{"turn":2,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"permission/preset","seq":102,"time":1783962244624,"data":{"preset":"danger-full-access"}} -{"type":"bash/sandbox-mode","seq":103,"time":1783962244624,"data":{"mode":"danger-full-access"}} +{"type":"sandbox/mode","seq":103,"time":1784518115842,"data":{"mode":"danger-full-access"}} {"type":"approval/policy","seq":104,"time":1783962244624,"data":{"policy":"never"}} {"type":"user/message","seq":105,"time":1783962244624,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: cat out.txt. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"context/message","seq":106,"time":1783962244624,"data":{"content":[{"type":"text","text":"The approval policy changed from \"ask\" to \"never\" (changed by the user)."}],"source":{"kind":"plugin","plugin":"user-approval"}},"surfaceOp":"append"} diff --git a/examples/acp-agent/tests/snapshots/permission-switching/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/permission-switching/system-prompt.expected.md index 622bc4e23a..47a68e9a03 100644 --- a/examples/acp-agent/tests/snapshots/permission-switching/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/permission-switching/system-prompt.expected.md @@ -5,6 +5,12 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working 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. @@ -22,6 +28,12 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working 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. 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 7c814257fb..9b64929d83 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 @@ -45,6 +45,72 @@ ] } }, + { + "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": "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.", @@ -271,6 +337,39 @@ "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": [ @@ -320,6 +419,72 @@ ] } }, + { + "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": "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.", @@ -546,6 +711,39 @@ "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/reject-extra-dirs/session.jsonl b/examples/acp-agent/tests/snapshots/reject-extra-dirs/session.jsonl index a6f73319bc..63f2775383 100644 --- a/examples/acp-agent/tests/snapshots/reject-extra-dirs/session.jsonl +++ b/examples/acp-agent/tests/snapshots/reject-extra-dirs/session.jsonl @@ -1 +1 @@ -{"type":"session","version":0,"id":"00000000-0000-0000-0000-000000000000","createdAt":0} +{"type":"session","version":0,"id":"00000000-0000-0000-0000-000000000000","createdAt":0,"delegationDepth":0} diff --git a/examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl b/examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl index 20d18973d9..c77e0ef38d 100644 --- a/examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl +++ b/examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Write the todo list 'watch the kettle boil' five times in a row without changing it, then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl index 0fe1a5152c..1a9704954a 100644 --- a/examples/acp-agent/tests/snapshots/skill-load/session.jsonl +++ b/examples/acp-agent/tests/snapshots/skill-load/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"9eb4181f-2d05-49d3-98fc-3711fe2f5664","createdAt":1783654655599,"cwd":"/var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-DhYwNW"} +{"type":"session","version":0,"id":"9eb4181f-2d05-49d3-98fc-3711fe2f5664","createdAt":1783654655599,"cwd":"/var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-DhYwNW","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783654655602,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783654655603,"data":{"content":[{"type":"text","text":"Load the snapshot-skill skill with the skill tool, then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783654655608,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/skill-load/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/skill-load/system-prompt.expected.md index 43ecb9746f..ddf502a773 100644 --- a/examples/acp-agent/tests/snapshots/skill-load/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/skill-load/system-prompt.expected.md @@ -5,6 +5,12 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working 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. 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 e422a063da..151e76201b 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 @@ -45,6 +45,72 @@ ] } }, + { + "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": "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.", @@ -271,6 +337,39 @@ "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": [] diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/input.json b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/input.json new file mode 100644 index 0000000000..414a33affe --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/input.json @@ -0,0 +1,7 @@ +{ + "steps": [ + { "op": "initialize" }, + { "op": "newSession" }, + { "op": "prompt", "text": "Delegate through two child generations. The depth-two child must attempt one more subagent call and report the rejection." } + ] +} diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/replay.override.json b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/replay.override.json new file mode 100644 index 0000000000..ea012cfb63 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/replay.override.json @@ -0,0 +1,22 @@ +[ + { + "kind": "chunks", + "chunks": [ + { "type": "block-start", "index": 0, "blockType": "tool-call" }, + { "type": "tool-call-delta", "index": 0, "id": "call_root_child", "name": "subagent", "argumentsDelta": "{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\"}" }, + { "type": "block-end", "index": 0, "block": { "type": "tool-call", "id": "call_root_child", "name": "subagent", "arguments": "{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\"}" } }, + { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 5 } }, + { "type": "finish", "reason": { "kind": "tool-calls" } } + ] + }, + { + "kind": "chunks", + "chunks": [ + { "type": "block-start", "index": 0, "blockType": "text" }, + { "type": "text-delta", "index": 0, "text": "ROOT_DONE" }, + { "type": "block-end", "index": 0, "block": { "type": "text", "text": "ROOT_DONE" } }, + { "type": "usage", "usage": { "inputTokens": 10, "outputTokens": 2 } }, + { "type": "finish", "reason": { "kind": "stop" } } + ] + } +] diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl new file mode 100644 index 0000000000..b7017c51f3 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl @@ -0,0 +1,23 @@ +{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1001,"cwd":"/tmp/subagent-depth-two","parentSession":"11111111-1111-4111-8111-111111111111","delegationDepth":1} +{"type":"turn/start","seq":0,"time":1784540790312,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1784540790312,"data":{"content":[{"type":"text","text":"Call subagent once. Ask that child to attempt one further subagent call, then report the result."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1784540790318,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1784540790318,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_depth_one_child","name":"subagent","argumentsDelta":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}}} +{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}}}} +{"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} +{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":9,"time":1784540790318,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"} +{"type":"tool/call","seq":10,"time":1784540790319,"data":{"turn":1,"step":1,"callId":"call_depth_one_child","name":"subagent","arguments":"{\"description\":\"Start depth two\",\"prompt\":\"Attempt one subagent call beyond the configured cap, then report the rejection.\"}"}} +{"type":"tool/result","seq":11,"time":1784540790362,"data":{"turn":1,"step":1,"callId":"call_depth_one_child","content":[{"type":"text","text":"DEPTH_REJECTED"}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"} +{"type":"step/end","seq":12,"time":1784540790363,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":13,"time":1784540790364,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":14,"time":1784540790365,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":15,"time":1784540790365,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DEPTH_ONE_DONE"}}} +{"type":"assistant/chunk","seq":16,"time":1784540790365,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_ONE_DONE"}}}} +{"type":"assistant/chunk","seq":17,"time":1784540790365,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}} +{"type":"assistant/chunk","seq":18,"time":1784540790365,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":19,"time":1784540790365,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"} +{"type":"step/end","seq":20,"time":1784540790365,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":21,"time":1784540790365,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl new file mode 100644 index 0000000000..7b36136970 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl @@ -0,0 +1,23 @@ +{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1002,"cwd":"/tmp/subagent-depth-two","parentSession":"22222222-2222-4222-8222-222222222222","delegationDepth":2} +{"type":"turn/start","seq":0,"time":1784540790319,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1784540790319,"data":{"content":[{"type":"text","text":"Attempt one subagent call beyond the configured cap, then report the rejection."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1784540790334,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1784540790334,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":5,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_depth_three_rejected","name":"subagent","argumentsDelta":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}}} +{"type":"assistant/chunk","seq":6,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_depth_three_rejected","name":"subagent","arguments":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}}}} +{"type":"assistant/chunk","seq":7,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} +{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":9,"time":1784540790335,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_depth_three_rejected","name":"subagent","arguments":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"} +{"type":"tool/call","seq":10,"time":1784540790335,"data":{"turn":1,"step":1,"callId":"call_depth_three_rejected","name":"subagent","arguments":"{\"description\":\"Exceed depth cap\",\"prompt\":\"This child must never start.\"}"}} +{"type":"tool/result","seq":11,"time":1784540790337,"data":{"turn":1,"step":1,"callId":"call_depth_three_rejected","content":[{"type":"text","text":"Error: subagent depth 3 exceeds maxDepth 2"}],"isError":true},"sourceEventSeqs":[10],"surfaceOp":"append"} +{"type":"step/end","seq":12,"time":1784540790338,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":13,"time":1784540790338,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":14,"time":1784540790339,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":15,"time":1784540790339,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"DEPTH_REJECTED"}}} +{"type":"assistant/chunk","seq":16,"time":1784540790339,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DEPTH_REJECTED"}}}} +{"type":"assistant/chunk","seq":17,"time":1784540790339,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}} +{"type":"assistant/chunk","seq":18,"time":1784540790339,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":19,"time":1784540790339,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"DEPTH_REJECTED"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"} +{"type":"step/end","seq":20,"time":1784540790339,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":21,"time":1784540790339,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl new file mode 100644 index 0000000000..b9bdc1baea --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl @@ -0,0 +1,23 @@ +{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1000,"cwd":"/tmp/subagent-depth-two","delegationDepth":0} +{"type":"turn/start","seq":0,"time":1784540790290,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} +{"type":"user/message","seq":1,"time":1784540790291,"data":{"content":[{"type":"text","text":"Delegate through two child generations. The depth-two child must attempt one more subagent call and report the rejection."}],"source":{"kind":"user"}},"surfaceOp":"append"} +{"type":"step/start","seq":2,"time":1784540790308,"data":{"turn":1,"step":1}} +{"type":"request/header","seq":3,"time":1784540790308,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"assistant/chunk","seq":4,"time":1784540790309,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":5,"time":1784540790309,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"call_root_child","name":"subagent","argumentsDelta":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\"}"}}} +{"type":"assistant/chunk","seq":6,"time":1784540790309,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\"}"}}}} +{"type":"assistant/chunk","seq":7,"time":1784540790309,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}} +{"type":"assistant/chunk","seq":8,"time":1784540790309,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":9,"time":1784540790310,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"} +{"type":"tool/call","seq":10,"time":1784540790310,"data":{"turn":1,"step":1,"callId":"call_root_child","name":"subagent","arguments":"{\"description\":\"Start depth one\",\"prompt\":\"Call subagent once. Ask that child to attempt one further subagent call, then report the result.\"}"}} +{"type":"tool/result","seq":11,"time":1784540790381,"data":{"turn":1,"step":1,"callId":"call_root_child","content":[{"type":"text","text":"DEPTH_ONE_DONE"}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"} +{"type":"step/end","seq":12,"time":1784540790382,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":13,"time":1784540790382,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":14,"time":1784540790383,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":15,"time":1784540790383,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"ROOT_DONE"}}} +{"type":"assistant/chunk","seq":16,"time":1784540790383,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ROOT_DONE"}}}} +{"type":"assistant/chunk","seq":17,"time":1784540790383,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}}} +{"type":"assistant/chunk","seq":18,"time":1784540790383,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":19,"time":1784540790383,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"ROOT_DONE"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":2}},"sourceEventSeqs":[14,15,16,17,18],"surfaceOp":"append"} +{"type":"step/end","seq":20,"time":1784540790383,"data":{"turn":1,"step":2}} +{"type":"turn/end","seq":21,"time":1784540790383,"data":{"turn":1,"reason":{"kind":"completed"}}} 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 new file mode 100644 index 0000000000..7f629a2d71 --- /dev/null +++ b/examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/stdout.expected.jsonl @@ -0,0 +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":"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"}}]}}} +{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ROOT_DONE"}}}} +{"jsonrpc":"2.0","id":3,"result":{"stopReason":"end_turn"}} diff --git a/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl index 4ecb61b60f..0ec70d6f81 100644 --- a/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"ada8966c-9fa3-441b-8721-37ff1e795e6a","createdAt":1783352137161,"cwd":"/tmp/acp-snap-cwd-0HLtcD","parentSession":"96cf59c9-b347-48b9-b234-a5200913ad05","seedLength":37} +{"type":"session","version":0,"id":"ada8966c-9fa3-441b-8721-37ff1e795e6a","createdAt":1783352137161,"cwd":"/tmp/acp-snap-cwd-0HLtcD","parentSession":"96cf59c9-b347-48b9-b234-a5200913ad05","seedLength":37,"delegationDepth":1} {"type":"turn/start","seq":0,"time":1783352134837,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352134838,"data":{"content":[{"type":"text","text":"Remember this fact for later: the project codeword is MARMALADE. Reply with the single word OK and stop. Do not use any tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352134840,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl index 64da6e60e5..60c1e5cc10 100644 --- a/examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"96cf59c9-b347-48b9-b234-a5200913ad05","createdAt":1783352134832,"cwd":"/tmp/acp-snap-cwd-0HLtcD"} +{"type":"session","version":0,"id":"96cf59c9-b347-48b9-b234-a5200913ad05","createdAt":1783352134832,"cwd":"/tmp/acp-snap-cwd-0HLtcD","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352134837,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352134838,"data":{"content":[{"type":"text","text":"Remember this fact for later: the project codeword is MARMALADE. Reply with the single word OK and stop. Do not use any tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352134840,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl index c99a5681e2..4c8d25ad82 100644 --- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"e4aafa18-b9e3-48d0-8aae-6c9b25dcae80","createdAt":1783352145223,"cwd":"/tmp/acp-snap-cwd-i43JSF","parentSession":"959ffdf5-03e2-465e-9482-009b704632dc"} +{"type":"session","version":0,"id":"e4aafa18-b9e3-48d0-8aae-6c9b25dcae80","createdAt":1783352145223,"cwd":"/tmp/acp-snap-cwd-i43JSF","parentSession":"959ffdf5-03e2-465e-9482-009b704632dc","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783352145224,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352145224,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352145224,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl index adfcb6f60e..4bc55d2910 100644 --- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"02b3a8dd-1d5e-4866-825f-5fbf5000a632","createdAt":1783352147504,"cwd":"/tmp/acp-snap-cwd-i43JSF","parentSession":"959ffdf5-03e2-465e-9482-009b704632dc","seedLength":31} +{"type":"session","version":0,"id":"02b3a8dd-1d5e-4866-825f-5fbf5000a632","createdAt":1783352147504,"cwd":"/tmp/acp-snap-cwd-i43JSF","parentSession":"959ffdf5-03e2-465e-9482-009b704632dc","seedLength":31,"delegationDepth":1} {"type":"turn/start","seq":0,"time":1783352142834,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352142834,"data":{"content":[{"type":"text","text":"Remember this fact for later: the project codeword is SAFFRON. Reply with the single word OK and stop. Do not use any tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352142835,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl index 1ea4f541e1..b977a17e16 100644 --- a/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"959ffdf5-03e2-465e-9482-009b704632dc","createdAt":1783352142830,"cwd":"/tmp/acp-snap-cwd-i43JSF"} +{"type":"session","version":0,"id":"959ffdf5-03e2-465e-9482-009b704632dc","createdAt":1783352142830,"cwd":"/tmp/acp-snap-cwd-i43JSF","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352142834,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352142834,"data":{"content":[{"type":"text","text":"Remember this fact for later: the project codeword is SAFFRON. Reply with the single word OK and stop. Do not use any tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352142835,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl index 86c481c5ff..53b59caa4b 100644 --- a/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"553f8e92-aac1-4df3-8657-eacbb58f9581","createdAt":1783352127669,"cwd":"/tmp/acp-snap-cwd-28z5Of","parentSession":"14dda109-5728-45ba-a002-7db9543fe50e"} +{"type":"session","version":0,"id":"553f8e92-aac1-4df3-8657-eacbb58f9581","createdAt":1783352127669,"cwd":"/tmp/acp-snap-cwd-28z5Of","parentSession":"14dda109-5728-45ba-a002-7db9543fe50e","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783352127670,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352127670,"data":{"content":[{"type":"text","text":"Reply with exactly the word ALPHA and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352127671,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl index 483e687a14..412ae705db 100644 --- a/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"5f49e80c-16fc-42c7-a617-0b6bd0680aa3","createdAt":1783352129662,"cwd":"/tmp/acp-snap-cwd-28z5Of","parentSession":"14dda109-5728-45ba-a002-7db9543fe50e"} +{"type":"session","version":0,"id":"5f49e80c-16fc-42c7-a617-0b6bd0680aa3","createdAt":1783352129662,"cwd":"/tmp/acp-snap-cwd-28z5Of","parentSession":"14dda109-5728-45ba-a002-7db9543fe50e","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783352129662,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352129662,"data":{"content":[{"type":"text","text":"Reply with exactly the word BETA and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352129663,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl index a8093fba04..b632943f89 100644 --- a/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"14dda109-5728-45ba-a002-7db9543fe50e","createdAt":1783352126247,"cwd":"/tmp/acp-snap-cwd-28z5Of"} +{"type":"session","version":0,"id":"14dda109-5728-45ba-a002-7db9543fe50e","createdAt":1783352126247,"cwd":"/tmp/acp-snap-cwd-28z5Of","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352126251,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352126251,"data":{"content":[{"type":"text","text":"Use the subagent tool TWICE, once at a time, to delegate two subtasks to child agents. First subtask: 'Reply with exactly the word ALPHA and nothing else.' Second subtask (after the first returns): 'Reply with exactly the word BETA and nothing else.' After both subagents return, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352126252,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl index 8dd26c4e70..3d0bf1e801 100644 --- a/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"ea339828-7885-42e1-9083-4355e6f1708d","createdAt":1783352120855,"cwd":"/tmp/acp-snap-cwd-rbeWyt","parentSession":"5138ed0d-e86e-4a7d-b75b-803307e92b17"} +{"type":"session","version":0,"id":"ea339828-7885-42e1-9083-4355e6f1708d","createdAt":1783352120855,"cwd":"/tmp/acp-snap-cwd-rbeWyt","parentSession":"5138ed0d-e86e-4a7d-b75b-803307e92b17","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783352120856,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352120856,"data":{"content":[{"type":"text","text":"Reply with exactly the word CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352120856,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl b/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl index 6a87cfabb5..655fb4f2a0 100644 --- a/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"5138ed0d-e86e-4a7d-b75b-803307e92b17","createdAt":1783352119267,"cwd":"/tmp/acp-snap-cwd-rbeWyt"} +{"type":"session","version":0,"id":"5138ed0d-e86e-4a7d-b75b-803307e92b17","createdAt":1783352119267,"cwd":"/tmp/acp-snap-cwd-rbeWyt","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352119273,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352119274,"data":{"content":[{"type":"text","text":"Use the subagent tool exactly once to delegate this subtask to a child agent: 'Reply with exactly the word CHILD_OK and nothing else.' After the subagent returns, reply with the single word PARENT_DONE and stop. Do not use the bash tool."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352119275,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/text-turn/session.jsonl b/examples/acp-agent/tests/snapshots/text-turn/session.jsonl index 04ac2f9794..1f2209ca19 100644 --- a/examples/acp-agent/tests/snapshots/text-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/text-turn/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"539aa64c-7f37-40ff-abd8-ed45b717be1b","createdAt":1783600629539,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-ka5r8w"} +{"type":"session","version":0,"id":"539aa64c-7f37-40ff-abd8-ed45b717be1b","createdAt":1783600629539,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-ka5r8w","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783600629541,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783600629541,"data":{"content":[{"type":"text","text":"Reply with exactly the word: PONG. Do not use any tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783600629542,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/text-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/text-turn/system-prompt.expected.md index 43ecb9746f..ddf502a773 100644 --- a/examples/acp-agent/tests/snapshots/text-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/text-turn/system-prompt.expected.md @@ -5,6 +5,12 @@ You are a coding assistant powered by the deepseek-v4-flash model. Your working 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. 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 e422a063da..151e76201b 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 @@ -45,6 +45,72 @@ ] } }, + { + "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": "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.", @@ -271,6 +337,39 @@ "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": [] diff --git a/examples/acp-agent/tests/snapshots/todo-plan/session.jsonl b/examples/acp-agent/tests/snapshots/todo-plan/session.jsonl index 909afc44cd..cea8a4fa88 100644 --- a/examples/acp-agent/tests/snapshots/todo-plan/session.jsonl +++ b/examples/acp-agent/tests/snapshots/todo-plan/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"b0f1f758-dcf0-474e-851d-e62c11ec0a09","createdAt":1783352057652,"cwd":"/tmp/acp-snap-cwd-AYilT7"} +{"type":"session","version":0,"id":"b0f1f758-dcf0-474e-851d-e62c11ec0a09","createdAt":1783352057652,"cwd":"/tmp/acp-snap-cwd-AYilT7","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352057655,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352057655,"data":{"content":[{"type":"text","text":"Use the todo_write tool to record a plan with exactly three todos: \"read the code\" (in_progress), \"write the fix\" (pending), \"run the tests\" (pending). Send all three in one todo_write call. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352057657,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl b/examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl index f631c9bf54..3b0e93b464 100644 --- a/examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl +++ b/examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"e9421ff4-baae-4807-a7ea-fd8a65f2c897","createdAt":1783352044766,"cwd":"/tmp/acp-snap-cwd-OwUkBh"} +{"type":"session","version":0,"id":"e9421ff4-baae-4807-a7ea-fd8a65f2c897","createdAt":1783352044766,"cwd":"/tmp/acp-snap-cwd-OwUkBh","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352044771,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352044771,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo SNAPSHOT_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352044773,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl b/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl index 3d89428bbd..eb8d9ed63e 100644 --- a/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl +++ b/examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"583a4db2-3350-436c-b4a5-5615fd159052","createdAt":1783600636316,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-vdJYjz","parentSession":"3fd7d599-56b1-493a-930d-f1fc5e1556e8"} +{"type":"session","version":0,"id":"583a4db2-3350-436c-b4a5-5615fd159052","createdAt":1783600636316,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-vdJYjz","parentSession":"3fd7d599-56b1-493a-930d-f1fc5e1556e8","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783600636316,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783600636316,"data":{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783600636316,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/workflow-run/session.jsonl b/examples/acp-agent/tests/snapshots/workflow-run/session.jsonl index 3e0ae3da73..20f4e296cd 100644 --- a/examples/acp-agent/tests/snapshots/workflow-run/session.jsonl +++ b/examples/acp-agent/tests/snapshots/workflow-run/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"3fd7d599-56b1-493a-930d-f1fc5e1556e8","createdAt":1783600631835,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-vdJYjz"} +{"type":"session","version":0,"id":"3fd7d599-56b1-493a-930d-f1fc5e1556e8","createdAt":1783600631835,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-vdJYjz","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783600631838,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783600631838,"data":{"content":[{"type":"text","text":"Use the workflow tool exactly once, with args omitted, meta set to { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }, and this EXACT script body (copy it verbatim):\nphase('Run')\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\nreturn { reply }\nAfter the workflow returns, reply with the single word WORKFLOW_DONE and stop. Do not use any other tool."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783600631839,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl b/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl index b133708b9a..9e08a7800b 100644 --- a/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783778297065,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783778297066,"data":{"content":[{"type":"text","text":"Read nested/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783778297069,"data":{"turn":1,"step":1}} @@ -11,7 +11,7 @@ {"type":"assistant/message","seq":9,"time":1783778297070,"data":{"turn":1,"step":1,"content":[{"type":"tool-call","id":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[4,5,6,7,8],"surfaceOp":"append"} {"type":"tool/call","seq":10,"time":1783778297070,"data":{"turn":1,"step":1,"callId":"call_workspace_read","name":"read","arguments":"{\"file_path\":\"nested/task.txt\"}"}} {"type":"tool/result","seq":11,"time":1783778297072,"data":{"turn":1,"step":1,"callId":"call_workspace_read","content":[{"type":"text","text":"{{cwd}}/nested/task.txt\nfile\n\n1: snapshot task\n\n(End of file - total 1 lines)\n"}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"} -{"type":"context/message","seq":12,"time":1783778297072,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"plugin","plugin":"workspace-context"},"envelope":"raw","meta":{"kind":"workspace-instructions","version":1,"changes":[{"action":"set","scope":"nested","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]}},"surfaceOp":"append"} +{"type":"context/message","seq":12,"time":1783778297072,"data":{"content":[{"type":"text","text":"\nAdditional instructions from: nested/AGENTS.md\n\nThese instructions apply to work under `nested`. Use them as guidance when relevant; more specific instructions take precedence. They do not override system, developer, or direct user instructions.\n\nNested snapshot instruction.\n\n"}],"source":{"kind":"plugin","plugin":"workspace-context"},"meta":{"kind":"workspace-instructions","version":1,"changes":[{"action":"set","scope":"nested","path":"nested/AGENTS.md","digest":"c446df9a85c7e73a3055f394a4822a19ac9ead5a"}]}},"surfaceOp":"append"} {"type":"step/end","seq":13,"time":1783778297072,"data":{"turn":1,"step":1}} {"type":"step/start","seq":14,"time":1783778297072,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":15,"time":1783778297073,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} 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 4b08a1e365..151e76201b 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 @@ -66,6 +66,18 @@ "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": [ @@ -339,6 +351,18 @@ "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": [ diff --git a/examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl b/examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl index 6b9b03a95e..4a5b37a374 100644 --- a/examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl +++ b/examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"48aca674-000a-4583-810b-01f8785cef13","createdAt":1783352264076,"cwd":"/tmp/acp-snap-cwd-rxbEpP"} +{"type":"session","version":0,"id":"48aca674-000a-4583-810b-01f8785cef13","createdAt":1783352264076,"cwd":"/tmp/acp-snap-cwd-rxbEpP","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352264080,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352264081,"data":{"content":[{"type":"text","text":"A file named greeting.txt in the current directory contains one word. Use the bash tool to append a second line containing the word WORLD to it (so it has two lines), then read the file back with `cat greeting.txt` to confirm, and reply with the single word DONE. Use a single bash call per action."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352264082,"data":{"turn":1,"step":1}} diff --git a/examples/acp-agent/tests/snapshots/workspace-edit/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/workspace-edit/system-prompt.expected.md deleted file mode 100644 index ddf502a773..0000000000 --- a/examples/acp-agent/tests/snapshots/workspace-edit/system-prompt.expected.md +++ /dev/null @@ -1,21 +0,0 @@ -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. - -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. diff --git a/examples/acp-agent/tests/snapshots/workspace-edit/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/workspace-edit/tool-schemas.expected.json deleted file mode 100644 index 4b08a1e365..0000000000 --- a/examples/acp-agent/tests/snapshots/workspace-edit/tool-schemas.expected.json +++ /dev/null @@ -1,352 +0,0 @@ -{ - "initial": [ - { - "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": "edit", - "description": "Edit an existing UTF-8 text file by replacing literal text.", - "parameters": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Path to edit, resolved by the filesystem backend." - }, - "old_string": { - "type": "string", - "description": "Literal text to replace. Must match exactly." - }, - "new_string": { - "type": "string", - "description": "Literal replacement text. Use an empty string to delete the match." - }, - "replace_all": { - "type": "boolean", - "description": "Replace all matches. Defaults to false; when false, old_string must appear exactly once." - } - }, - "required": [ - "file_path", - "old_string", - "new_string" - ] - } - }, - { - "name": "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": "workflow", - "description": "Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const — no oneOf/pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.", - "parameters": { - "type": "object", - "properties": { - "script": { - "type": "string", - "description": "The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)." - }, - "meta": { - "type": "object", - "description": "The workflow identity block (plain JSON — never code).", - "properties": { - "name": { - "type": "string", - "description": "Short kebab-case workflow name." - }, - "description": { - "type": "string", - "description": "One-line description of what the workflow does." - }, - "whenToUse": { - "type": "string", - "description": "Optional guidance on when this workflow applies." - }, - "phases": { - "type": "array", - "description": "Optional phase declarations matched by phase() calls.", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "description": "The phase title phase() calls match by exact string." - }, - "detail": { - "type": "string", - "description": "Optional one-line description of the phase." - }, - "provider": { - "type": "string", - "description": "Optional provider override this phase is expected to use." - }, - "model": { - "type": "string", - "description": "Optional model override this phase is expected to use." - } - }, - "required": [ - "title" - ] - } - } - }, - "required": [ - "name", - "description" - ] - }, - "args": { - "type": "object", - "description": "Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]})." - } - }, - "required": [ - "script", - "meta" - ] - } - }, - { - "name": "write", - "description": "Create or fully replace a UTF-8 text file.", - "parameters": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "Path to write, resolved by the filesystem backend." - }, - "content": { - "type": "string", - "description": "Full UTF-8 text content to write." - } - }, - "required": [ - "file_path", - "content" - ] - } - } - ], - "changes": [] -} diff --git a/examples/acp-agent/workspace-context.cordis.snapshot.yml b/examples/acp-agent/workspace-context.cordis.snapshot.yml index e0b02cbb21..c5cccac519 100644 --- a/examples/acp-agent/workspace-context.cordis.snapshot.yml +++ b/examples/acp-agent/workspace-context.cordis.snapshot.yml @@ -25,13 +25,5 @@ Verify your work by running the code or tests. Keep answers brief and factual. - insert: - - id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.cwd() - - id: fs-policy - name: '@deepseek-ai/dsh-fs-policy' - - id: tool-fs - name: '@deepseek-ai/dsh-tool-fs' - id: llm-replay name: '@deepseek-ai/dsh-llm-replay' diff --git a/examples/acp-agent/workspace-context.cordis.yml b/examples/acp-agent/workspace-context.cordis.yml index 1b8c0279f0..9f422f65b9 100644 --- a/examples/acp-agent/workspace-context.cordis.yml +++ b/examples/acp-agent/workspace-context.cordis.yml @@ -21,12 +21,3 @@ You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. Verify your work by running the code or tests. Keep answers brief and factual. - - insert: - - id: fs-local - name: '@deepseek-ai/dsh-fs-local' - config: - cwd: !!js process.cwd() - - id: fs-policy - name: '@deepseek-ai/dsh-fs-policy' - - id: tool-fs - name: '@deepseek-ai/dsh-tool-fs' diff --git a/examples/headless-agent/cordis.yml b/examples/headless-agent/cordis.yml index 7f48c529c4..1cd161eea7 100644 --- a/examples/headless-agent/cordis.yml +++ b/examples/headless-agent/cordis.yml @@ -64,12 +64,14 @@ config: provider: spawn toolName: subagent + maxDepth: 1 - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: provider: fork toolName: subagent_fork + maxDepth: 1 # The worker-thread workflow engine fans a model-written JavaScript script's # `agent()` calls out through the spawn backend. diff --git a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl index b4e8cd6340..d8affeaab8 100644 --- a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl +++ b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"/tmp/advanced-headless","parentSession":"11111111-1111-4111-8111-111111111111"} +{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"/tmp/advanced-headless","parentSession":"11111111-1111-4111-8111-111111111111","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783957884563,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783957884563,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783957884564,"data":{"turn":1,"step":1}} diff --git a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl index d8d2e59e6e..f0b6154472 100644 --- a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl +++ b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"/tmp/advanced-headless","parentSession":"11111111-1111-4111-8111-111111111111"} +{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"/tmp/advanced-headless","parentSession":"11111111-1111-4111-8111-111111111111","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783957884700,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783957884700,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783957884700,"data":{"turn":1,"step":1}} diff --git a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl index e3849110ce..045d7879df 100644 --- a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl +++ b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1783950000000,"cwd":"/tmp/advanced-headless"} +{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1783950000000,"cwd":"/tmp/advanced-headless","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783957884479,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783957884479,"data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: mount a no-op Cordis plugin named snapshot-marker; use run_code to inspect the live dynamic mounts through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; unmount dyn-1; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783957884486,"data":{"turn":1,"step":1}} diff --git a/examples/package.json b/examples/package.json index 0830be7159..2bfe53e634 100644 --- a/examples/package.json +++ b/examples/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.1", "type": "module", - "description": "Workspace umbrella for runnable demos and example-owned test compositions: declares their cordis.yml packages so plain Node resolves real exports→lib. Not a build target.", + "description": "Workspace umbrella for runnable demos and example-owned test compositions: declares their cordis.yml packages so plain Node resolves real exports\u2192lib. Not a build target.", "dependencies": { "@cordisjs/plugin-hmr": "workspace:*", "@cordisjs/plugin-include": "workspace:*", @@ -14,9 +14,11 @@ "@deepseek-ai/dsh-cli-demo": "workspace:*", "@deepseek-ai/dsh-code-runtime-worker": "workspace:*", "@deepseek-ai/dsh-compact-basic": "workspace:*", + "@deepseek-ai/dsh-compact-tool-result-prune": "workspace:*", "@deepseek-ai/dsh-fs-local": "workspace:*", "@deepseek-ai/dsh-fs-policy": "workspace:*", "@deepseek-ai/dsh-goal": "workspace:*", + "@deepseek-ai/dsh-fs-sandbox": "workspace:^", "@deepseek-ai/dsh-hooks-claude": "workspace:*", "@deepseek-ai/dsh-hooks-codex": "workspace:*", "@deepseek-ai/dsh-jsonrpc": "workspace:*", @@ -26,6 +28,7 @@ "@deepseek-ai/dsh-permission": "workspace:*", "@deepseek-ai/dsh-repeat-tool-guard": "workspace:*", "@deepseek-ai/dsh-sandbox-local": "workspace:*", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*", "@deepseek-ai/dsh-spill-local": "workspace:*", "@deepseek-ai/dsh-spill-policy": "workspace:*", diff --git a/examples/repl-agent/README.md b/examples/repl-agent/README.md index 53cc68bb1a..f89e24618c 100644 --- a/examples/repl-agent/README.md +++ b/examples/repl-agent/README.md @@ -50,6 +50,7 @@ This example is a thin leaf `cordis.yml`: it picks the swappable backends, loads | `llm-deepseek` | real `LlmAdapter` via config (`!!js process.env.…` secrets); swap one line to `@deepseek-ai/dsh-llm-pi-ai` for the library-backed twin | | `bash` (`dsh-bash-local`) | the executor implementation — the swappable half of the bash seam. The model-facing `bash` schema (`tool-bash`) and generic `task_*` controls (`tool-tasks`) come from `dsh-agent-spine-demo`, so only the executor is a leaf choice | | `stdio-agent` (`@deepseek-ai/dsh-stdio-demo`) | the app bundle: the agent-spine demo + JSONL persistence + the configured terminal channel + a pre-created `main` agent. This leaf fixes `ui.mode` to `readline`; `tui-agent` owns the corresponding TUI leaf | +| `token-meter`, `tool-result-prune`, `compact-basic` | replay-aware pressure, model-free oversized tool-result pruning, and LLM summary compaction. Pruning runs only after a compaction trigger qualifies and can avoid the summarization call | | `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 | @@ -61,7 +62,7 @@ This example is a thin leaf `cordis.yml`: it picks the swappable backends, loads - `tests/full-loop.e2e.ts` — the canary: real model runs `echo e2e-ok` through the real bash tool; asserts `tool/call`/`tool/result` session events and the final answer. - `tests/coding-task.e2e.ts` — the swebench-style smoke: a temp dir holds `add.js` (with `a - b` where `a + b` belongs) and a failing `add.test.js`; the agent must fix the bug and verify. The test re-runs `node add.test.js` ITSELF and inspects the files — agent claims are not trusted. - `tests/resume.e2e.ts` — durable continuity across processes: run 1 tells the real model a secret code and persists the turn to a temp JSONL root, then the whole context is disposed; run 2 is a fresh context over the same root that RESUMES the session id and asks the model to recall the code. The recall can only come from the rehydrated log. -- `tests/compaction.e2e.ts` — the compaction smoke: a real multi-step bash task runs with a deliberately tiny context window so the auto-compaction listener fires MID-SESSION. Verifies the WORLD — a `compact/start…end` pair landed in the real log, the surface shrank (a replace node shadowed older nodes), and the agent still produced a correct final answer after compaction. +- `tests/compaction.e2e.ts` — the compaction smoke: a real multi-step bash task runs with a deliberately tiny context window so automatic pruning or summary compaction fires mid-session. It verifies the world: a replayable surface replacement lands, summary brackets are complete when summarization is needed, the surface shrinks, and the agent still produces a correct final answer. - `tests/todo-write.e2e.ts` — a real model drives the real `todo_write` tool and the test verifies the resulting `todo/write` session event. These self-skip without `DEEPSEEK_API_KEY`. `tests/code-mode.e2e.ts` is 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. The keyless Loader smokes run in the default e2e gate: `tests/keyless-smoke.e2e.ts` and `tests/code-mode-keyless-smoke.e2e.ts`. diff --git a/examples/repl-agent/composition.md b/examples/repl-agent/composition.md index af3f810585..6d298e7a0a 100644 --- a/examples/repl-agent/composition.md +++ b/examples/repl-agent/composition.md @@ -3,7 +3,7 @@ # REPL Agent App Composition -The REPL agent demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package. +The REPL agent demo adds the real DeepSeek adapter, filesystem tools, todo_write, tool-result pruning, compaction, and both subagent transports on top of the stdio app package. ```mermaid flowchart LR @@ -25,6 +25,8 @@ flowchart LR bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"] plugin_repl_token_meter["token-meter
@deepseek-ai/dsh-token-meter"] cfg --> plugin_repl_token_meter + plugin_repl_tool_result_prune["tool-result-prune
@deepseek-ai/dsh-compact-tool-result-prune"] + cfg --> plugin_repl_tool_result_prune plugin_repl_compact_basic["compact-basic
@deepseek-ai/dsh-compact-basic"] cfg --> plugin_repl_compact_basic plugin_repl_subagent["subagent
@deepseek-ai/dsh-subagent"] @@ -66,6 +68,7 @@ flowchart LR | `bash` | `@deepseek-ai/dsh-bash-local` | | `stdio-agent` | `@deepseek-ai/dsh-stdio-demo` | | `token-meter` | `@deepseek-ai/dsh-token-meter` | +| `tool-result-prune` | `@deepseek-ai/dsh-compact-tool-result-prune` | | `compact-basic` | `@deepseek-ai/dsh-compact-basic` | | `subagent` | `@deepseek-ai/dsh-subagent` | | `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` | diff --git a/examples/repl-agent/cordis.yml b/examples/repl-agent/cordis.yml index a8a211a795..20a9e5c7e3 100644 --- a/examples/repl-agent/cordis.yml +++ b/examples/repl-agent/cordis.yml @@ -51,6 +51,10 @@ - id: token-meter name: '@deepseek-ai/dsh-token-meter' +# Prune oversized tool output without a model call before summary compaction. +- id: tool-result-prune + name: '@deepseek-ai/dsh-compact-tool-result-prune' + # Summarize an older range after measured pressure or a canonical provider overflow. # Service-wide policy provides pressure, retention, and one overflow-retry default. - id: compact-basic @@ -77,12 +81,14 @@ config: provider: spawn toolName: subagent + maxDepth: 1 - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: provider: fork toolName: subagent_fork + maxDepth: 1 # The worker-thread workflow engine fans a model-written JavaScript script's diff --git a/examples/repl-agent/tests/harness.ts b/examples/repl-agent/tests/harness.ts index eeba57fc61..edf611e89d 100644 --- a/examples/repl-agent/tests/harness.ts +++ b/examples/repl-agent/tests/harness.ts @@ -9,6 +9,7 @@ import * as ToolTodo from '@deepseek-ai/dsh-tool-todo' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import TokenMeterService from '@deepseek-ai/dsh-token-meter' import type { TokenMeterConfig } from '@deepseek-ai/dsh-token-meter' +import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune' import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' import { BasicCompactService } from '@deepseek-ai/dsh-compact-basic' import type { BasicCompactConfig } from '@deepseek-ai/dsh-compact-basic' @@ -63,6 +64,7 @@ export async function codingHarness(workdir: string, options: CodingHarnessOptio // backend, with a lower context window so a short real session crosses the threshold. if (options.compact !== undefined) { await ctx.plugin(TokenMeterService, options.tokenMeter) + await ctx.plugin(ToolResultPruneService) await ctx.plugin(BasicCompactService, options.compact) } // Durable JSONL persistence is opt-in: only the resume e2e needs it, and the diff --git a/examples/tui-agent/tests/snapshots/bash-terminal-card/session.jsonl b/examples/tui-agent/tests/snapshots/bash-terminal-card/session.jsonl index e53f4b3da3..5c3bba5676 100644 --- a/examples/tui-agent/tests/snapshots/bash-terminal-card/session.jsonl +++ b/examples/tui-agent/tests/snapshots/bash-terminal-card/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"e128dda9-ed11-4868-8266-0ef90d03c3d6","createdAt":1783352050748,"cwd":"/tmp/acp-snap-cwd-mrFUuk"} +{"type":"session","version":0,"id":"e128dda9-ed11-4868-8266-0ef90d03c3d6","createdAt":1783352050748,"cwd":"/tmp/acp-snap-cwd-mrFUuk","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352050753,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352050753,"data":{"content":[{"type":"text","text":"Use the bash tool to run exactly: echo TERMINAL_OK. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352050755,"data":{"turn":1,"step":1}} diff --git a/examples/tui-agent/tests/snapshots/code-mode/session.jsonl b/examples/tui-agent/tests/snapshots/code-mode/session.jsonl index 0fe57055e8..8ee160ba26 100644 --- a/examples/tui-agent/tests/snapshots/code-mode/session.jsonl +++ b/examples/tui-agent/tests/snapshots/code-mode/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"94cd1ae4-e1d1-4ec8-9d27-50a1f849b6b3","createdAt":1783611771392,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-BteTVR"} +{"type":"session","version":0,"id":"94cd1ae4-e1d1-4ec8-9d27-50a1f849b6b3","createdAt":1783611771392,"cwd":"/var/folders/2c/psb0_fmx7hbgz558xjt_f0l00000gn/T/acp-snap-cwd-BteTVR","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783611771394,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783611771394,"data":{"content":[{"type":"text","text":"Using ONE run_code program: call the bash tool twice — exactly `echo CODE_ONE` then exactly `echo CODE_TWO` — and return the two outputs joined with a plus sign. Then reply with that joined string only and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783611771396,"data":{"turn":1,"step":1}} diff --git a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.1.jsonl b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.1.jsonl index 25a6f76411..26519d458e 100644 --- a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.1.jsonl +++ b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.1.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"/tmp/advanced-acp","parentSession":"11111111-1111-4111-8111-111111111111"} +{"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"/tmp/advanced-acp","parentSession":"11111111-1111-4111-8111-111111111111","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783957884563,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783957884563,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783957884564,"data":{"turn":1,"step":1}} diff --git a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.2.jsonl b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.2.jsonl index 45d9043a4a..9daa8958dc 100644 --- a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.2.jsonl +++ b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.2.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"/tmp/advanced-acp","parentSession":"11111111-1111-4111-8111-111111111111"} +{"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"/tmp/advanced-acp","parentSession":"11111111-1111-4111-8111-111111111111","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783957884700,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783957884700,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783957884700,"data":{"turn":1,"step":1}} diff --git a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.jsonl b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.jsonl index 39f867984a..46c8e41257 100644 --- a/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.jsonl +++ b/examples/tui-agent/tests/snapshots/cordis-dynamic-toolchain/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1783950000000,"cwd":"/tmp/advanced-acp"} +{"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1783950000000,"cwd":"/tmp/advanced-acp","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783957884479,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783957884479,"data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: mount a no-op Cordis plugin named snapshot-marker; use run_code to inspect the live dynamic mounts through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; unmount dyn-1; then reply with exactly ADVANCED_ACP_OK."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783957884486,"data":{"turn":1,"step":1}} diff --git a/examples/tui-agent/tests/snapshots/dynamic-workflow/session.1.jsonl b/examples/tui-agent/tests/snapshots/dynamic-workflow/session.1.jsonl index 3d89428bbd..eb8d9ed63e 100644 --- a/examples/tui-agent/tests/snapshots/dynamic-workflow/session.1.jsonl +++ b/examples/tui-agent/tests/snapshots/dynamic-workflow/session.1.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"583a4db2-3350-436c-b4a5-5615fd159052","createdAt":1783600636316,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-vdJYjz","parentSession":"3fd7d599-56b1-493a-930d-f1fc5e1556e8"} +{"type":"session","version":0,"id":"583a4db2-3350-436c-b4a5-5615fd159052","createdAt":1783600636316,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-vdJYjz","parentSession":"3fd7d599-56b1-493a-930d-f1fc5e1556e8","delegationDepth":1} {"type":"turn/start","seq":0,"time":1783600636316,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783600636316,"data":{"content":[{"type":"text","text":"Reply with exactly the word WF_CHILD_OK and nothing else."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783600636316,"data":{"turn":1,"step":1}} diff --git a/examples/tui-agent/tests/snapshots/dynamic-workflow/session.jsonl b/examples/tui-agent/tests/snapshots/dynamic-workflow/session.jsonl index 3e0ae3da73..20f4e296cd 100644 --- a/examples/tui-agent/tests/snapshots/dynamic-workflow/session.jsonl +++ b/examples/tui-agent/tests/snapshots/dynamic-workflow/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"3fd7d599-56b1-493a-930d-f1fc5e1556e8","createdAt":1783600631835,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-vdJYjz"} +{"type":"session","version":0,"id":"3fd7d599-56b1-493a-930d-f1fc5e1556e8","createdAt":1783600631835,"cwd":"/var/folders/bn/vj1dvck95yd5jh3x4wskflxm0000gn/T/acp-snap-cwd-vdJYjz","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783600631838,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783600631838,"data":{"content":[{"type":"text","text":"Use the workflow tool exactly once, with args omitted, meta set to { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }, and this EXACT script body (copy it verbatim):\nphase('Run')\nconst reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.')\nreturn { reply }\nAfter the workflow returns, reply with the single word WORKFLOW_DONE and stop. Do not use any other tool."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783600631839,"data":{"turn":1,"step":1}} diff --git a/examples/tui-agent/tests/snapshots/multi-turn-conversation/session.jsonl b/examples/tui-agent/tests/snapshots/multi-turn-conversation/session.jsonl index cd32072eaa..83ccf18a3f 100644 --- a/examples/tui-agent/tests/snapshots/multi-turn-conversation/session.jsonl +++ b/examples/tui-agent/tests/snapshots/multi-turn-conversation/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"228b7b82-84ed-49b7-a567-981c03b28c77","createdAt":1783352113760,"cwd":"/tmp/acp-snap-cwd-aN2GRR"} +{"type":"session","version":0,"id":"228b7b82-84ed-49b7-a567-981c03b28c77","createdAt":1783352113760,"cwd":"/tmp/acp-snap-cwd-aN2GRR","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352113765,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352113765,"data":{"content":[{"type":"text","text":"Reply with exactly the word: ONE. No tools."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352113767,"data":{"turn":1,"step":1}} diff --git a/examples/tui-agent/tests/snapshots/parallel-file-reads/session.jsonl b/examples/tui-agent/tests/snapshots/parallel-file-reads/session.jsonl index 81503dc4cf..e83f0cd59c 100644 --- a/examples/tui-agent/tests/snapshots/parallel-file-reads/session.jsonl +++ b/examples/tui-agent/tests/snapshots/parallel-file-reads/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} {"type":"turn/start","seq":0,"time":0,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Use the read tool twice in the same assistant message: read a.txt and b.txt. Then reply DONE."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":0,"data":{"turn":1,"step":1}} diff --git a/examples/tui-agent/tests/snapshots/todo-plan/session.jsonl b/examples/tui-agent/tests/snapshots/todo-plan/session.jsonl index 909afc44cd..cea8a4fa88 100644 --- a/examples/tui-agent/tests/snapshots/todo-plan/session.jsonl +++ b/examples/tui-agent/tests/snapshots/todo-plan/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","version":0,"id":"b0f1f758-dcf0-474e-851d-e62c11ec0a09","createdAt":1783352057652,"cwd":"/tmp/acp-snap-cwd-AYilT7"} +{"type":"session","version":0,"id":"b0f1f758-dcf0-474e-851d-e62c11ec0a09","createdAt":1783352057652,"cwd":"/tmp/acp-snap-cwd-AYilT7","delegationDepth":0} {"type":"turn/start","seq":0,"time":1783352057655,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}} {"type":"user/message","seq":1,"time":1783352057655,"data":{"content":[{"type":"text","text":"Use the todo_write tool to record a plan with exactly three todos: \"read the code\" (in_progress), \"write the fix\" (pending), \"run the tests\" (pending). Send all three in one todo_write call. Then reply with the single word DONE and stop."}],"source":{"kind":"user"}},"surfaceOp":"append"} {"type":"step/start","seq":2,"time":1783352057657,"data":{"turn":1,"step":1}} diff --git a/knip.json b/knip.json index 6f04363494..00f53b1782 100644 --- a/knip.json +++ b/knip.json @@ -2,7 +2,7 @@ "$schema": "https://unpkg.com/knip@5/schema.json", "exclude": ["duplicates"], "ignoreBinaries": ["bwrap", "python3", "sandbox-exec"], - "ignoreWorkspaces": ["vendor/*", "python/sdk-runtime", "website"], + "ignoreWorkspaces": ["vendor/*", "python/sdk-runtime"], "workspaces": { ".": { "project": ["scripts/**/*.ts"] @@ -19,6 +19,16 @@ "project": ["**/*.ts"], "ignoreDependencies": ["@deepseek-ai/.+", "@cordisjs/.+"] }, + "website": { + "project": ["**/*.ts"], + "ignoreDependencies": [ + "@braintree/sanitize-url", + "cytoscape", + "cytoscape-cose-bilkent", + "dayjs", + "debug" + ] + }, "packages/*/*": { "entry": ["tests/**/*.spec.ts"], "project": ["src/**/*.ts", "tests/**/*.ts"] diff --git a/package.json b/package.json index ad4704bf98..995a2bf998 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,12 @@ "verify-translation-prompt": "tsx scripts/verify-translation-prompt.ts", "verify-translation-pairing": "tsx scripts/verify-translation-pairing.ts", "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: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", + "website:build": "pnpm run docs:build", "verify-package-readme-limitations": "tsx scripts/verify-package-readme-limitations.ts", "verify-node-next-types": "tsx scripts/verify-node-next-types.ts", "verify-runtime-closure": "tsx scripts/verify-runtime-closure.ts", @@ -69,13 +75,8 @@ "gen-scoped-events": "tsx scripts/gen-scoped-events.ts", "verify-scoped-events": "tsx scripts/gen-scoped-events.ts --check", "verify-module-graph": "tsx scripts/gen-module-graph.ts --check", - "gen-website-api": "tsx scripts/gen-website-api.ts", - "verify-website-api": "tsx scripts/gen-website-api.ts --check", - "verify-website-yaml": "tsx scripts/verify-website-yaml.ts", - "website:dev": "pnpm --filter @deepseek-ai/website run dev", - "website:build": "pnpm --filter @deepseek-ai/website run build", "constraints": "tsx scripts/check-workspace-constraints.ts", - "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-events && pnpm run verify-website-api && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-package-readme-model-experience && pnpm run verify-mermaid && pnpm run verify-agent-note-classification && pnpm run verify-agent-note-format && pnpm run verify-type-equiv && pnpm run verify-translation-prompt && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run verify-package-readme-limitations && pnpm run verify-website-yaml", + "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-events && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-package-readme-model-experience && pnpm run verify-mermaid && pnpm run verify-agent-note-classification && pnpm run verify-agent-note-format && pnpm run verify-type-equiv && pnpm run verify-translation-prompt && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets && pnpm run verify-package-readme-limitations && pnpm run docs:check", "hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-runtime-closure", "demo:echo": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/echo-agent/cordis.yml", "demo:repl": "node --expose-internals --import tsx packages/examples/stdio-demo/src/bin.ts examples/repl-agent/cordis.yml", diff --git a/packages/bash/bash-sandbox/README.md b/packages/bash/bash-sandbox/README.md index d08c9b4550..97419b45d4 100644 --- a/packages/bash/bash-sandbox/README.md +++ b/packages/bash/bash-sandbox/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-bash-sandbox -Sandbox-consuming implementation of the [`@deepseek-ai/dsh-bash`](../bash/) executor seam. Load it **instead of** `@deepseek-ai/dsh-bash-local`, together with a [`ctx.sandbox`](../../sandbox/sandbox/) provider (e.g. [`@deepseek-ai/dsh-sandbox-local`](../../sandbox/sandbox-local/)) — no alternate tool plugin is needed; `dsh-tool-bash` detects the executor's `sandboxMode` capability and adds the escalation fields. +Sandbox-consuming implementation of the [`@deepseek-ai/dsh-bash`](../bash/) executor seam. Load it **instead of** `@deepseek-ai/dsh-bash-local`, together with a [`ctx.sandbox`](../../sandbox/sandbox/) provider (e.g. [`@deepseek-ai/dsh-sandbox-local`](../../sandbox/sandbox-local/)) and a [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) (which owns the default mode + workspace root, shared with the sandboxed filesystem) — no alternate tool plugin is needed; `dsh-tool-bash` detects the executor's `sandboxMode` capability and adds the escalation fields. The package root exports the default and named `SandboxBashExecutor` plugin plus its `Config`; quoting and result-classification helpers stay internal. @@ -16,7 +16,7 @@ Semantics: - **Denials are result facts.** A failed run whose stderr carries the selected backend's own denial dialect — the signatures the provider stamps on every wrap (EROFS text under bwrap, EACCES under Landlock, EPERM under Seatbelt) — is reported as `BashRunResult.sandbox.denied: true` (conservative classification, read from the collected stderr tail); every CONFINED run also carries the mode it executed under (`result.sandbox.mode`) and the provider's enforcement completeness (`result.sandbox.enforcement`: `full`, or `partial` on an older Landlock ABI). - **Runner failures are sandbox failures, never command failures.** Foreground execution throws `SANDBOX_UNAVAILABLE`; a settled background process stamps `process.sandbox.runnerFailed`, which the bash producer renders through generic `task_output`. Spawn failures also pass through settlement, so confined background handles retain their mode/enforcement facts and release per-process accounting. -- **Config-time default, per-call policy.** The DEFAULT mode is fixed by this entry's config for the executor's lifetime; `resolve()` stamps it onto every spec, and an explicit request-level `sandboxMode` override — set by the tool layer only for a call whose wider mode a human granted through `ctx.approval` ([the sandbox Agent Note § Escalation](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)) — makes THAT call run, classify, and report under its own mode while every neighbor keeps the default (background facts are stamped per task at settle). The capability fact `ctx.bash.sandboxMode` reports the configured default so the tool layer advertises escalation only when this executor is mounted. The model learns of the sandbox only through result facts — the static bash tool description explains the denial marker; there is no current-mode statement in the system prompt. +- **Deployment default, per-call policy.** The DEFAULT mode + workspace root are owned by [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/) (one home both enforcing families read), not this executor's config; `resolve()` stamps the default onto every spec, and an explicit request-level `sandboxMode` override — set by the tool layer only for a call whose wider mode a human granted through `ctx.approval` ([the sandbox Agent Note § Escalation](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)) — makes THAT call run, classify, and report under its own mode while every neighbor keeps the default (background facts are stamped per task at settle). The capability fact `ctx.bash.sandboxMode` reports the configured default so the tool layer advertises escalation only when this executor is mounted. The model learns of the sandbox only through result facts — the static bash tool description explains the denial marker; there is no current-mode statement in the system prompt. - **File effects only.** Network and process visibility are deliberately not restricted — the mode vocabulary does not pretend to cover what the backend does not enforce. - Process mechanics (spawn, process-group kills, output collection/spill, background handles, credential scrub) are inherited from [`dsh-bash-local`](../bash-local/); runner selection lives in [`dsh-sandbox-local`](../../sandbox/sandbox-local/). @@ -25,11 +25,13 @@ Deny-only at the seam: a denial is a reported fact, and this executor never nego ```yaml - id: sandbox name: '@deepseek-ai/dsh-sandbox-local' -- id: bash - name: '@deepseek-ai/dsh-bash-sandbox' +- id: sandbox-policy + name: '@deepseek-ai/dsh-sandbox-policy' config: mode: read-only workspaceRoot: !!js process.cwd() +- id: bash + name: '@deepseek-ai/dsh-bash-sandbox' ``` The keyless consumer-integration proofs are `tests/bwrap.e2e.ts`, `tests/landlock.e2e.ts`, and `tests/seatbelt.e2e.ts` (the real provider + real runner driven through `ctx.bash`, world-verified, each self-skipping where its runner is absent); see [the acp-agent example's default composition](../../../examples/acp-agent/) for the runnable demo. diff --git a/packages/bash/bash-sandbox/package.json b/packages/bash/bash-sandbox/package.json index b4f61abcbe..d4ac641f8d 100644 --- a/packages/bash/bash-sandbox/package.json +++ b/packages/bash/bash-sandbox/package.json @@ -25,16 +25,15 @@ "@deepseek-ai/dsh-bash": "^0.0.1", "@deepseek-ai/dsh-bash-local": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", + "@deepseek-ai/dsh-sandbox-policy": "^0.0.1", "cordis": "^4.0.0-rc.7" }, - "dependencies": { - "schemastery": "^3.18.0" - }, "devDependencies": { "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", "@deepseek-ai/dsh-sandbox-local": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "node-addon-landlock-run": "0.0.0-test.0", "cordis": "^4.0.0-rc.7" } diff --git a/packages/bash/bash-sandbox/src/index.ts b/packages/bash/bash-sandbox/src/index.ts index 124f6b6a9f..67b850916c 100644 --- a/packages/bash/bash-sandbox/src/index.ts +++ b/packages/bash/bash-sandbox/src/index.ts @@ -7,52 +7,39 @@ * @module @deepseek-ai/dsh-bash-sandbox */ -import { resolve } from 'node:path' import { Context } from 'cordis' -import z from 'schemastery' import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash' import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox' import type { ConfinedSandboxMode, SandboxEnforcement, SandboxMode } from '@deepseek-ai/dsh-sandbox' +import type {} from '@deepseek-ai/dsh-sandbox-policy' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' import type { Config as LocalConfig } from '@deepseek-ai/dsh-bash-local' import { classifyDenial, classifyRunnerFailure, matchesSignature, shellQuote } from './helpers.ts' /** - * Plugin config: the local executor's knobs plus the sandbox policy. All - * optional — `static Config` supplies the defaults (`mode: 'read-only'` is the - * fail-safe default; an example that wants a workspace-writable agent opts in - * explicitly). The runner choice is not configured here: which platform - * backend confines the command is the `ctx.sandbox` provider's config. + * Plugin config: the local executor's knobs, verbatim. The sandbox policy — + * the default mode and the `workspace-write` boundary root — is NOT here: it + * lives on `ctx.sandboxPolicy` (`@deepseek-ai/dsh-sandbox-policy`), the one + * home both enforcing families read, so bash and fs can never confine to + * different roots. The runner choice is likewise the `ctx.sandbox` provider's + * config, not this executor's. */ -export interface Config extends LocalConfig { - /** File-sandbox mode commands run under (default: `read-only`). */ - mode?: SandboxMode - /** - * Root directory `workspace-write` mode may write under (default: the - * executor's default working directory — `cwd`, else `process.cwd()`). - */ - workspaceRoot?: string -} +export type Config = LocalConfig /** * Registers as `ctx.bash` in place of the local executor and requires a - * `ctx.sandbox` provider; the tool layer is unchanged. The configured mode is - * the fallback, while a session override or approved one-shot escalation may - * select each call's mode. The prompt does not state the standing mode; - * `result.sandbox` reports the mode and enforcement actually used. + * `ctx.sandbox` provider plus `ctx.sandboxPolicy`; the tool layer is + * unchanged. The policy default (mode + workspace root) is the fallback, + * while a session override or approved one-shot escalation may select each + * call's mode. The prompt does not state the standing mode; `result.sandbox` + * reports the mode and enforcement actually used. */ export class SandboxBashExecutor extends LocalBashExecutor { - static inject = ['sandbox'] + static inject = ['sandbox', 'sandboxPolicy'] - // The sandbox-specific fields intersect the local executor's Config as an - // inline schema call: the config catalog walks `static Config` statically. - static override Config: z = z.intersect([ - LocalBashExecutor.Config, - z.object({ - mode: z.union(['read-only', 'workspace-write', 'danger-full-access'] as const).default('read-only'), - workspaceRoot: z.string(), - }), - ]) + // No own Config: the sandbox default (mode + workspaceRoot) moved to + // ctx.sandboxPolicy, so this executor inherits LocalBashExecutor's Config + // verbatim (the config catalog walks the inherited static). private readonly mode: SandboxMode private readonly workspaceRoot: string @@ -71,9 +58,11 @@ export class SandboxBashExecutor extends LocalBashExecutor { constructor(ctx: Context, config: Config) { super(ctx, config) - // Schemastery fills mode before construction; workspaceRoot and cwd retain runtime fallbacks. - this.mode = config.mode as SandboxMode - this.workspaceRoot = resolve(config.workspaceRoot ?? config.cwd ?? process.cwd()) + // The sandbox default (mode + workspaceRoot) is the one shared policy home + // both enforcing families read; injecting sandboxPolicy guarantees it is + // constructed first. workspaceRoot arrives already resolved absolute. + this.mode = ctx.sandboxPolicy.defaultMode + this.workspaceRoot = ctx.sandboxPolicy.workspaceRoot } /** The configured default mode — the capability fact the tool layer reads. */ diff --git a/packages/bash/bash-sandbox/tests/bwrap.e2e.ts b/packages/bash/bash-sandbox/tests/bwrap.e2e.ts index 237619ae06..32a246e42e 100644 --- a/packages/bash/bash-sandbox/tests/bwrap.e2e.ts +++ b/packages/bash/bash-sandbox/tests/bwrap.e2e.ts @@ -6,6 +6,7 @@ import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { Context } from 'cordis' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' +import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy' import { bwrapProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts' import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox' @@ -40,7 +41,8 @@ async function tempDir(base: string): Promise { async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-write'): Promise { ctx = new Context() await ctx.plugin(LocalSandboxProvider, {}) - await ctx.plugin(SandboxBashExecutor, { mode, cwd: workspace, workspaceRoot: workspace, timeoutMs: 30_000 }) + await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace }) + await ctx.plugin(SandboxBashExecutor, { cwd: workspace, timeoutMs: 30_000 }) return ctx.bash as SandboxBashExecutor } diff --git a/packages/bash/bash-sandbox/tests/landlock.e2e.ts b/packages/bash/bash-sandbox/tests/landlock.e2e.ts index 8263dad6fe..b8c86d95b2 100644 --- a/packages/bash/bash-sandbox/tests/landlock.e2e.ts +++ b/packages/bash/bash-sandbox/tests/landlock.e2e.ts @@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { Context } from 'cordis' import { launcherPath } from 'node-addon-landlock-run' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' +import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy' import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox' /** @@ -45,7 +46,8 @@ async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-w ctx = new Context() await ctx.plugin(LocalSandboxProvider, {}) ;(ctx.sandbox as LocalSandboxProvider).internals = { probeBwrap: () => false } - await ctx.plugin(SandboxBashExecutor, { mode, cwd: workspace, workspaceRoot: workspace, timeoutMs: 30_000 }) + await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace }) + await ctx.plugin(SandboxBashExecutor, { cwd: workspace, timeoutMs: 30_000 }) return ctx.bash as SandboxBashExecutor } diff --git a/packages/bash/bash-sandbox/tests/sandbox.spec.ts b/packages/bash/bash-sandbox/tests/sandbox.spec.ts index 619bc05151..5b2b3ba16a 100644 --- a/packages/bash/bash-sandbox/tests/sandbox.spec.ts +++ b/packages/bash/bash-sandbox/tests/sandbox.spec.ts @@ -12,7 +12,8 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import type { BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash' import { SANDBOX_UNAVAILABLE, SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox' -import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox' +import type { ConfinedArgv, SandboxMode, SandboxPolicy } from '@deepseek-ai/dsh-sandbox' +import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy' import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox' import { classifyDenial, classifyRunnerFailure, shellQuote } from '../src/helpers.ts' import type { Config } from '@deepseek-ai/dsh-bash-sandbox' @@ -39,7 +40,11 @@ const passthrough = (argv: readonly string[]): ConfinedArgv => * Boot a context with a recording fake `ctx.sandbox` (behavior injectable * per test) and the executor under test on top of it. */ -async function setup(config: Config = {}, behavior: (argv: readonly string[], policy: SandboxPolicy) => ConfinedArgv = passthrough) { +async function setup( + config: { mode?: SandboxMode; workspaceRoot?: string } & Config = {}, + behavior: (argv: readonly string[], policy: SandboxPolicy) => ConfinedArgv = passthrough, +) { + const { mode, workspaceRoot, ...execConfig } = config const calls: ConfineCall[] = [] class FakeSandboxProvider extends SandboxProvider { confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv { @@ -49,7 +54,11 @@ async function setup(config: Config = {}, behavior: (argv: readonly string[], po } const ctx = new Context() await ctx.plugin(FakeSandboxProvider) - await ctx.plugin(SandboxBashExecutor, { graceMs: 200, ...config }) + await ctx.plugin(SandboxPolicyService, { + ...mode !== undefined ? { mode } : {}, + ...workspaceRoot !== undefined ? { workspaceRoot } : {}, + }) + await ctx.plugin(SandboxBashExecutor, { graceMs: 200, ...execConfig }) const bash = ctx.bash as SandboxBashExecutor bash.internals = { spillDir } return { ctx, bash, calls } @@ -84,14 +93,14 @@ describe('the provider hand-off', () => { expect(result.sandbox).toEqual({ mode: 'read-only', denied: false, enforcement: 'full' }) }) - it('workspace-write rides the policy, workspaceRoot falling back to cwd when not configured', async () => { - const { bash, calls } = await setup({ mode: 'workspace-write', cwd: tmpdir() }) + it('workspace-write rides the policy, workspaceRoot falling back to process.cwd() when not configured', async () => { + const { bash, calls } = await setup({ mode: 'workspace-write' }) const result = await bash.run(bash.resolve({ command: 'true' })) expect(result.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' }) - expect(calls[0]?.policy).toEqual({ mode: 'workspace-write', workspaceRoot: resolve(tmpdir()) }) + expect(calls[0]?.policy).toEqual({ mode: 'workspace-write', workspaceRoot: resolve(process.cwd()) }) }) - it('an explicit workspaceRoot wins over cwd', async () => { + it('an explicit workspaceRoot on the policy wins', async () => { const { calls, bash } = await setup({ mode: 'workspace-write', workspaceRoot: '/ws', cwd: tmpdir() }) await bash.run(bash.resolve({ command: 'true' })) expect(calls[0]?.policy.workspaceRoot).toBe(resolve('/ws')) diff --git a/packages/bash/bash-sandbox/tests/seatbelt.e2e.ts b/packages/bash/bash-sandbox/tests/seatbelt.e2e.ts index cc3703d742..7e08ea0365 100644 --- a/packages/bash/bash-sandbox/tests/seatbelt.e2e.ts +++ b/packages/bash/bash-sandbox/tests/seatbelt.e2e.ts @@ -6,6 +6,7 @@ import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { Context } from 'cordis' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' +import { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy' import { seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts' import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox' @@ -39,7 +40,8 @@ async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-w ctx = new Context() await ctx.plugin(LocalSandboxProvider, {}) ;(ctx.sandbox as LocalSandboxProvider).internals = { probeBwrap: () => false, probeLandlock: () => 'unusable' } - await ctx.plugin(SandboxBashExecutor, { mode, cwd: workspace, workspaceRoot: workspace, timeoutMs: 30_000 }) + await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace }) + await ctx.plugin(SandboxBashExecutor, { cwd: workspace, timeoutMs: 30_000 }) return ctx.bash as SandboxBashExecutor } diff --git a/packages/bash/bash-sandbox/tsconfig.json b/packages/bash/bash-sandbox/tsconfig.json index 6dad98d54f..531ae140ea 100644 --- a/packages/bash/bash-sandbox/tsconfig.json +++ b/packages/bash/bash-sandbox/tsconfig.json @@ -14,9 +14,6 @@ { "path": "../../../vendor/cordis" }, - { - "path": "../../../vendor/schemastery" - }, { "path": "../../util/brand" }, @@ -26,6 +23,9 @@ { "path": "../../sandbox/sandbox" }, + { + "path": "../../sandbox/sandbox-policy" + }, { "path": "../../bash/bash" }, diff --git a/packages/bash/bash/README.md b/packages/bash/bash/README.md index 30fa92b78f..ec5005ec70 100644 --- a/packages/bash/bash/README.md +++ b/packages/bash/bash/README.md @@ -29,7 +29,7 @@ Implementations subclass `BashExecutor` and implement the abstract methods. Disp `BashExecRequest` (command, workdir?, timeoutMs?, stdoutMaxBytes?, signal?, stdin?, env?, dshEnv?, sandboxMode?) resolves to `BashExecSpec` (command, workdir, timeoutMs, stdoutMaxBytes, signal?, stdin?, env?, dshEnv?, sandboxMode) before execution. `stdoutMaxBytes` is a trusted foreground-run capture budget for consumers that must parse complete bounded stdout; the model-facing bash tool does not expose it. `sandboxMode` is optional on the request and required-but-nullable on the resolved spec: it carries an approved one-shot escalation or the session's standing override; a sandboxing executor stamps its configured default when absent, while a non-sandboxing executor carries the field and confines nothing. -The seam also owns the per-session mode override vocabulary: the log-only `'bash/sandbox-mode'` session event, the pure `effectiveSandboxMode(events)` fold, and the `setSandboxMode(session, mode)` write path. `run()` returns `BashRunResult`; `start()` returns `BashProcess`, whose incremental read and kill methods are adapted by `dsh-tool-bash` into a generic task registration. A sandboxing executor stamps `BashSandboxInfo` on foreground results and settled process handles. See `src/types.ts` and [core-data-structures/bash.md](../../../docs/core-data-structures/bash.md). +The per-session sandbox-mode override vocabulary (the `'sandbox/mode'` event, the `effectiveSandboxMode(events)` fold, and the `setSandboxMode(session, mode)` write path) is NOT here — it is policy state shared by every enforcing family, owned by [`@deepseek-ai/dsh-sandbox-policy`](../../sandbox/sandbox-policy/). `run()` returns `BashRunResult`; `start()` returns `BashProcess`, whose incremental read and kill methods are adapted by `dsh-tool-bash` into a generic task registration. A sandboxing executor stamps `BashSandboxInfo` on foreground results and settled process handles. See `src/types.ts` and [core-data-structures/bash.md](../../../docs/core-data-structures/bash.md). `stdin` and ordinary `env` are set by in-process plugins (the hooks bridges, native plugins) to feed a hook command its JSON payload and `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` values. `dshEnv` is a separate trusted overlay restricted by type to managed keys; the exported `DSH_ENV_PREFIX` is the single source for that namespace, its `DshEnvironmentKey` template type, executor scrubbing, registry validation, derived built-in names, and model guidance. Model bash uses the current snapshot collected by `ctx.bashEnv`. Implementations remove inherited managed keys, reject those names in ordinary `env`, then merge `dshEnv`, so an omitted current fact cannot fall back to stale ambient state. The model-facing tool exposes none of these as parameters. All three remain optional on the resolved spec; absent means no input/overlay. See [the bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [the session environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md). diff --git a/packages/bash/bash/package.json b/packages/bash/bash/package.json index 0783995161..5fab273e39 100644 --- a/packages/bash/bash/package.json +++ b/packages/bash/bash/package.json @@ -23,12 +23,10 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-sandbox": "^0.0.1", - "@deepseek-ai/dsh-session": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-sandbox": "workspace:^", - "@deepseek-ai/dsh-session": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/bash/bash/src/index.ts b/packages/bash/bash/src/index.ts index 75a5f7f230..d9eedee052 100644 --- a/packages/bash/bash/src/index.ts +++ b/packages/bash/bash/src/index.ts @@ -10,7 +10,6 @@ import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from './types.ts' export { DSH_ENV_PREFIX } from './types.ts' -export { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from './session-mode.ts' export type { BashExecRequest, BashExecSpec, diff --git a/packages/bash/bash/src/session-mode.ts b/packages/bash/bash/src/session-mode.ts deleted file mode 100644 index dfe8c22f4f..0000000000 --- a/packages/bash/bash/src/session-mode.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Per-session sandbox-mode override stored as log-only events. Folding the log - * isolates sessions and survives replay; the tool stamps the override onto - * each call unless an approved one-shot escalation outranks it, and the - * executor default applies when neither exists. The model receives neither the - * event nor a standing-mode notice; denial results name the effective mode. - * @module dsh-bash/session-mode - */ - -import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' -import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' - -declare module '@deepseek-ai/dsh-session' { - interface SessionEventMap { - /** - * Durable log-only sandbox-mode override; never a surface event or model - * message. Execution and ACP option reporting fold the latest event through - * {@link effectiveSandboxMode} without adding a prompt notice. - */ - 'bash/sandbox-mode': { mode: SandboxMode } - } -} - -/** Every {@link SandboxMode}, for option advertisement and runtime validation of untrusted mode strings. */ -export const SANDBOX_MODES: readonly SandboxMode[] = ['read-only', 'workspace-write', 'danger-full-access'] - -/** - * The session's sandbox-mode override: the last `bash/sandbox-mode` event in - * the log, or undefined when the session never switched and callers should use - * the executor default. Replay needs no separate catch-up state. - * @param events - session events in log order (other event types are skipped). - * @returns the mode of the last switch event, or undefined without one. - */ -export function effectiveSandboxMode(events: readonly SessionEvent[]): SandboxMode | undefined { - for (let index = events.length - 1; index >= 0; index -= 1) { - const event = events[index] as SessionEvent - if (event.type === 'bash/sandbox-mode') return event.data.mode - } - return undefined -} - -/** - * Append one `bash/sandbox-mode` event as the only override write path. - * Execution and ACP option reporting fold it on read; prompt assembly does not - * consume it. - * @param session - the session the override belongs to. - * @param mode - the mode every subsequent bash call in this session runs - * under (until the next switch). - */ -export function setSandboxMode(session: Session, mode: SandboxMode): void { - session.append('bash/sandbox-mode', { mode }) -} diff --git a/packages/bash/bash/tsconfig.json b/packages/bash/bash/tsconfig.json index b9ab094edb..de6feb55b9 100644 --- a/packages/bash/bash/tsconfig.json +++ b/packages/bash/bash/tsconfig.json @@ -16,9 +16,6 @@ }, { "path": "../../sandbox/sandbox" - }, - { - "path": "../../core/session" } ] } diff --git a/packages/bash/tool-bash/package.json b/packages/bash/tool-bash/package.json index 816be1ef88..1ce1103e48 100644 --- a/packages/bash/tool-bash/package.json +++ b/packages/bash/tool-bash/package.json @@ -23,15 +23,16 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", - "@deepseek-ai/dsh-user-approval": "^0.0.1", "@deepseek-ai/dsh-bash": "^0.0.1", "@deepseek-ai/dsh-home": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", + "@deepseek-ai/dsh-sandbox-policy": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", "@deepseek-ai/dsh-tasks": "^0.0.1", "@deepseek-ai/dsh-tools": "^0.0.1", + "@deepseek-ai/dsh-user-approval": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "dependencies": { @@ -47,6 +48,7 @@ "@deepseek-ai/dsh-home": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", @@ -54,6 +56,7 @@ "@deepseek-ai/dsh-tasks": "workspace:^", "@deepseek-ai/dsh-tool-tasks": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-user-approval": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/bash/tool-bash/src/index.ts b/packages/bash/tool-bash/src/index.ts index cad92c9276..0feec1729a 100644 --- a/packages/bash/tool-bash/src/index.ts +++ b/packages/bash/tool-bash/src/index.ts @@ -15,12 +15,13 @@ import { defineTool } from '@deepseek-ai/dsh-tools' import type { GenericCallView, TerminalCallView, ToolExecution, ToolResult, ToolResultView } from '@deepseek-ai/dsh-tools' import type { Agent } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-session-persistence' -import { assertNever } from '@deepseek-ai/dsh-llm' import type {} from '@deepseek-ai/dsh-system-prompt' import type {} from '@deepseek-ai/dsh-tasks' import type {} from '@deepseek-ai/dsh-user-approval' import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' -import { DSH_ENV_PREFIX, effectiveSandboxMode } from '@deepseek-ai/dsh-bash' +import { ESCALATION_TARGETS, approveEscalation, validateEscalationArgs } from '@deepseek-ai/dsh-sandbox' +import { effectiveSandboxMode } from '@deepseek-ai/dsh-sandbox-policy' +import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-bash' import type { DshEnvironment, DshEnvironmentKey } from '@deepseek-ai/dsh-bash' import { DSH_HOME_ENV, resolveDshHome } from '@deepseek-ai/dsh-home' import { processOutcome } from './background.ts' @@ -226,24 +227,11 @@ function validateBashArgs(args: BashToolArgs): void { if (args.timeoutMs !== undefined && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) { throw new Error(`invalid timeoutMs: expected a positive number, got ${JSON.stringify(args.timeoutMs)}`) } - if (args.sandbox_permissions !== undefined && args.justification === undefined) { - throw new Error('invalid escalation: sandbox_permissions requires a justification') - } - if (args.justification !== undefined && args.sandbox_permissions === undefined) { - throw new Error('invalid escalation: justification is only valid together with sandbox_permissions') - } - if (args.justification !== undefined && args.justification.trim().length === 0) { - throw new Error('invalid justification: expected a non-empty sentence') - } + // The escalation pairing (sandbox_permissions ⇔ justification, non-empty) is + // the shared rule both enforcing families validate identically. + validateEscalationArgs(args.sandbox_permissions, args.justification) } -const WIDER_MODES: Record = { - 'read-only': ['workspace-write', 'danger-full-access'], - 'workspace-write': ['danger-full-access'], -} - -const ESCALATION_TARGETS: readonly SandboxMode[] = ['workspace-write', 'danger-full-access'] - function bashDescription(backgroundEnabled: boolean, escalationModes: readonly SandboxMode[]): string { const background = backgroundEnabled ? '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`.' @@ -346,35 +334,32 @@ export function apply(ctx: Context, config: Config = {}): void { const sessionOverride = (exec: ToolExecution): SandboxMode | undefined => defaultMode === undefined || exec.agent === undefined ? undefined : effectiveSandboxMode(exec.agent.session.events) - const approveEscalation = async (mode: string, justification: string, exec: ToolExecution): Promise => { + /** + * Resolve a sandbox-escalation request through `ctx.approval` BEFORE + * anything executes, delegating the shared fail-closed sequence (strict + * widening, channel resolution, outcome mapping) to + * {@link approveEscalation}. This tool contributes only the composition + * guard (the fields are unadvertised without a sandboxing executor, yet + * schema validation checks advertised keys only, so an unadvertised + * `sandbox_permissions` still reaches execute) and the approval ingredients + * — the seam is consumed opportunistically (`ctx.get`) so a deployment + * without it degrades per call. + */ + const approveBashEscalation = (mode: string, justification: string, exec: ToolExecution): Promise => { if (escalationModes.length === 0) { throw new Error('sandbox_permissions is not available in this composition (no sandboxing executor to escalate)') } const effectiveMode = (sessionOverride(exec) ?? defaultMode) as SandboxMode - if (!(WIDER_MODES[effectiveMode] ?? []).includes(mode as SandboxMode)) { - throw new Error(`sandbox escalation to "${mode}" is not strictly wider than this call's current "${effectiveMode}" mode`) - } - const approval = ctx.get('approval') - if (approval === undefined) { - throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval service is composed`) - } - if (exec.agent === undefined) { - throw new Error(`sandbox escalation to "${mode}" requires approval, but the call has no agent to route it through`) - } - const outcome = await approval.request({ - agent: exec.agent, - toolName: 'bash', - callId: exec.callId, - reason: `escalate sandbox to ${mode}: ${justification}`, - ...exec.signal ? { signal: exec.signal } : {}, - }) - switch (outcome) { - case 'allowed-once': return mode as SandboxMode - case 'rejected': throw new Error(`the user rejected escalating this command to "${mode}"`) - case 'cancelled': throw new Error(`approval for escalating to "${mode}" was cancelled`) - case 'unavailable': throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval channel is available`) - default: return assertNever(outcome, 'ApprovalOutcome') - } + return approveEscalation( + { requestedMode: mode, justification, effectiveMode, subject: 'command' }, + { + approver: ctx.get('approval'), + agent: exec.agent, + callId: exec.callId, + toolName: 'bash', + ...exec.signal ? { signal: exec.signal } : {}, + }, + ) } // Cross-call guidance belongs in the prompt rather than one-call schema prose. @@ -417,7 +402,7 @@ export function apply(ctx: Context, config: Config = {}): void { validateBashArgs(args) // Description is display metadata; workdir defaults to the caller's session. const sandboxMode = args.sandbox_permissions !== undefined && args.justification !== undefined - ? await approveEscalation(args.sandbox_permissions, args.justification, exec) + ? await approveBashEscalation(args.sandbox_permissions, args.justification, exec) : sessionOverride(exec) const workdir = resolveWorkdir(args.workdir, exec) const dshEnv = bashEnv.collect(exec) diff --git a/packages/bash/tool-bash/src/render.ts b/packages/bash/tool-bash/src/render.ts index 2ce36af593..77a88e28f3 100644 --- a/packages/bash/tool-bash/src/render.ts +++ b/packages/bash/tool-bash/src/render.ts @@ -6,6 +6,7 @@ import type { BashProcessRead, BashRunResult, BashSandboxInfo, CollectedOutput } from '@deepseek-ai/dsh-bash' import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' +import { escalationHintMarker, sandboxDenialMarker } from '@deepseek-ai/dsh-sandbox' /** Append the truncation notice (with the full-output spill path) to a stream's text. */ function streamText(output: CollectedOutput): string { @@ -42,10 +43,10 @@ export function renderResult( const markers: string[] = [] // Keep the exit marker last because parseExitStatus anchors there. if (result.sandbox?.denied) { - markers.push(`[sandbox: file access denied under ${result.sandbox.mode} mode]`) + markers.push(sandboxDenialMarker(result.sandbox.mode)) // Hint only when the composition exposes escalation, before the final exit marker. if (escalationModes.length > 0) { - markers.push('[sandbox: escalation available — retry this exact command once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]') + markers.push(escalationHintMarker('command')) } } // A command may trap SIGTERM and exit 0 after timeout; still report interruption. @@ -84,9 +85,9 @@ export function renderProcessRead( if (sandbox?.runnerFailed) { notices.push(`[sandbox: the sandbox runner itself failed under ${sandbox.mode} mode — the command did not run; this is a sandbox problem, not a command failure]`) } else if (sandbox?.denied) { - notices.push(`[sandbox: file access denied under ${sandbox.mode} mode]`) + notices.push(sandboxDenialMarker(sandbox.mode)) if (escalationModes.length > 0) { - notices.push('[sandbox: escalation available — retry this exact command once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]') + notices.push(escalationHintMarker('command')) } } if (notices.length === 0) return read.delta diff --git a/packages/bash/tool-bash/tests/tools.spec.ts b/packages/bash/tool-bash/tests/tools.spec.ts index 857e08072a..04c18264b1 100644 --- a/packages/bash/tool-bash/tests/tools.spec.ts +++ b/packages/bash/tool-bash/tests/tools.spec.ts @@ -181,7 +181,7 @@ async function setupSandboxed(withApproval = false) { function sandboxAgent(mode?: 'read-only' | 'workspace-write' | 'danger-full-access', ctx?: Context): Agent { const events: Array<{ type: string; data?: Record }> = [{ type: 'turn/start' }] - if (mode !== undefined) events.push({ type: 'bash/sandbox-mode', data: { mode } }) + if (mode !== undefined) events.push({ type: 'sandbox/mode', data: { mode } }) const id = SessionId('sandbox-session') return { id, @@ -553,7 +553,7 @@ describe('sandbox escalation through the generic task producer', () => { const malformed = sandboxAgent() ;(malformed.session.events as unknown as Array<{ type: string; data: { mode: string } }>).push({ - type: 'bash/sandbox-mode', + type: 'sandbox/mode', data: { mode: 'unknown-mode' }, }) expect(text(await call(ctx, 'bash', escalate, malformed))).toContain('not strictly wider') @@ -610,7 +610,7 @@ describe('sandbox escalation through the generic task producer', () => { const { ctx } = await setupSandboxed(true) ctx.approval.request = () => Promise.resolve('rogue' as ApprovalOutcome) const result = await call(ctx, 'bash', escalate, sandboxAgent()) - expect(text(result)).toContain('unreachable variant in ApprovalOutcome') + expect(text(result)).toContain('unreachable variant in EscalationOutcome') }) }) diff --git a/packages/bash/tool-bash/tsconfig.json b/packages/bash/tool-bash/tsconfig.json index 407e78ebd8..c17b10ab8d 100644 --- a/packages/bash/tool-bash/tsconfig.json +++ b/packages/bash/tool-bash/tsconfig.json @@ -46,6 +46,9 @@ }, { "path": "../../sandbox/sandbox" + }, + { + "path": "../../sandbox/sandbox-policy" } ] } diff --git a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts index 23c9a8ee60..ae2cb2b639 100644 --- a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts @@ -129,10 +129,12 @@ describe('WorkerCodeRuntime — budgets and containment (real workers)', () => { }, 15_000) it('does not charge time spent awaiting a slow binding against the compute budget', async () => { - const { runtime } = await setup({ computeMs: 250, maxWallMs: 30_000 }) + // Keep the binding delay above the compute allowance while leaving enough + // headroom for worker bootstrap on loaded CI hosts. + const { runtime } = await setup({ computeMs: 1_000, maxWallMs: 30_000 }) const result = await runtime.run({ program: 'return await tools.slow({})', - bindings: tools({ slow: () => new Promise(resolve => setTimeout(() => { resolve('slow-done') }, 700)) }), + bindings: tools({ slow: () => new Promise(resolve => setTimeout(() => { resolve('slow-done') }, 1_500)) }), }) expect(result.error).toBeUndefined() expect(result.value).toBe('slow-done') diff --git a/packages/compact/README.md b/packages/compact/README.md index 890bfa3260..be29e6093f 100644 --- a/packages/compact/README.md +++ b/packages/compact/README.md @@ -1,11 +1,12 @@ # compact/ — compaction capability family -A three-package capability seam (see [capability seams](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract compaction interface, a backend that summarizes, and the model-facing tool that consumes it. The interface and a first backend (`compact-basic/`) exist; the consumer tool is deferred. All **product** packages. +A compaction capability family (see [capability seams](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract interface, a summarizing backend, a model-free tool-result pruning companion, and a deferred model-facing consumer. All **product** packages. | Package | Role | ctx key | |---|---|---| | `compact/` | Abstract compaction seam (interface + `compact/*` events + `CompactionResult`) | `ctx.compact` | | `compact-basic/` | A backend: `ctx.tokenMeter` pressure + token-budget retention + `llm.stream()` summarization | (registers `ctx.compact`) | +| `compact-tool-result-prune/` | Optional model-free head/middle/tail rewriting before summary compaction | `ctx.toolResultPrune` | | `tool-compact/` (deferred) | Model-facing `/compact` tool over `ctx.compact` | (registers on `ctx.tools`) | -The interface lives at `compact/compact/`, the backend at `compact/compact-basic/`. Unlike the bash seam, it depends on `dsh-session` and `dsh-llm` — its verbs are defined over a `Session` and its output is the `ContentBlock` vocabulary, so the contract cannot be expressed without naming them. That deviation from the "interface depends only on cordis" guidance is intentional and recorded in the [compaction capability-seam Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md). Token measurement is a reusable LLM-family service rather than a `CompactService` method; a template- or model-backed compactor can replace `compact-basic` without changing the meter or callers. +The interface lives at `compact/compact/`, the backend at `compact/compact-basic/`, and deterministic pruning at `compact/compact-tool-result-prune/`. Unlike the bash seam, the interface depends on `dsh-session` and `dsh-llm` because its verbs are defined over a `Session` and its output uses `ContentBlock`. That deviation is recorded in the [compaction capability-seam Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md). Token measurement remains a reusable LLM-family service; a template- or model-backed compactor can replace `compact-basic` without changing the meter, pruner, or callers. diff --git a/packages/compact/compact-basic/README.md b/packages/compact/compact-basic/README.md index 6a67d2dfd4..d789bf2328 100644 --- a/packages/compact/compact-basic/README.md +++ b/packages/compact/compact-basic/README.md @@ -9,13 +9,14 @@ This is the implementation tier of the compaction capability — see the [interf This backend owns the compaction policy: - **Measurement** — the singleton `ctx.tokenMeter` prices the latest canonical logged envelope and current surface at one consumed-log revision. Post-step pressure therefore includes the actual system prompt, tools, prefix, routing, assistant completion, tool results, buffered context, and steering. -- **Retention** — compact the oldest whole surface units while preserving a recent tail and balanced tool-call/result cuts through the [`dsh-compact` boundary helpers](../compact/README.md#tool-pairing-boundaries). Turn boundaries do not protect old steps inside a runaway turn. An open indivisible tail declines until it closes; a single unit larger than the budget remains out of scope. +- **Model-free pruning** — after pressure or canonical overflow qualifies, the optional [`ctx.toolResultPrune`](../compact-tool-result-prune/README.md) service rewrites oversized tool results before range selection. Compact-basic remeasures through `ctx.tokenMeter`, skips summarization when pressure becomes safe, and otherwise summarizes the pruned surface. Below-pressure post-step checks never prune. +- **Retention** — compact the oldest whole surface units while preserving a recent tail and balanced tool-call/result cuts through the [`dsh-compact` boundary helpers](../compact/README.md#tool-pairing-boundaries). Turn boundaries do not protect old steps inside a runaway turn. An open indivisible tail declines until it closes. The optional pruner can repair an oversized closed tool unit when its text-bearing result is the removable bulk; indivisible non-tool units and non-prunable tool remainders remain out of scope. - **Convergence** — retry head-checkpoint compaction up to `compactionRetries`; reject a summary that does not shrink its source, and throw if retries cannot return below threshold. - **Summarization** — a direct `llm/stream` call uses the configured provider/model pair and cap, falling back to the latest logged request target and then the agent target, without running the loop-only `agent/request` seam. The input transcript preserves non-text blocks as tagged placeholders; only returned text enters the checkpoint, excluding reasoning and tool calls that would leak private reasoning or create an orphaned call. - **Framing** — the replacement user message marks established checkpoint context with `` tags. The raw summary remains on the provenance event, and later automatic cycles merge the prior checkpoint. - **Lifecycle** — `compactRegion()` mutates `agent.session` and records its start, summary, replacement, and end. The serial `agent/post-step` listener checks pressure after successful output and tool work are durable but before `step/end`. Canonical provider overflow is handled through `agent/request-error` after the failed step closes. -- **Overflow recovery** — below-threshold overflow bypasses normal retention and attempts one maximal balanced head reduction while leaving the newest indivisible unit. Retry is authorized only when `surface.replaceGeneration` advances; no range, no replacement, recovery failure, an exhausted cap, cancellation, or an unknown/noncanonical error preserves the original provider failure. -- **Failure handling** — an unmatched `compact/start` is an inert crash marker because no replacement landed. A region failure records an error end and leaves the surface unchanged. Operational post-step failures warn and continue; overflow-recovery failure preserves the original provider error. +- **Overflow recovery** — below-threshold overflow bypasses normal retention, prunes, then attempts one maximal balanced head reduction while leaving the newest indivisible unit. Retry is authorized whenever `surface.replaceGeneration` advances, including when pruning lands before later summary work throws. No replacement, an exhausted cap, cancellation, or an unknown/noncanonical error preserves the original provider failure. +- **Failure handling** — an unmatched `compact/start` is an inert crash marker because no summary replacement landed. A region failure records an error end; the surface remains unchanged unless pruning already landed. Operational post-step failures warn and continue, while overflow-recovery failure preserves the original provider error only when no earlier replacement advanced the surface. Cancellation remains authoritative after any progress. The protected `summarize()` method is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, provenance, shrink validation, and shadowed-token accounting stay on `ctx.tokenMeter`. The hook returns the summary blocks together with the call envelope it used (`{ summary, provider, model, maxTokens? }`), which is logged on `compact/summary`. @@ -50,7 +51,7 @@ export function apply(ctx: Context): void { } ``` -Loading the plugin registers `ctx.compact`. With `auto: true` (the default) it compacts automatically under token pressure; a consumer (a future `/compact` tool) can also call `ctx.compact.compactIfNeeded(...)` or `ctx.compact.compactRegion(...)` directly. +Loading the plugin registers `ctx.compact`. Add [`dsh-compact-tool-result-prune`](../compact-tool-result-prune/README.md) as a sibling before this plugin to enable the optional model-free pass. With `auto: true` (the default) it compacts automatically under token pressure; a consumer (a future `/compact` tool) can also call `ctx.compact.compactIfNeeded(...)` or `ctx.compact.compactRegion(...)` directly. ## Model Experience @@ -58,7 +59,7 @@ Loading the plugin registers `ctx.compact`. With `auto: true` (the default) it c #### What the model sees -After a successful step crosses the threshold, the next request receives the checkpoint preamble below, a blank line, ``, the data-dependent summary, and ``. Overflow recovery rebuilds the immediate retry from that replacement. This one checkpoint replaces the selected older range and is followed by the retained recent units. +After a successful step crosses the threshold, oversized tool results are first rewritten when the optional pruner is loaded. If summarization remains necessary, the next request receives the checkpoint preamble below, a blank line, ``, the data-dependent summary, and ``. Overflow recovery rebuilds the immediate retry from whatever replacement advanced the surface. A checkpoint replaces the selected older range and is followed by the retained recent units. ##### Conversation checkpoint preamble @@ -68,7 +69,7 @@ This is an automatically generated checkpoint condensing an earlier span of the #### Token effect -The replacement reduces future input history rather than appending a second copy. The summary remains until a later compaction replaces it; one oversized indivisible unit can still exceed the budget. +Model-free pruning can avoid the auxiliary call entirely; otherwise it reduces that call's transcript before the summary replaces an older range. The replacement reduces future input history rather than appending a second copy. A summary remains until a later compaction replaces it, while an indivisible non-tool unit can still exceed the budget. #### KV Cache effect @@ -144,7 +145,7 @@ Prefix-stable for auxiliary calls while this instruction and the summarizer rout - **Meter accuracy follows the fixed heuristic** — missing reusable provider usage falls back to character count plus structural overhead rather than exact tokenization. - **Overflow classification is adapter-maintained** — provider wording can change; both DeepSeek adapters normalize currently recognized context-limit failures to `CONTEXT_WINDOW_EXCEEDED`. -- **Single-unit and envelope-only overflow remain outside surface compaction** — recovery cannot split one indivisible message/tool unit or shrink system/tools/prefix. +- **Some indivisible-unit and envelope-only overflow remains outside surface compaction** — recovery cannot shrink system/tools/prefix, split an indivisible non-tool node, or repair a tool unit whose non-prunable remainder still exceeds the window. The optional pruner can shrink text-bearing tool-result bulk inside an otherwise indivisible pair. - **`compactRegion` requires an open turn** — a manual call on a fully-closed session throws ("no open turn") rather than compacting. -- **Summarization failure fails closed with full, over-budget history** — including truncation at the summarization `maxTokens`, which hidden reasoning tokens can consume; the auto path logs a warning and proceeds. +- **Summarization failure preserves the latest durable surface** — before any replacement, the auto path logs a warning and proceeds with full over-budget history. If pruning already landed, a later summarization failure proceeds from that durable pruned surface. Summarization truncation at `maxTokens`, which hidden reasoning tokens can consume, follows the same rule. - **The summarization call has no transcript-snapshot coverage** — `dsh-llm-replay` derives calls from `assistant/chunk` events, so this chunk-less direct `ctx.llm.stream()` call cannot replay (named deferred replay infrastructure in [the seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)). diff --git a/packages/compact/compact-basic/package.json b/packages/compact/compact-basic/package.json index a0ee2b4036..01cc2ab6bf 100644 --- a/packages/compact/compact-basic/package.json +++ b/packages/compact/compact-basic/package.json @@ -27,8 +27,14 @@ "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-token-meter": "^0.0.1", + "@deepseek-ai/dsh-compact-tool-result-prune": "^0.0.1", "cordis": "^4.0.0-rc.7" }, + "peerDependenciesMeta": { + "@deepseek-ai/dsh-compact-tool-result-prune": { + "optional": true + } + }, "dependencies": { "schemastery": "^3.18.0" }, @@ -43,6 +49,7 @@ "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", + "@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "cordis": "^4.0.0-rc.7" } diff --git a/packages/compact/compact-basic/src/index.ts b/packages/compact/compact-basic/src/index.ts index 5d325d57ba..caa1ed9c5c 100644 --- a/packages/compact/compact-basic/src/index.ts +++ b/packages/compact/compact-basic/src/index.ts @@ -12,6 +12,8 @@ import type { Session } from '@deepseek-ai/dsh-session' import { CONTEXT_WINDOW_EXCEEDED_CODE, assertNever } from '@deepseek-ai/dsh-llm' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import type { Agent } from '@deepseek-ai/dsh-agent' +// Type-only: makes the optional sibling service available to `ctx.get()`. +import type {} from '@deepseek-ai/dsh-compact-tool-result-prune' import { resolveConfig } from './config.ts' import { compactSurfaceRegion, selectCompactableRange } from './region.ts' import { summarizeWithLlm } from './summarizer.ts' @@ -98,22 +100,35 @@ export class BasicCompactService extends CompactService { || retryAttempt >= this.config.maxOverflowRetries || signal.aborted) return next() - let generation: number + const generation = agent.session.surface.replaceGeneration let result: CompactionResult | null try { - generation = agent.session.surface.replaceGeneration result = await this.compactIfNeeded(agent, 'context-overflow', signal) } catch (recoveryError: unknown) { const message = recoveryError instanceof Error ? recoveryError.message : String(recoveryError) + // A model-free prune can land before later summary work fails. That + // durable reduction is sufficient retry proof; do not discard it just + // because the optional second phase threw. Cancellation still wins. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can abort while recovery is awaited. + if (!signal.aborted && agent.session.surface.replaceGeneration > generation) { + ctx.logger.warn( + `context-overflow compaction failed after durable surface progress: ${message}; ` + + 'retrying from the replacement surface', + ) + return { action: 'retry' } + } ctx.logger.warn( - `context-overflow compaction failed: ${message}; preserving the original request error`, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can abort while recovery is awaited. + `context-overflow compaction failed: ${message}; ${signal.aborted + ? 'cancellation prevents retry' + : 'preserving the original request error'}`, ) return next() } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can abort while compaction is awaited. - if (signal.aborted || result === null + if (signal.aborted || agent.session.surface.replaceGeneration <= generation) return next() - logResult(result, 'context overflow recovery') + if (result !== null) logResult(result, 'context overflow recovery') return { action: 'retry' } }) } @@ -142,7 +157,7 @@ export class BasicCompactService extends CompactService { * @param agent - agent whose latest durable routed request is measured. * @param trigger - normal post-step pressure or context-overflow recovery. * @param signal - live turn cancellation signal forwarded to summarization. - * @returns the latest compaction result, or `null` when no check/work applies. + * @returns the latest summary compaction result, or `null` when no summary ran. */ override async compactIfNeeded( agent: Agent, @@ -152,22 +167,34 @@ export class BasicCompactService extends CompactService { const model = routedModel(agent.session) if (model === undefined) return null const meter = this.ctx.tokenMeter + const threshold = Math.floor(meter.contextWindow * this.config.thresholdRatio) + let measurement = meter.measure(agent.session) switch (trigger) { - case 'context-overflow': { - const measurement = meter.measure(agent.session) - const range = selectCompactableRange(agent.session, measurement, 0) - if (range === null) return null - return this.compactRegion(range.start, range.end, agent, signal) - } + case 'context-overflow': + break case 'pressure': + if (measurement.totalTokens < threshold) return null break /* v8 ignore next -- closed-union exhaustiveness guard */ default: assertNever(trigger, 'compaction trigger') } - const threshold = Math.floor(meter.contextWindow * this.config.thresholdRatio) - let measurement = meter.measure(agent.session) + // Pruning is optional so compact-basic remains independently composable. + // Once either trigger qualifies, land the model-free pass before choosing + // a summary range, then remeasure through the singleton replay fold. + const prune = this.ctx.get('toolResultPrune') + if (prune !== undefined) { + prune.pruneSession(agent.session) + measurement = meter.measure(agent.session) + } + + if (trigger === 'context-overflow') { + const range = selectCompactableRange(agent.session, measurement, 0) + if (range === null) return null + return this.compactRegion(range.start, range.end, agent, signal) + } + if (measurement.totalTokens < threshold) return null let result: CompactionResult | null = null diff --git a/packages/compact/compact-basic/tests/compact-basic.spec.ts b/packages/compact/compact-basic/tests/compact-basic.spec.ts index 0a411440b3..9098ebef50 100644 --- a/packages/compact/compact-basic/tests/compact-basic.spec.ts +++ b/packages/compact/compact-basic/tests/compact-basic.spec.ts @@ -10,6 +10,7 @@ import LlmService, { CallId, CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter } from '@d import type { ContentBlock, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import TokenMeterService from '@deepseek-ai/dsh-token-meter' +import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune' import type { Agent } from '@deepseek-ai/dsh-agent' const SIGNAL = new AbortController().signal @@ -97,6 +98,43 @@ function toolConversation(): Session { return session } +/** One closed routed tool step followed by an open turn for rewrite events. */ +function oversizedToolResult(chars = 3_000, withCompactablePrompt = false): Session { + const session = new Session(SessionId(`oversized-tool-${chars}`)) + const callId = CallId('oversized') + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + if (withCompactablePrompt) { + session.append('user/message', { + content: [{ type: 'text', text: 'older history '.repeat(200) }], + source: { kind: 'user' }, + }, { surfaceOp: 'append' }) + } + session.append('step/start', { turn: 1, step: 1 }) + session.append('request/header', { + header: { config: { provider: MODEL, model: MODEL } }, + reason: 'initial', + }) + session.append('assistant/message', { + turn: 1, + step: 1, + content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }], + provenance: { provider: MODEL, model: MODEL }, + }, { surfaceOp: 'append' }) + session.append('tool/call', { turn: 1, step: 1, callId, name: 'bash', arguments: '{}' }) + session.append('tool/result', { + turn: 1, + step: 1, + callId, + content: [{ type: 'text', text: 'X'.repeat(chars) }], + isError: false, + meta: { presentation: 'preserved' }, + }, { surfaceOp: 'append' }) + session.append('step/end', { turn: 1, step: 1 }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } }) + return session +} + class TestCompactService extends BasicCompactService { summary: ContentBlock[] = [{ type: 'text', text: 'small checkpoint' }] summaryProvider = 'summary-provider' @@ -416,6 +454,78 @@ describe('pressure measurement and retention', () => { }) }) +describe('optional model-free tool-result pruning', () => { + const pruneConfig = { thresholdChars: 100, headChars: 20, tailChars: 10 } + + it('does not prune a below-pressure session opportunistically', async () => { + const ctx = createContext(10_000) + const prune = new ToolResultPruneService(ctx, pruneConfig) + const compact = new TestCompactService(ctx, { + auto: false, + thresholdRatio: 0.8, + retainTokens: 100, + }) + const session = oversizedToolResult() + const pruneSession = vi.spyOn(prune, 'pruneSession') + + expect(await compactIfNeeded(compact, session)).toBeNull() + expect(pruneSession).not.toHaveBeenCalled() + expect(compact.calls).toHaveLength(0) + expect(session.surface.replaceGeneration).toBe(0) + }) + + it('skips LLM summarization when pruning alone clears pressure', async () => { + const ctx = createContext(1_000) + void new ToolResultPruneService(ctx, pruneConfig) + const compact = new TestCompactService(ctx, { + auto: false, + thresholdRatio: 0.5, + retainTokens: 50, + }) + const session = oversizedToolResult() + + expect(ctx.tokenMeter.measure(session).totalTokens).toBeGreaterThanOrEqual(500) + expect(await compactIfNeeded(compact, session)).toBeNull() + expect(ctx.tokenMeter.measure(session).totalTokens).toBeLessThan(500) + expect(compact.calls).toHaveLength(0) + expect(session.surface.replaceGeneration).toBe(1) + }) + + it('summarizes the pruned surface when pruning is insufficient', async () => { + const ctx = createContext(2_000) + void new ToolResultPruneService(ctx, pruneConfig) + const compact = new TestCompactService(ctx, { + auto: false, + thresholdRatio: 0.5, + retainTokens: 50, + }) + const session = toolConversation() + + expect(await compactIfNeeded(compact, session)).not.toBeNull() + expect(compact.calls).toHaveLength(1) + expect(compact.calls[0]!.text).toContain('tool result middle pruned') + expect(compact.calls[0]!.text).not.toContain('result 1 '.repeat(300)) + }) + + it('retains the original compact-basic behavior without the optional plugin', async () => { + const ctx = createContext(2_000) + const compact = new TestCompactService(ctx, { + auto: false, + thresholdRatio: 0.5, + retainTokens: 50, + }) + const session = oversizedToolResult(3_000, true) + + expect(await compactIfNeeded(compact, session)).not.toBeNull() + expect(compact.calls).toHaveLength(1) + const original = session.events.find(event => event.type === 'tool/result') + expect(original?.type === 'tool/result' && original.data.content[0]) + .toEqual({ type: 'text', text: 'X'.repeat(3_000) }) + expect(session.events.filter(event => + event.type === 'tool/result' && event.surfaceOp !== 'append')).toHaveLength(0) + }) +}) + describe('compaction region transaction', () => { it('lands a framed, replayable checkpoint with exact pricing provenance', async () => { const compact = service() @@ -876,6 +986,89 @@ describe('automatic listener and loader composition', () => { expect(session.surface.nodes).toContain(retainedSeq) }) + it('authorizes overflow retry when pruning alone advances an indivisible surface', async () => { + const ctx = createContext(10_000) + void new ToolResultPruneService(ctx, { + thresholdChars: 100, + headChars: 20, + tailChars: 10, + }) + const compact = new TestCompactService(ctx, { + thresholdRatio: 1, + retainTokens: 900, + }) + const session = oversizedToolResult() + + expect(await recover(ctx, agent(session, MODEL), overflow())).toEqual({ action: 'retry' }) + expect(session.surface.replaceGeneration).toBe(1) + expect(session.events.some(event => event.type === 'compact/summary')).toBe(false) + expect(compact.calls).toHaveLength(0) + }) + + it('continues overflow recovery with summarization on the pruned surface', async () => { + const ctx = createContext(10_000) + void new ToolResultPruneService(ctx, { + thresholdChars: 100, + headChars: 20, + tailChars: 10, + }) + const compact = new TestCompactService(ctx, { + thresholdRatio: 1, + retainTokens: 900, + }) + const session = toolConversation() + + expect(await recover(ctx, agent(session, MODEL), overflow())).toEqual({ action: 'retry' }) + expect(session.events.some(event => event.type === 'compact/summary')).toBe(true) + expect(compact.calls).toHaveLength(1) + expect(compact.calls[0]!.text).toContain('tool result middle pruned') + }) + + it('retries from a durable prune when later overflow summarization throws', async () => { + const ctx = createContext(10_000) + const warnings: string[] = [] + ctx.logger.warn = ((message: string) => void warnings.push(message)) as typeof ctx.logger.warn + void new ToolResultPruneService(ctx, { + thresholdChars: 100, + headChars: 20, + tailChars: 10, + }) + const compact = new TestCompactService(ctx, { + thresholdRatio: 1, + retainTokens: 900, + }) + compact.error = new Error('summary unavailable after prune') + const session = oversizedToolResult(3_000, true) + + expect(await recover(ctx, agent(session, MODEL), overflow())).toEqual({ action: 'retry' }) + expect(session.surface.replaceGeneration).toBe(1) + expect(session.events.filter(event => event.type === 'tool/result')).toHaveLength(2) + expect(session.events.findLast(event => event.type === 'compact/end')?.data) + .toMatchObject({ error: 'summary unavailable after prune' }) + expect(warnings).toContainEqual(expect.stringContaining('retrying from the replacement surface')) + }) + + it('lets cancellation win when summary throws after a durable prune', async () => { + const ctx = createContext(10_000) + const controller = new AbortController() + void new ToolResultPruneService(ctx, { + thresholdChars: 100, + headChars: 20, + tailChars: 10, + }) + const compact = new TestCompactService(ctx, { + thresholdRatio: 1, + retainTokens: 900, + }) + compact.mutateDuringSummary = () => { controller.abort('cancelled during summary') } + compact.error = new Error('summary cancelled after prune') + const session = oversizedToolResult(3_000, true) + + expect(await recover(ctx, agent(session, MODEL), overflow(), 0, controller.signal)) + .toEqual({ action: 'fail' }) + expect(session.surface.replaceGeneration).toBe(1) + }) + it('preserves the newest whole tool-call/result pair during forced overflow compaction', async () => { const ctx = createContext() void new TestCompactService(ctx, { diff --git a/packages/compact/compact-basic/tests/loader-composition.spec.ts b/packages/compact/compact-basic/tests/loader-composition.spec.ts index b13e8f8b67..2035627f64 100644 --- a/packages/compact/compact-basic/tests/loader-composition.spec.ts +++ b/packages/compact/compact-basic/tests/loader-composition.spec.ts @@ -9,6 +9,7 @@ import Include from '@cordisjs/plugin-include' import LlmService from '@deepseek-ai/dsh-llm' import TokenMeterService from '@deepseek-ai/dsh-token-meter' import BasicCompactService from '@deepseek-ai/dsh-compact-basic' +import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune' let root: string | undefined let context: Context | undefined @@ -32,6 +33,7 @@ async function loadYaml(lines: readonly string[]): Promise { const modules = new Map([ ['@deepseek-ai/dsh-llm', LlmService], ['@deepseek-ai/dsh-token-meter', TokenMeterService], + ['@deepseek-ai/dsh-compact-tool-result-prune', ToolResultPruneService], ['@deepseek-ai/dsh-compact-basic', BasicCompactService], ]) context.loader.internal = { @@ -50,12 +52,17 @@ async function loadYaml(lines: readonly string[]): Promise { } describe('real Loader composition', () => { - it('loads the flat token-meter and compact-basic YAML shape', async () => { + it('loads the shipped token-meter, pruning, and compact-basic YAML order', async () => { const loaded = await loadYaml([ "- name: '@deepseek-ai/dsh-llm'", "- name: '@deepseek-ai/dsh-token-meter'", ' config:', ' contextWindow: 4096', + "- name: '@deepseek-ai/dsh-compact-tool-result-prune'", + ' config:', + ' thresholdChars: 100', + ' headChars: 20', + ' tailChars: 10', "- name: '@deepseek-ai/dsh-compact-basic'", ' config:', ' thresholdRatio: 0.5', @@ -68,6 +75,7 @@ describe('real Loader composition', () => { .map(entry => entry.options.name) expect(unloaded).toEqual([]) expect(loaded.tokenMeter.contextWindow).toBe(4096) + expect(loaded.get('toolResultPrune')).toBeInstanceOf(ToolResultPruneService) expect(loaded.get('compact')).toBeInstanceOf(BasicCompactService) expect((loaded.compact as BasicCompactService).config).toMatchObject({ thresholdRatio: 0.5, diff --git a/packages/compact/compact-basic/tsconfig.json b/packages/compact/compact-basic/tsconfig.json index 0103ad82a8..47d552c3f0 100644 --- a/packages/compact/compact-basic/tsconfig.json +++ b/packages/compact/compact-basic/tsconfig.json @@ -13,6 +13,7 @@ { "path": "../../llm/token-meter" }, { "path": "../../core/session" }, { "path": "../../core/agent" }, - { "path": "../compact" } + { "path": "../compact" }, + { "path": "../compact-tool-result-prune" } ] } diff --git a/packages/compact/compact-tool-result-prune/README.md b/packages/compact/compact-tool-result-prune/README.md new file mode 100644 index 0000000000..c06eab5405 --- /dev/null +++ b/packages/compact/compact-tool-result-prune/README.md @@ -0,0 +1,60 @@ +# @deepseek-ai/dsh-compact-tool-result-prune + +The replay-safe model-free pruning service (`ctx.toolResultPrune`). It rewrites over-budget `tool/result` surface nodes to a bounded head, a fixed omission marker, and a bounded tail while retaining the full original event in the append-only session log. + +This is a concrete companion to [`dsh-compact-basic`](../compact-basic/README.md), not a compaction backend or model-facing tool. Compact-basic reads it through optional `ctx.get('toolResultPrune')`, so either package remains independently composable. + +## Service API + +`pruneSession(session)` scans one stable snapshot of the current surface. Every over-budget tool result is replaced by one newly appended `tool/result` carrying `{ surfaceOp: { op: 'replace', start: originalSeq, end: originalSeq }, sourceEventSeqs: [originalSeq] }`. The replacement spreads the complete original data and changes only `content`, preserving `turn`, `step`, `callId`, error fields, `meta`, and later data additions. The original event remains available for persistence, replay, and exact-log inspection. + +The method throws synchronously when the session rejects a replacement. Replacements committed earlier in the pass remain durable. + +`measureContent(blocks)` counts Unicode code points in `text` blocks. `pruneContent(blocks)` returns the bounded replacement or `null` when content is already within the threshold. Non-text blocks are retained at their original relative positions; text slicing never splits a UTF-16 surrogate pair, though it can split a multi-code-point grapheme cluster. + +Every emitted result has exactly the configured head budget, fixed marker, and tail budget in text code points, is no larger than `thresholdChars`, and is strictly smaller than the triggering input. A second pass therefore emits no replacement. + +## Config + +Unrecognized keys fail at plugin construction. Resolved config is detached and deeply immutable. + +| Key | Required | Meaning | +|---|---|---| +| `thresholdChars` | no (default `8192`) | Prune when combined text exceeds this many Unicode code points. | +| `headChars` | no (default `4096`) | Leading Unicode code points retained. | +| `tailChars` | no (default `1024`) | Trailing Unicode code points retained. | + +All values are integers; the threshold is positive and head/tail are non-negative. `headChars + marker + tailChars` must fit within `thresholdChars`, so a valid configuration can prune every over-budget result without growth or repeated rewriting. + +## Usage + +```ts +import type { Context } from 'cordis' +import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune' + +export function apply(ctx: Context): void { + ctx.plugin(ToolResultPruneService) +} +``` + +## Model Experience + +### Pruned tool result + +#### What the model sees + +Once a compaction trigger qualifies, future requests see the retained head, `\n\n[... tool result middle pruned ...]\n\n`, and retained tail in place of the removed text. Rich blocks keep their order. The model does not see a second copy of the original. + +#### Token effect + +Each rewritten tool result has at most `thresholdChars` text code points. Pruning itself makes no model call; compact-basic skips summarization when the remeasured request falls below pressure, otherwise the summarizer reads the pruned surface. + +#### KV Cache effect + +Replacing an earlier result invalidates reuse from the first changed token. The pruned prefix is eligible for reuse while its route, envelope, and preceding history remain identical. + +## Known Limitations and Deferred Work + +- **Character budgets are not token budgets** — provider token density varies, so `ctx.tokenMeter` remains the authority for deciding whether pruning relieved request pressure. +- **Pruning is syntactic** — it retains the beginning and end without interpreting which middle lines are semantically important. +- **Grapheme clusters can split** — code-point slicing protects surrogate pairs but does not perform locale-aware grapheme segmentation. diff --git a/packages/compact/compact-tool-result-prune/package.json b/packages/compact/compact-tool-result-prune/package.json new file mode 100644 index 0000000000..81c81eb894 --- /dev/null +++ b/packages/compact/compact-tool-result-prune/package.json @@ -0,0 +1,40 @@ +{ + "name": "@deepseek-ai/dsh-compact-tool-result-prune", + "description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes", + "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" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", + "src" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "@deepseek-ai/dsh-llm": "^0.0.1", + "@deepseek-ai/dsh-session": "^0.0.1", + "cordis": "^4.0.0-rc.7" + }, + "dependencies": { + "schemastery": "^3.18.0" + }, + "devDependencies": { + "@cordisjs/plugin-include": "workspace:^", + "@cordisjs/plugin-loader": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "cordis": "^4.0.0-rc.7" + } +} diff --git a/packages/compact/compact-tool-result-prune/src/config.ts b/packages/compact/compact-tool-result-prune/src/config.ts new file mode 100644 index 0000000000..a2d33ac76e --- /dev/null +++ b/packages/compact/compact-tool-result-prune/src/config.ts @@ -0,0 +1,77 @@ +/** Configuration resolution for deterministic tool-result pruning. */ + +import { deepFreeze } from '@deepseek-ai/dsh-llm' +import type { ResolvedConfig, ToolResultPruneConfig } from './types.ts' + +/** Fixed marker substituted for every removed middle span. */ +export const PRUNE_MARKER = '\n\n[... tool result middle pruned ...]\n\n' + +/** Low-friction defaults for coding-agent tool output. */ +export const DEFAULTS: ResolvedConfig = deepFreeze({ + thresholdChars: 8192, + headChars: 4096, + tailChars: 1024, +}) + +const CONFIG_KEYS: ReadonlySet = new Set([ + 'thresholdChars', + 'headChars', + 'tailChars', +]) + +/** + * Count Unicode code points without splitting surrogate pairs. + * @param text - text to measure. + * @returns the Unicode code-point count. + */ +export function codePointLength(text: string): number { + return Array.from(text).length +} + +/** + * Resolve and validate pruning budgets. + * @param config - raw plugin configuration. + * @returns a detached deeply immutable configuration. + */ +export function resolveConfig(config: ToolResultPruneConfig = {}): ResolvedConfig { + for (const key of Object.keys(config)) { + if (!CONFIG_KEYS.has(key)) { + throw new Error( + `ToolResultPruneConfig: unknown key "${key}" ` + + '(allowed: thresholdChars, headChars, tailChars)', + ) + } + } + + const resolved: ResolvedConfig = { + thresholdChars: config.thresholdChars ?? DEFAULTS.thresholdChars, + headChars: config.headChars ?? DEFAULTS.headChars, + tailChars: config.tailChars ?? DEFAULTS.tailChars, + } + assertPositiveInteger('thresholdChars', resolved.thresholdChars) + assertNonNegativeInteger('headChars', resolved.headChars) + assertNonNegativeInteger('tailChars', resolved.tailChars) + + const emittedChars = resolved.headChars + + codePointLength(PRUNE_MARKER) + + resolved.tailChars + if (emittedChars > resolved.thresholdChars) { + throw new Error( + `ToolResultPruneConfig: headChars + marker + tailChars (${emittedChars}) ` + + `must be at most thresholdChars (${resolved.thresholdChars})`, + ) + } + return deepFreeze(structuredClone(resolved)) +} + +function assertPositiveInteger(name: string, value: number): void { + if (!Number.isInteger(value) || value <= 0) { + throw new Error(`ToolResultPruneConfig: ${name} (${value}) must be a positive integer`) + } +} + +function assertNonNegativeInteger(name: string, value: number): void { + if (!Number.isInteger(value) || value < 0) { + throw new Error(`ToolResultPruneConfig: ${name} (${value}) must be a non-negative integer`) + } +} diff --git a/packages/compact/compact-tool-result-prune/src/index.ts b/packages/compact/compact-tool-result-prune/src/index.ts new file mode 100644 index 0000000000..d4a2daecbc --- /dev/null +++ b/packages/compact/compact-tool-result-prune/src/index.ts @@ -0,0 +1,159 @@ +/** + * Replay-safe, model-free tool-result pruning service. + * + * @module @deepseek-ai/dsh-compact-tool-result-prune + */ + +import { Context, Service } from 'cordis' +import z from 'schemastery' +import type { ContentBlock } from '@deepseek-ai/dsh-llm' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts' +import type { + PrunedEntry, + PruneResult, + ResolvedConfig, + ToolResultPruneConfig, +} from './types.ts' + +export { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts' +export type { + PrunedEntry, + PruneResult, + ResolvedConfig, + ToolResultPruneConfig, +} from './types.ts' + +declare module 'cordis' { + interface Context { + toolResultPrune: ToolResultPruneService + } +} + +interface SnapshotCandidate { + readonly seq: number + readonly event: SessionEvent<'tool/result'> +} + +/** Deterministic head/middle/tail pruning for current tool-result surface nodes. */ +export class ToolResultPruneService extends Service { + static Config: z = z.object({ + thresholdChars: z.number().step(1).min(1).default(DEFAULTS.thresholdChars), + headChars: z.number().step(1).min(0).default(DEFAULTS.headChars), + tailChars: z.number().step(1).min(0).default(DEFAULTS.tailChars), + }) + + /** Resolved and immutable character budgets. */ + readonly config: ResolvedConfig + + constructor(ctx: Context, config: ToolResultPruneConfig = {}) { + super(ctx, 'toolResultPrune') + this.config = resolveConfig(config) + } + + /** + * Measure text content in Unicode code points; non-text blocks cost zero. + * @param blocks - tool-result content to measure. + * @returns total Unicode code points across text blocks. + */ + measureContent(blocks: readonly ContentBlock[]): number { + let chars = 0 + for (const block of blocks) { + if (block.type === 'text') chars += codePointLength(block.text) + } + return chars + } + + /** + * Replace an over-budget text middle while retaining rich-block order. + * Text slicing is by Unicode code point, not UTF-16 code unit, so a retained + * boundary cannot split a surrogate pair. Grapheme clusters may still split. + * @param blocks - original tool-result content. + * @returns pruned content, or `null` when the text is within budget. + */ + pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null { + const totalChars = this.measureContent(blocks) + if (totalChars <= this.config.thresholdChars) return null + + const removedStart = this.config.headChars + const removedEnd = totalChars - this.config.tailChars + const pruned: ContentBlock[] = [] + let consumed = 0 + let markerInserted = false + + for (const block of blocks) { + if (block.type !== 'text') { + pruned.push(block) + continue + } + + const points = Array.from(block.text) + const blockStart = consumed + const blockEnd = blockStart + points.length + const headEnd = Math.min(points.length, Math.max(0, removedStart - blockStart)) + const tailStart = Math.min(points.length, Math.max(0, removedEnd - blockStart)) + const intersectsRemoved = blockStart < removedEnd && blockEnd > removedStart + const marker = intersectsRemoved && !markerInserted ? PRUNE_MARKER : '' + if (marker.length > 0) markerInserted = true + const text = points.slice(0, headEnd).join('') + + marker + + points.slice(tailStart).join('') + if (text.length > 0) pruned.push({ ...block, text }) + consumed = blockEnd + } + + /* v8 ignore next -- totalChars > threshold and valid budgets guarantee a removed text span. */ + if (!markerInserted) throw new Error('tool-result prune: failed to locate the removed text span') + const charsAfter = this.measureContent(pruned) + /* v8 ignore next -- config validation fixes the emitted head + marker + tail budget. */ + if (charsAfter > this.config.thresholdChars || charsAfter >= totalChars) { + throw new Error('tool-result prune: replacement must be smaller and within threshold') + } + return pruned + } + + /** + * Prune every over-budget tool result from one stable current-surface snapshot. + * Each replacement preserves the complete event data except for `content`, + * and points at the shadowed node for durable provenance and replay. + * @param session - session whose current surface is rewritten. + * @returns landed replacements and aggregate Unicode-code-point savings. + * @throws when the session rejects a replacement; replacements committed + * earlier in the pass remain durable. + */ + pruneSession(session: Session): PruneResult { + const candidates: SnapshotCandidate[] = [] + for (const seq of [...session.surface.nodes]) { + const event = session.events[seq] + /* v8 ignore next -- surface seqs are validated contiguous log references. */ + if (event?.type === 'tool/result') candidates.push({ seq, event }) + } + + const pruned: PrunedEntry[] = [] + let charsRemoved = 0 + for (const { seq, event } of candidates) { + const content = this.pruneContent(event.data.content) + if (content === null) continue + const charsBefore = this.measureContent(event.data.content) + const charsAfter = this.measureContent(content) + const replacement = session.append('tool/result', { + ...event.data, + content, + }, { + surfaceOp: { op: 'replace', start: seq, end: seq }, + sourceEventSeqs: [seq], + }) + pruned.push({ + originalSeq: seq, + replacementSeq: replacement.seq, + callId: event.data.callId, + charsBefore, + charsAfter, + }) + charsRemoved += charsBefore - charsAfter + } + return { pruned, charsRemoved } + } +} + +export default ToolResultPruneService diff --git a/packages/compact/compact-tool-result-prune/src/types.ts b/packages/compact/compact-tool-result-prune/src/types.ts new file mode 100644 index 0000000000..f9dd846f35 --- /dev/null +++ b/packages/compact/compact-tool-result-prune/src/types.ts @@ -0,0 +1,40 @@ +import type { CallId } from '@deepseek-ai/dsh-llm' + +/** Character-budget policy for deterministic tool-result pruning. */ +export interface ToolResultPruneConfig { + /** Prune when total text exceeds this many Unicode code points. Defaults to `8192`. */ + thresholdChars?: number + /** Maximum leading Unicode code points retained. Defaults to `4096`. */ + headChars?: number + /** Maximum trailing Unicode code points retained. Defaults to `1024`. */ + tailChars?: number +} + +/** Validated, detached, deeply immutable pruning configuration. */ +export interface ResolvedConfig { + readonly thresholdChars: number + readonly headChars: number + readonly tailChars: number +} + +/** Provenance and size accounting for one landed surface replacement. */ +export interface PrunedEntry { + /** Full-fidelity tool-result event shadowed by the replacement. */ + readonly originalSeq: number + /** Newly appended pruned tool-result event. */ + readonly replacementSeq: number + /** Tool call shared by the original and replacement. */ + readonly callId: CallId + /** Original text size in Unicode code points. */ + readonly charsBefore: number + /** Replacement text size in Unicode code points. */ + readonly charsAfter: number +} + +/** Aggregate outcome of one stable-surface pruning pass. */ +export interface PruneResult { + /** Replacements in the snapshotted surface order. */ + readonly pruned: readonly PrunedEntry[] + /** Total Unicode code points removed across replacements. */ + readonly charsRemoved: number +} diff --git a/packages/compact/compact-tool-result-prune/tests/loader-composition.spec.ts b/packages/compact/compact-tool-result-prune/tests/loader-composition.spec.ts new file mode 100644 index 0000000000..db4c29ebdb --- /dev/null +++ b/packages/compact/compact-tool-result-prune/tests/loader-composition.spec.ts @@ -0,0 +1,67 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { afterEach, describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import Loader from '@cordisjs/plugin-loader' +import Include from '@cordisjs/plugin-include' +import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune' + +let root: string | undefined +let context: Context | undefined + +afterEach(async () => { + await context?.fiber.dispose() + context = undefined + if (root !== undefined) await rm(root, { recursive: true, force: true }) + root = undefined +}) + +describe('compact-tool-result-prune real Loader composition', () => { + it('loads and resolves the flat YAML plugin shape', async () => { + root = await mkdtemp(join(tmpdir(), 'dsh-compact-tool-result-prune-loader-')) + const configPath = join(root, 'cordis.yml') + await writeFile(configPath, [ + "- name: '@deepseek-ai/dsh-compact-tool-result-prune'", + ' config:', + ' thresholdChars: 100', + ' headChars: 20', + ' tailChars: 10', + '', + ].join('\n')) + + context = new Context() + context.baseUrl = pathToFileURL(root).href + '/' + await context.plugin(Loader) + context.loader.builtins.include = Include + context.loader.internal = { + version: 'v2', + async import(specifier: string) { + if (specifier !== '@deepseek-ai/dsh-compact-tool-result-prune') { + throw new Error(`unexpected Loader import: ${specifier}`) + } + return ToolResultPruneService + }, + } as unknown as NonNullable + await context.loader.create({ + name: 'cordis:include', + config: { path: pathToFileURL(configPath).href }, + }) + await context.loader.await() + + expect(context.get('toolResultPrune')).toBeInstanceOf(ToolResultPruneService) + expect(context.toolResultPrune.config).toEqual({ + thresholdChars: 100, + headChars: 20, + tailChars: 10, + }) + }) + + it('rejects stale config after plugin schema normalization', async () => { + context = new Context() + await expect(context.plugin(ToolResultPruneService, { + maxChars: 100, + } as never)).rejects.toThrow(/unknown key "maxChars"/) + }) +}) diff --git a/packages/compact/compact-tool-result-prune/tests/tool-result-prune.spec.ts b/packages/compact/compact-tool-result-prune/tests/tool-result-prune.spec.ts new file mode 100644 index 0000000000..bc382c8e4e --- /dev/null +++ b/packages/compact/compact-tool-result-prune/tests/tool-result-prune.spec.ts @@ -0,0 +1,239 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { CallId } from '@deepseek-ai/dsh-llm' +import type { ContentBlock } from '@deepseek-ai/dsh-llm' +import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' +import type { SurfaceEvent } from '@deepseek-ai/dsh-session' +import * as Invariants from '@deepseek-ai/dsh-invariants' +import ToolResultPruneService, { + codePointLength, + DEFAULTS, + PRUNE_MARKER, + resolveConfig, +} from '@deepseek-ai/dsh-compact-tool-result-prune' +import type { ToolResultPruneConfig } from '@deepseek-ai/dsh-compact-tool-result-prune' + +const MODEL = 'test-model' +const SMALL: ToolResultPruneConfig = { + thresholdChars: 50, + headChars: 4, + tailChars: 3, +} + +function service(config: ToolResultPruneConfig = SMALL): ToolResultPruneService { + return new ToolResultPruneService(new Context(), config) +} + +function appendToolStep( + session: Session, + turn: number, + call: string, + content: ContentBlock[], + extra: Record = {}, +): number { + const callId = CallId(call) + session.append('turn/start', { + turn, + trigger: { kind: 'message', source: { kind: 'user' } }, + }) + session.append('step/start', { turn, step: 1 }) + session.append('assistant/message', { + turn, + step: 1, + content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }], + provenance: { provider: MODEL, model: MODEL }, + }, { surfaceOp: 'append' }) + session.append('tool/call', { turn, step: 1, callId, name: 'bash', arguments: '{}' }) + const result = session.append('tool/result', { + turn, + step: 1, + callId, + content, + isError: false, + ...extra, + }, { surfaceOp: 'append' }) + session.append('step/end', { turn, step: 1 }) + session.append('turn/end', { turn, reason: { kind: 'completed' } }) + return result.seq +} + +describe('tool-result pruning configuration', () => { + it('resolves detached immutable defaults and partial overrides', () => { + const raw = { thresholdChars: 100, headChars: 20, tailChars: 10 } + const resolved = resolveConfig(raw) + raw.headChars = 1 + expect(resolved).toEqual({ thresholdChars: 100, headChars: 20, tailChars: 10 }) + expect(Object.isFrozen(resolved)).toBe(true) + expect(DEFAULTS).toEqual({ thresholdChars: 8192, headChars: 4096, tailChars: 1024 }) + expect(Object.isFrozen(DEFAULTS)).toBe(true) + }) + + it('rejects stale keys, invalid scalars, and an output budget above threshold', () => { + const bad = [ + [{ thresholdChars: 0 }, /thresholdChars .* positive integer/], + [{ headChars: -1 }, /headChars .* non-negative integer/], + [{ tailChars: 1.5 }, /tailChars .* non-negative integer/], + [{ thresholdChars: 50, headChars: 20, tailChars: 20 }, /headChars \+ marker \+ tailChars/], + [{ threshold: 10 }, /unknown key "threshold"/], + ] as Array<[unknown, RegExp]> + for (const [config, pattern] of bad) { + expect(() => resolveConfig(config as ToolResultPruneConfig)).toThrow(pattern) + } + }) +}) + +describe('ToolResultPruneService content transform', () => { + it('measures text code points only and skips content within threshold', () => { + const prune = service() + const blocks = [ + { type: 'text', text: 'a😀b' }, + { type: 'reasoning', text: 'not measured' }, + ] satisfies ContentBlock[] + expect(prune.measureContent(blocks)).toBe(3) + expect(prune.pruneContent(blocks)).toBeNull() + expect(codePointLength('a😀b')).toBe(3) + }) + + it('keeps configured head and tail without splitting surrogate pairs', () => { + const prune = service() + const result = prune.pruneContent([{ type: 'text', text: '😀'.repeat(60) }]) + expect(result).toEqual([{ + type: 'text', + text: `${'😀'.repeat(4)}${PRUNE_MARKER}${'😀'.repeat(3)}`, + }]) + expect(prune.measureContent(result!)).toBeLessThanOrEqual(50) + expect(result![0]).toMatchObject({ type: 'text' }) + expect((result![0] as { text: string }).text).not.toContain('\uFFFD') + }) + + it('preserves non-text blocks and their relative ordering across removed text', () => { + const prune = service() + const reasoning: ContentBlock = { type: 'reasoning', text: 'private-rich-block' } + const call: ContentBlock = { + type: 'tool-call', + id: CallId('nested'), + name: 'nested', + arguments: '{}', + } + const result = prune.pruneContent([ + { type: 'text', text: 'A'.repeat(40) }, + reasoning, + { type: 'text', text: 'B'.repeat(30) }, + call, + { type: 'text', text: 'C'.repeat(30) }, + ]) + expect(result).toEqual([ + { type: 'text', text: `AAAA${PRUNE_MARKER}` }, + reasoning, + call, + { type: 'text', text: 'CCC' }, + ]) + expect(prune.measureContent(result!)).toBeLessThanOrEqual(50) + }) + + it('supports zero-sized head and tail while still shrinking', () => { + const prune = service({ + thresholdChars: codePointLength(PRUNE_MARKER), + headChars: 0, + tailChars: 0, + }) + const result = prune.pruneContent([{ type: 'text', text: 'x'.repeat(100) }]) + expect(result).toEqual([{ type: 'text', text: PRUNE_MARKER }]) + expect(prune.measureContent(result!)).toBe(prune.config.thresholdChars) + }) +}) + +describe('ToolResultPruneService session transaction', () => { + it('prunes a stable snapshot, preserves all data, and records provenance', () => { + const session = new Session(SessionId('preserve')) + const originalSeq = appendToolStep(session, 1, 'one', [{ + type: 'text', + text: 'x'.repeat(100), + }], { + isError: true, + error: { name: 'ExitError', code: 'EXIT_1' }, + meta: { diff: ['a', 'b'] }, + futureField: { nested: true }, + }) + session.append('turn/start', { + turn: 2, + trigger: { kind: 'message', source: { kind: 'user' } }, + }) + + const result = service().pruneSession(session) + expect(result.pruned).toHaveLength(1) + expect(result.charsRemoved).toBeGreaterThan(0) + const entry = result.pruned[0]! + expect(entry).toMatchObject({ originalSeq, callId: CallId('one'), charsBefore: 100 }) + expect(entry.charsAfter).toBeLessThanOrEqual(50) + + const original = session.events[originalSeq]! + const replacement = session.events[entry.replacementSeq]! as SurfaceEvent + expect(original).toMatchObject({ + type: 'tool/result', + data: { content: [{ type: 'text', text: 'x'.repeat(100) }] }, + }) + expect(replacement).toMatchObject({ + type: 'tool/result', + data: { + turn: 1, + step: 1, + callId: CallId('one'), + isError: true, + error: { name: 'ExitError', code: 'EXIT_1' }, + meta: { diff: ['a', 'b'] }, + futureField: { nested: true }, + }, + surfaceOp: { op: 'replace', start: originalSeq, end: originalSeq }, + sourceEventSeqs: [originalSeq], + }) + expect(session.surface.nodes).not.toContain(originalSeq) + }) + + it('prunes multiple results, skips short ones, and converges in one pass', () => { + const session = new Session(SessionId('multiple')) + appendToolStep(session, 1, 'a', [{ type: 'text', text: 'A'.repeat(100) }]) + appendToolStep(session, 2, 'b', [{ type: 'text', text: 'short' }]) + appendToolStep(session, 3, 'c', [{ type: 'text', text: 'C'.repeat(80) }]) + session.append('turn/start', { + turn: 4, + trigger: { kind: 'message', source: { kind: 'user' } }, + }) + const prune = service() + const first = prune.pruneSession(session) + const second = prune.pruneSession(session) + expect(first.pruned.map(entry => entry.callId)).toEqual([CallId('a'), CallId('c')]) + expect(first.charsRemoved).toBe( + first.pruned.reduce((sum, entry) => sum + entry.charsBefore - entry.charsAfter, 0), + ) + expect(second).toEqual({ pruned: [], charsRemoved: 0 }) + }) + + it('replays to the identical pruned model messages', () => { + const session = new Session(SessionId('replay')) + appendToolStep(session, 1, 'a', [{ type: 'text', text: 'A'.repeat(100) }]) + session.append('turn/start', { + turn: 2, + trigger: { kind: 'message', source: { kind: 'user' } }, + }) + service().pruneSession(session) + const replay = new Session(session.id, [...session.events]) + expect(replay.deriveMessages()).toEqual(session.deriveMessages()) + expect(replay.surface.replaceGeneration).toBe(session.surface.replaceGeneration) + }) + + it('runs under real invariants between closed steps but not outside a turn', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(Invariants) + const prune = new ToolResultPruneService(ctx, SMALL) + const session = ctx.sessions.create(SessionId('invariants')) + appendToolStep(session, 1, 'a', [{ type: 'text', text: 'A'.repeat(100) }]) + expect(() => prune.pruneSession(session)).toThrow(/outside any open turn/) + session.append('turn/start', { + turn: 2, + trigger: { kind: 'message', source: { kind: 'user' } }, + }) + expect(() => prune.pruneSession(session)).not.toThrow() + }) +}) diff --git a/packages/compact/compact-tool-result-prune/tsconfig.json b/packages/compact/compact-tool-result-prune/tsconfig.json new file mode 100644 index 0000000000..e021fa336e --- /dev/null +++ b/packages/compact/compact-tool-result-prune/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": ["src"], + "references": [ + { "path": "../../../vendor/cosmokit" }, + { "path": "../../../vendor/cordis" }, + { "path": "../../../vendor/schemastery" }, + { "path": "../../llm/llm" }, + { "path": "../../core/session" } + ] +} diff --git a/packages/compact/compact/README.md b/packages/compact/compact/README.md index c4a2dc7a5d..6e33b6e570 100644 --- a/packages/compact/compact/README.md +++ b/packages/compact/compact/README.md @@ -38,7 +38,7 @@ The private per-session cache is keyed by `session.surface.replaceGeneration` an 1. appends `compact/start` (log-only) — acquires the lock, 2. summarizes the range, 3. appends `compact/summary` (log-only) — provenance: summary, range, shadowed seqs, token count, and provider/model call envelope, -4. appends a single `user/message` with `surfaceOp: { op: 'replace', start, end }` carrying the summary — **the only surface mutation**, +4. appends a single `user/message` with `surfaceOp: { op: 'replace', start, end }` carrying the summary — **the only surface mutation in this operation**, 5. appends `compact/end` (log-only) — releases the lock. The surface mutation (step 4) sits **inside** the lock bracket: `compact/end` is the last event, so the lock is never released before the mutation lands. A crash between `compact/start` and `compact/end` therefore leaves a detectable orphaned lock (a `compact/start` with no matching `compact/end`) rather than a `compact/end` that falsely claims compaction finished while the surface was never shadowed. @@ -90,5 +90,5 @@ No conversation-cache invalidation. A consumer's auxiliary request can reuse onl ## Known Limitations and Deferred Work - **No model-facing consumer tier yet** — `@deepseek-ai/dsh-tool-compact` (the `/compact` tool) is deferred; compaction is reachable only via direct `ctx.compact` calls or a backend's auto listener. -- **Single-unit overflow is out of contract** — one indivisible unit (a closed tool pair or a large pasted `user/message`) alone exceeding the budget cannot be compacted. +- **Some single-unit overflow is out of contract** — balanced summary compaction cannot split one indivisible unit. The optional pruning companion can still repair a closed tool pair when text-bearing tool-result bulk is removable; a large non-tool node or a tool unit whose non-prunable remainder is oversized cannot be compacted. - **An envelope that alone approaches the window is not surface-compaction work** — compaction shrinks derived history, never the system prompt, tools, or session prefix. diff --git a/packages/context/workspace-context/README.md b/packages/context/workspace-context/README.md index a04c844e9b..7dd1e06931 100644 --- a/packages/context/workspace-context/README.md +++ b/packages/context/workspace-context/README.md @@ -42,7 +42,7 @@ These instructions apply to work under `packages/app`. Use them as guidance when A same-file edit starts with `Updated instructions from: ` and says to use the new content instead of the previously loaded content. A candidate switch additionally names the old path. When no candidate remains, the message is `Instructions removed: ` followed by `The previously loaded instructions from this file no longer apply.` Literal `
` text inside an instruction file is escaped so file content cannot close the plugin-owned frame. -The core `context/message` envelope is disabled for these messages because the plugin already owns the complete `` framing. This is caller-selected with `envelope: 'raw'`; ordinary injected context still receives the canonical `` envelope. +The plugin owns the complete `` framing, and every `context/message` (from this plugin or any other) reaches the model verbatim as a user-role message with no wrapping. ## State And Refresh diff --git a/packages/context/workspace-context/src/index.ts b/packages/context/workspace-context/src/index.ts index 83bbfa9d26..21ef979459 100644 --- a/packages/context/workspace-context/src/index.ts +++ b/packages/context/workspace-context/src/index.ts @@ -92,7 +92,6 @@ export function apply(ctx: Context, config: Config): void { if (update !== undefined) { agent.inject(update.context.content, { source: update.context.source, - envelope: update.context.envelope, meta: update.context.meta, }) applyInstructionVersionUpdates(agent.session, update.versionUpdates, instructionVersions) diff --git a/packages/context/workspace-context/src/render.ts b/packages/context/workspace-context/src/render.ts index 34e427d0e3..910853b13b 100644 --- a/packages/context/workspace-context/src/render.ts +++ b/packages/context/workspace-context/src/render.ts @@ -163,6 +163,12 @@ function buildInstructionText( ): string { const marker = markerText(maxBytes, omitted, truncated) const body = [marker, style.intro, ...files.map(file => style.section(file))].filter(block => block.length > 0) + // Caller-owned framing: the plugin bakes the complete `` + // frame into the message content. The session surface projects context + // verbatim and does not wrap it, so any framing must live here in the + // producer's content (the pattern a future `meta`-driven renderer would + // generalize — see the deferred note in + // ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md). return [SYSTEM_REMINDER_OPEN, body.join('\n\n'), SYSTEM_REMINDER_CLOSE].join('\n') } diff --git a/packages/context/workspace-context/src/state.ts b/packages/context/workspace-context/src/state.ts index 4e8ee7de56..f2e113ddfe 100644 --- a/packages/context/workspace-context/src/state.ts +++ b/packages/context/workspace-context/src/state.ts @@ -61,9 +61,8 @@ export interface ReconciledInstructionContext { versionUpdates: InstructionVersionUpdate[] } -/** Plugin-owned raw context with required replay metadata. */ +/** Plugin-owned context with required replay metadata. */ export interface WorkspaceHookContext extends HookContext { - envelope: 'raw' meta: JsonValue } @@ -76,7 +75,7 @@ function workspaceContextHook(text: string, changes: WorkspaceInstructionChange[ ...change.digest !== undefined ? { digest: change.digest } : {}, })) const meta: JsonValue = { kind: 'workspace-instructions', version: 1, changes: serializedChanges } - return { content: [{ type: 'text', text }], source: PLUGIN_SOURCE, envelope: 'raw', meta } + return { content: [{ type: 'text', text }], source: PLUGIN_SOURCE, meta } } /** diff --git a/packages/context/workspace-context/tests/workspace-context.spec.ts b/packages/context/workspace-context/tests/workspace-context.spec.ts index 2ff2067cf5..8b0320bcfc 100644 --- a/packages/context/workspace-context/tests/workspace-context.spec.ts +++ b/packages/context/workspace-context/tests/workspace-context.spec.ts @@ -173,7 +173,6 @@ function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent { session.append('context/message', { content, source: options?.source ?? { kind: 'user' }, - ...options?.envelope !== undefined ? { envelope: options.envelope } : {}, ...options?.meta !== undefined ? { meta: options.meta } : {}, }, { surfaceOp: 'append' }) }, @@ -202,7 +201,6 @@ function workspaceChangeContext(scope: string, digest: string): HookContext { return { content: [{ type: 'text', text: `instructions for ${scope}` }], source: { kind: 'plugin', plugin: 'workspace-context' }, - envelope: 'raw', meta: { kind: 'workspace-instructions', version: 1, @@ -217,7 +215,6 @@ function appendAdditionalContexts(agent: Agent, result: { additionalContexts?: H lastSeq = agent.session.append('context/message', { content: context.content, source: context.source, - ...context.envelope !== undefined ? { envelope: context.envelope } : {}, ...context.meta !== undefined ? { meta: context.meta } : {}, }, { surfaceOp: 'append' }).seq } @@ -1695,7 +1692,6 @@ describe('dynamic nested workspace context injection', () => { expect(result.isError).toBe(false) expect(workspaceContextOf(result)?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' }) - expect(workspaceContextOf(result)?.envelope).toBe('raw') expect(workspaceContextOf(result)?.meta).toMatchObject({ kind: 'workspace-instructions', version: 1, @@ -2461,7 +2457,6 @@ describe('dynamic nested workspace context injection', () => { expect(blocksText(result.content)).toBe('downstream replacement') expect(result.additionalContexts).toHaveLength(2) expect(workspaceContextOf(result)?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' }) - expect(workspaceContextOf(result)?.envelope).toBe('raw') expect(workspaceContextOf(result)?.meta).toMatchObject({ kind: 'workspace-instructions', changes: [{ action: 'set', scope: 'pkg', path: 'pkg/AGENTS.md' }], @@ -2474,7 +2469,8 @@ describe('dynamic nested workspace context injection', () => { }) const agent = stubAgent(root) appendAdditionalContexts(agent, result) - expect(blocksText(agent.session.deriveMessages()[1]?.content)).toContain('\ndownstream context\n') + expect(blocksText(agent.session.deriveMessages()[1]?.content)).toContain('downstream context') + expect(blocksText(agent.session.deriveMessages()[1]?.content)).not.toContain(' { const otherWorkspaceEvent = agent.session.append('context/message', { content: otherContext.content, source: otherContext.source, - ...otherContext.envelope !== undefined ? { envelope: otherContext.envelope } : {}, ...otherContext.meta !== undefined ? { meta: otherContext.meta } : {}, }, { surfaceOp: 'append' }) observeInstructionSessionEvent(agent.session, otherWorkspaceEvent, pending, versions) @@ -2817,7 +2812,6 @@ describe('workspace context pending state', () => { const confirmed = agent.session.append('context/message', { content: context.content, source: context.source, - ...context.envelope !== undefined ? { envelope: context.envelope } : {}, ...context.meta !== undefined ? { meta: context.meta } : {}, }, { surfaceOp: 'append' }) observeInstructionSessionEvent(agent.session, confirmed, pending, versions) diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index ba53f5c0ec..1cf51693b0 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -241,12 +241,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ jsDoc: '/**\n * List direct children of a directory in stable name order. Returns resolved\n * child targets plus cheap metadata only; never reads file contents.\n * @param target - the resolved directory target.\n * @param signal - aborts the listing.\n * @returns one entry per direct child, in stable name order.\n */', }, { - signature: 'abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal): Promise', - jsDoc: '/**\n * Atomically create or replace UTF-8 text. `expected` guards intent and\n * staleness; omission allows unconditional overwrite.\n * @param target - the resolved target to write.\n * @param content - the full new file content.\n * @param expected - the write intent guarding the write; omit for unconditional.\n * @param signal - aborts before the atomic rename takes effect.\n * @returns the outcome, including the version the write produced.\n */', + signature: 'abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxMode?: SandboxMode, ): Promise', + jsDoc: '/**\n * Atomically create or replace UTF-8 text. `expected` guards intent and\n * staleness; omission allows unconditional overwrite.\n * @param target - the resolved target to write.\n * @param content - the full new file content.\n * @param expected - the write intent guarding the write; omit for unconditional.\n * @param signal - aborts before the atomic rename takes effect.\n * @param sandboxMode - the per-call sandbox mode this write runs under; a\n * sandboxing backend fences the write by it, the bare backend ignores it.\n * Omit to leave the backend its own default.\n * @returns the outcome, including the version the write produced.\n */', }, { - signature: 'abstract editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal): Promise', - jsDoc: '/**\n * Atomically edit literal text. When supplied, the version guard is checked\n * before matching so stale content reports `FS_STALE_VERSION`; omission edits\n * the current content without a freshness precondition.\n * @param target - the resolved target to edit.\n * @param edit - the literal search/replace request.\n * @param expected - the version guard; omit for an unconditional edit.\n * @param signal - aborts before the atomic rename takes effect.\n * @returns the outcome, including the version the edit produced.\n */', + signature: 'abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxMode?: SandboxMode, ): Promise', + jsDoc: '/**\n * Atomically edit literal text. When supplied, the version guard is checked\n * before matching so stale content reports `FS_STALE_VERSION`; omission edits\n * the current content without a freshness precondition.\n * @param target - the resolved target to edit.\n * @param edit - the literal search/replace request.\n * @param expected - the version guard; omit for an unconditional edit.\n * @param signal - aborts before the atomic rename takes effect.\n * @param sandboxMode - the per-call sandbox mode this edit runs under; a\n * sandboxing backend fences the edit by it, the bare backend ignores it.\n * Omit to leave the backend its own default.\n * @returns the outcome, including the version the edit produced.\n */', }, ], }, @@ -342,6 +342,11 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, ], }, + { + key: 'sandboxPolicy', + summary: 'The sandbox-policy service (`ctx.sandboxPolicy`).', + methods: [], + }, { key: 'sessionPersistence', summary: 'Durable append-only session storage.', @@ -400,7 +405,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ methods: [ { signature: 'create(id?: SessionId, options?: CreateSessionOptions): Session', - jsDoc: '/**\n * Create a session owned by the calling fiber: disposing that fiber stops\n * event notification and removes the session from the store. `options.seed`\n * populates the session with a copy of those events (replay/fork);\n * `options.meta` attaches creation metadata (validated absolute `cwd`,\n * `parentSession` lineage) as the immutable {@link SessionHeader} (the store\n * fills `version`/`id`/`createdAt`).\n *\n * For an agent whose session must be torn down IN ORDER with its loop (so the\n * loop\'s final flush is captured before the store attachment ends), do NOT use this\n * — fold the session lifecycle into the agent\'s own effect via\n * {@link prepare} + {@link enter} + {@link announce} (see\n * `dsh-agent-loop`\'s creation transaction).\n *\n * @param id - the session id; omitted, the store mints `session-`.\n * @param options - seed events and/or creation metadata for the header.\n * @returns the live session, already entered and announced.\n * @throws if a session with `id` already exists, metadata is not a plain\n * lossless-JSON record with valid scalar fields, or `meta.cwd` is a\n * non-absolute path (storage backends key directories off it).\n */', + jsDoc: '/**\n * Create a session owned by the calling fiber: disposing that fiber stops\n * event notification and removes the session from the store. `options.seed`\n * populates the session with a copy of those events (replay/fork);\n * `options.meta` attaches creation metadata (validated absolute `cwd`, seed\n * and parent lineage, and delegation depth) as the immutable\n * {@link SessionHeader} (the store fills `version`/`id`/`createdAt`).\n *\n * For an agent whose session must be torn down IN ORDER with its loop (so the\n * loop\'s final flush is captured before the store attachment ends), do NOT use this\n * — fold the session lifecycle into the agent\'s own effect via\n * {@link prepare} + {@link enter} + {@link announce} (see\n * `dsh-agent-loop`\'s creation transaction).\n *\n * @param id - the session id; omitted, the store mints `session-`.\n * @param options - seed events and/or creation metadata for the header.\n * @returns the live session, already entered and announced.\n * @throws if a session with `id` already exists, metadata is not a plain\n * lossless-JSON record with valid scalar fields, or `meta.cwd` is a\n * non-absolute path (storage backends key directories off it).\n */', }, { signature: 'prepare(id?: SessionId, options?: CreateSessionOptions): Session', @@ -560,6 +565,24 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, ], }, + { + key: 'toolResultPrune', + summary: 'Deterministic head/middle/tail pruning for current tool-result surface nodes.', + methods: [ + { + signature: 'measureContent(blocks: readonly ContentBlock[]): number', + jsDoc: '/**\n * Measure text content in Unicode code points; non-text blocks cost zero.\n * @param blocks - tool-result content to measure.\n * @returns total Unicode code points across text blocks.\n */', + }, + { + signature: 'pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null', + jsDoc: '/**\n * Replace an over-budget text middle while retaining rich-block order.\n * Text slicing is by Unicode code point, not UTF-16 code unit, so a retained\n * boundary cannot split a surrogate pair. Grapheme clusters may still split.\n * @param blocks - original tool-result content.\n * @returns pruned content, or `null` when the text is within budget.\n */', + }, + { + signature: 'pruneSession(session: Session): PruneResult', + jsDoc: '/**\n * Prune every over-budget tool result from one stable current-surface snapshot.\n * Each replacement preserves the complete event data except for `content`,\n * and points at the shadowed node for durable provenance and replay.\n * @param session - session whose current surface is rewritten.\n * @returns landed replacements and aggregate Unicode-code-point savings.\n * @throws when the session rejects a replacement; replacements committed\n * earlier in the pass remain durable.\n */', + }, + ], + }, { key: 'tools', summary: 'Tool registry and execution pipeline.', @@ -690,8 +713,8 @@ export const EVENT_API: readonly EventApiEntry[] = [ name: 'agent/prompt-submit', mode: 'waterfall', signature: '\'agent/prompt-submit\'(this: Scoped, agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise): Promise', - jsDoc: '/**\n * Allow, rewrite, or block one drained prompt before it becomes a user\n * message. Call `next()` for the unchanged default.\n * @param agent - the agent draining its inbox.\n * @param content - the drained message\'s blocks, as queued.\n * @param source - the message\'s resolved source.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */', - summary: 'Allow, rewrite, or block one drained prompt before it becomes a user message.', + jsDoc: '/**\n * Allow, rewrite, or block one claimed prompt before it becomes a user\n * message. Call `next()` for the unchanged default.\n * @param agent - the agent whose turn claimed the message.\n * @param content - the claimed message\'s blocks, as queued.\n * @param source - the message\'s resolved source.\n * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.\n * @mode waterfall\n */', + summary: 'Allow, rewrite, or block one claimed prompt before it becomes a user message.', }, { name: 'agent/queued', @@ -1113,13 +1136,9 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'ContentBlockType', declaration: 'export type ContentBlockType = keyof ContentBlockMap;', }, - { - name: 'ContextEnvelope', - declaration: 'export type ContextEnvelope = \'context\' | \'raw\';', - }, { name: 'CreateAgentOptions', - declaration: 'export interface CreateAgentOptions {\n readonly sessionId: SessionId;\n readonly meta?: {\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n };\n readonly seed?: readonly SessionEvent[];\n readonly agentOptions?: AgentOptions;\n readonly signal?: AbortSignal;\n readonly setup?: (agentCtx: Context) => Promise | void;\n}', + declaration: 'export interface CreateAgentOptions {\n readonly sessionId: SessionId;\n readonly meta?: {\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly delegationDepth?: number;\n };\n readonly seed?: readonly SessionEvent[];\n readonly agentOptions?: AgentOptions;\n readonly signal?: AbortSignal;\n readonly setup?: (agentCtx: Context) => Promise | void;\n}', }, { name: 'CreateGoalRequest', @@ -1127,7 +1146,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'CreateSessionOptions', - declaration: 'export interface CreateSessionOptions {\n readonly seed?: readonly SessionEvent[];\n readonly meta?: {\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly createdAt?: number;\n readonly seedLength?: number;\n };\n}', + declaration: 'export interface CreateSessionOptions {\n readonly seed?: readonly SessionEvent[];\n readonly meta?: {\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly createdAt?: number;\n readonly seedLength?: number;\n readonly delegationDepth?: number;\n };\n}', }, { name: 'DiffCallView', @@ -1251,11 +1270,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'HookContext', - declaration: 'export interface HookContext {\n content: ContentBlock[];\n source: MessageSource;\n envelope?: ContextEnvelope;\n meta?: JsonValue;\n}', + declaration: 'export interface HookContext {\n content: ContentBlock[];\n source: MessageSource;\n meta?: JsonValue;\n}', }, { name: 'InjectOptions', - declaration: 'export interface InjectOptions extends SendOptions {\n envelope?: ContextEnvelope;\n meta?: JsonValue;\n}', + declaration: 'export interface InjectOptions extends SendOptions {\n meta?: JsonValue;\n}', }, { name: 'JsonValue', @@ -1301,6 +1320,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'PromptSection', declaration: 'export interface PromptSection {\n readonly name: string;\n readonly order: number;\n readonly text: string | ((context: AssembleContext) => string);\n}', }, + { + name: 'PrunedEntry', + declaration: 'export interface PrunedEntry {\n readonly originalSeq: number;\n readonly replacementSeq: number;\n readonly callId: CallId;\n readonly charsBefore: number;\n readonly charsAfter: number;\n}', + }, + { + name: 'PruneResult', + declaration: 'export interface PruneResult {\n readonly pruned: readonly PrunedEntry[];\n readonly charsRemoved: number;\n}', + }, { name: 'ReasoningBlock', declaration: 'export interface ReasoningBlock {\n type: \'reasoning\';\n text: string;\n}', @@ -1339,7 +1366,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionEventMap', - declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n trigger: TurnTrigger;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': {\n content: ContentBlock[];\n source: MessageSource;\n };\n \'prompt/blocked\': {\n content: ContentBlock[];\n source: MessageSource;\n reason: string;\n };\n \'context/message\': {\n content: ContentBlock[];\n source: MessageSource;\n envelope?: ContextEnvelope;\n meta?: JsonValue;\n };\n \'assistant/chunk\': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n content: ContentBlock[];\n provenance: AssistantProvenance;\n usage?: TokenUsage;\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: unknown;\n };\n \'steering/message\': {\n turn: number;\n content: ContentBlock[];\n source: MessageSource; /* …truncated — full shape in source */', + declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n trigger: TurnTrigger;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': {\n content: ContentBlock[];\n source: MessageSource;\n };\n \'prompt/blocked\': {\n content: ContentBlock[];\n source: MessageSource;\n reason: string;\n };\n \'context/message\': {\n content: ContentBlock[];\n source: MessageSource;\n meta?: JsonValue;\n };\n \'assistant/chunk\': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n content: ContentBlock[];\n provenance: AssistantProvenance;\n usage?: TokenUsage;\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: {\n name: string;\n code: string;\n };\n meta?: unknown;\n };\n \'steering/message\': {\n turn: number;\n content: ContentBlock[];\n source: MessageSource;\n };\n \'todo/write\': {\n /* …truncated — full shape in source */', }, { name: 'SessionEventReadRequest', @@ -1375,7 +1402,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionHeader', - declaration: 'export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n}', + declaration: 'export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly delegationDepth?: number;\n}', }, { name: 'SessionId', diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 9c5c869168..acb49b1ba5 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -46,7 +46,9 @@ Configured agents start automatically. A model call requires both `provider` and ### Internal concrete driver -The concrete `Agent` class, its `Inbox`, `runLoop`, and instance-bound publication/start controls are package-internal. The package root exports only the plugin/service/config contract, and the package exports map exposes no `./src/*` escape hatch; lifecycle owners create agents through `ctx.agents` rather than naming, constructing, or starting driver internals. The concrete `send()`, running `steer()`, and open-turn `inject()` materialize content plus resolved source once as detached, deeply frozen lossless JSON; malformed data throws before enqueue or append. An injection that arrives while the current step executes assistant tool calls stays in a FIFO until the batch settles; successful batches place it after the complete result batch, and interrupted batches drain it before the turn closes. One prepared session can be claimed by only one concrete driver, and everything observable happens through session events and the `agent/*` event taxonomy. +The concrete `Agent` class, its `Inbox`, `runLoop`, and instance-bound publication/start controls are package-internal. The package root exports only the plugin/service/config contract, and the package exports map exposes no `./src/*` escape hatch; lifecycle owners create agents through `ctx.agents` rather than naming, constructing, or starting driver internals. One prepared session can be claimed by only one concrete driver, and everything observable happens through session events and the `agent/*` event taxonomy. + +Each concrete `send()` materializes content plus resolved source once as a detached, deeply frozen lossless-JSON FIFO item. If claimed, it is the sole ordinary message in its turn; a successor waits for the preceding ordinary turn's checkpoint to settle, while cancellation, disposal, or a pre-start failure may drop it without a turn. Running `steer()` enters the steering FIFO: an open turn records it at the next steering checkpoint before a request or continuation decision, but policy can still stop before another step; steering left after turn close and its checkpoint becomes later queued input unless terminal turn policy, cancellation, or disposal discards it. Open-turn `inject()` uses the same accepted-value boundary but defers in a FIFO while the current step executes assistant tool calls; successful batches place it after all results, and interrupted batches drain it before turn close. Malformed data throws before enqueue or append. ### Loop lifecycle (`loop.ts`) diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 61b661c082..d569146d64 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -252,7 +252,6 @@ export class ReactLoopAgent implements Agent { const context = { content, source, - ...options?.envelope !== undefined ? { envelope: options.envelope } : {}, ...options?.meta !== undefined ? { meta: options.meta } : {}, } if (isTurnOpen(this.session)) { @@ -398,7 +397,7 @@ export class ReactLoopAgent implements Agent { cancelReason: () => this.cancelReason, clearCancel: () => { this.cancelRequested = false }, withToolBatch: run => this.withToolBatch(run), - // Pre-step cancellation re-parks without emitting a status transition. + // Pre-start cancellation settles queued-work waiters before publishing idle. settleIdle: () => { this.settleIdleWaiters() }, })) } diff --git a/packages/core/agent-loop/src/inbox.ts b/packages/core/agent-loop/src/inbox.ts index abb588b919..b26a79a1ef 100644 --- a/packages/core/agent-loop/src/inbox.ts +++ b/packages/core/agent-loop/src/inbox.ts @@ -15,7 +15,7 @@ export interface InboxMessage { } /** - * Per-agent inbox: a queued FIFO (drained at turn start) and a steering FIFO + * Per-agent inbox: a queued FIFO (dequeued once per turn start) and a steering FIFO * (drained between steps of a running turn). Purely an in-memory mechanism of * the loop — the public surface is `Agent.send()` / `Agent.steer()`. */ @@ -54,11 +54,11 @@ export class Inbox { } /** - * Drain all queued messages (turn start). - * @returns the drained messages in arrival order; the queued FIFO is left empty. + * Remove the oldest queued message for one turn start. + * @returns the oldest message, or `undefined` when the queued FIFO is empty. */ - drainQueued(): InboxMessage[] { - return this.queuedMessages.splice(0) + dequeueQueued(): InboxMessage | undefined { + return this.queuedMessages.shift() } /** @@ -72,7 +72,7 @@ export class Inbox { /** * Discard all pending messages (queued + steering) without delivering them — * used by `cancel()`, which drops un-started work rather than draining it into - * a turn. Unlike `drainQueued`/`drainSteering`, the messages are thrown away. + * a turn. Unlike `dequeueQueued`/`drainSteering`, the messages are thrown away. */ clear(): void { this.queuedMessages.length = 0 diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 2a77afc983..7268182552 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -620,12 +620,7 @@ export class AgentLoop extends Service implements AgentFactory { transaction.assertActive() const session = this.runtime.ctx.sessions.prepare(options.resumeSessionId, { seed: loaded.events, - meta: { - createdAt: loaded.meta.createdAt, - ...loaded.meta.cwd === undefined ? {} : { cwd: loaded.meta.cwd }, - ...loaded.meta.parentSession === undefined ? {} : { parentSession: loaded.meta.parentSession }, - ...loaded.meta.seedLength === undefined ? {} : { seedLength: loaded.meta.seedLength }, - }, + meta: loaded.meta, }) const agent = transaction.prepare(agentOptions, session, this.maxParallelToolCalls) await transaction.waitFor(options.setup?.(agent.ctx)) diff --git a/packages/core/agent-loop/src/loop.ts b/packages/core/agent-loop/src/loop.ts index 9016c16d9b..e8008996a9 100644 --- a/packages/core/agent-loop/src/loop.ts +++ b/packages/core/agent-loop/src/loop.ts @@ -91,16 +91,16 @@ export interface LoopHandle { cancelReason(): string /** Clear the cancel marker (called once per iteration after the turn returns). */ clearCancel(): void - /** Settle idle waiters when pre-running cancellation skips a turn, without emitting `agent/status`. */ + /** Settle idle waiters before pre-running cancellation publishes idle. */ settleIdle(): void /** Run an active tool-call batch, accepting post-tool context into the FIFO drained before settlement. */ readonly withToolBatch: (run: (acceptContext: (context: HookContext) => void) => Promise) => Promise } /** - * Drive queued batches as durable turns until disposal. Plugin failures end the - * current turn without terminating the driver. The caller establishes the - * `ctx.agents.withInitiator()` boundary before entry; package-private + * Drive queued messages as independent durable turns until disposal. Plugin + * failures end the current turn without terminating the driver. The caller + * establishes the `ctx.agents.withInitiator()` boundary before entry; package-private * orchestration recovers that exact Agent and captures its Session locally. * @param ctx - the plugin context the loop reaches its initiating Agent, * events (agent/…, session/flush), and services (systemPrompt, llm, tools) @@ -118,20 +118,35 @@ export async function runLoop(ctx: Context, handle: LoopHandle): Promise { const events = agentEvents(ctx, agent) while (!handle.isDisposed()) { - await handle.inbox.waitForQueued(handle.disposed) - if (handle.isDisposed()) break - - // Cancellation between wake and `running` skips only the cancelled work; - // a replacement prompt still runs and owns the eventual idle transition. + // An idle listener can enqueue and cancel replacement work before the next + // wait is installed. Consume that empty marker before parking the driver. if (handle.isCancelled()) { handle.clearCancel() if (!handle.inbox.hasQueued) { handle.settleIdle() + handle.setStatus('idle') + continue + } + } + + await handle.inbox.waitForQueued(handle.disposed) + if (handle.isDisposed()) break + + // Cancellation between wake and `running` skips only the cancelled work; + // a replacement prompt still runs before the eventual idle transition. + if (handle.isCancelled()) { + handle.clearCancel() + if (!handle.inbox.hasQueued) { + // Settle before publishing idle: the already-idle path has no status + // transition, while an idle listener can register waiters for new work. + handle.settleIdle() + handle.setStatus('idle') continue } } handle.setStatus('running') + if (handle.isDisposed()) break // A synchronous `running` listener can cancel before `runTurn`; balance the // status only when no replacement prompt was queued by that listener. @@ -182,12 +197,11 @@ async function runTurn( return messages.length > 0 } - // Drain before opening the turn, but append only after `turn/start`. - const queued = handle.inbox.drainQueued() - const first = queued[0] + // Claim one queued message before opening its turn, but append it only after `turn/start`. + const message = handle.inbox.dequeueQueued() /* v8 ignore next 3 -- invariant guard: runLoop only calls runTurn when hasQueued */ - if (!first) throw new Error('runTurn invariant violated: no queued message at turn start') - const trigger: TurnTrigger = { kind: 'message', source: first.source } + if (!message) throw new Error('runTurn invariant violated: no queued message at turn start') + const trigger: TurnTrigger = { kind: 'message', source: message.source } let reason: TurnEndReason = { kind: 'completed' } let step = 0 @@ -226,56 +240,36 @@ async function runTurn( // matter what throws below; the catch + closeTurn guarantee it. A pre-commit // veto leaves no turn/start in the log and therefore owes no turn/end. session.append('turn/start', { turn, trigger }) - // Each drained queued message runs the `agent/prompt-submit` waterfall before - // it becomes a `user/message` — a hook can rewrite the prompt or block it. + // The claimed message runs the `agent/prompt-submit` waterfall before it + // becomes a `user/message` — a hook can rewrite the prompt or block it. // Recorded INSIDE the turn (after turn/start) so every event is turn-enclosed; // turn/end is now owed, so a throwing prompt-submit listener (the waterfall // throws) is caught below and the turn still closes. - let anyAllowed = false - // Seeded with a floor (only observable if the batch were empty, which - // runTurn never allows — it is called with ≥1 queued message); each `block` - // decision carries a required `reason` and overwrites it, so a fully-blocked - // batch always reports the last vetoing reason. - let lastBlockReason = 'prompt blocked by hook' - for (const message of queued) { - const decision = await events.waterfall( - 'agent/prompt-submit', message.content, message.source, - () => Promise.resolve({ kind: 'allow' }), - ) - if (decision.kind === 'block') { - lastBlockReason = decision.reason - // Record the veto durably: `PromptDecision.reason` is the durable record - // of why a prompt was blocked, but a fully-blocked batch's `rejected` - // turn/end only preserves the LAST reason, and a MIXED batch (this prompt - // blocked, another allowed) does not end `rejected` at all — so without - // this append a blocked prompt would vanish from the log whenever any - // sibling prompt is allowed. `prompt/blocked` sits in the open turn in - // place of the `user/message` this prompt would have become. - session.append('prompt/blocked', { content: message.content, source: message.source, reason: decision.reason }) - continue - } - anyAllowed = true + const promptDecision = await events.waterfall( + 'agent/prompt-submit', message.content, message.source, + () => Promise.resolve({ kind: 'allow' }), + ) + if (promptDecision.kind === 'block') { + session.append('prompt/blocked', { content: message.content, source: message.source, reason: promptDecision.reason }) + reason = { kind: 'rejected', reason: promptDecision.reason } + } else { // `allow.content` REPLACES the prompt bytes (a rewrite); absent keeps them. - const content = decision.content ?? message.content + const content = promptDecision.content ?? message.content session.append('user/message', { content, source: message.source }, { surfaceOp: 'append' }) // Every `allow.additionalContexts` entry is a separate context/message the // next request also sees. The turn is open, so inject() appends each one - // into THIS turn without flattening provenance, framing, or metadata. - for (const context of decision.additionalContexts ?? []) { + // into THIS turn without flattening provenance or metadata. + for (const context of promptDecision.additionalContexts ?? []) { agent.inject(context.content, { source: context.source, - ...context.envelope !== undefined ? { envelope: context.envelope } : {}, ...context.meta !== undefined ? { meta: context.meta } : {}, }) } } while (true) { - // A fully blocked batch closes its zero-step turn as rejected. - if (!anyAllowed) { - reason = { kind: 'rejected', reason: lastBlockReason } - break - } + // A blocked prompt closes its zero-step turn as rejected. + if (promptDecision.kind === 'block') break step += 1 // Steering from the previous round's continuation listeners joins before diff --git a/packages/core/agent-loop/tests/cancel.spec.ts b/packages/core/agent-loop/tests/cancel.spec.ts index 14e7cf8976..39289779be 100644 --- a/packages/core/agent-loop/tests/cancel.spec.ts +++ b/packages/core/agent-loop/tests/cancel.spec.ts @@ -79,7 +79,8 @@ describe('Agent.cancel()', () => { // send() queues synchronously (status still idle, loop microtask not yet // resumed). Cancel in that pre-step window: the queued turn must not run. - send(agent, 'drop me') + send(agent, 'drop me first') + send(agent, 'drop me second') agent.cancel('pre-step') // Give the loop a chance to wake and process the cancel. @@ -91,6 +92,35 @@ describe('Agent.cancel()', () => { expect(agent.status).toBe('idle') }) + it('disposal from the running notification drops queued work before turn start', async () => { + const adapter = new MockAdapter([textResponse('should not run')]) + const ctx = await harness(adapter) + const handle = await ctx.agents.create({ + sessionId: SessionId('dispose-running-session'), + agentOptions: { provider: 'mock', model: 'mock' }, + }) + const agent = handle.agent + + const running = Promise.withResolvers() + let disposalDone: Promise | undefined + ctx.on('agent/status', (subject, status) => { + if (subject !== agent || status !== 'running') return + disposalDone = handle.dispose() + running.resolve(undefined) + }) + + send(agent, 'drop before claim') + await running.promise + if (disposalDone === undefined) throw new Error('running listener did not start disposal') + await disposalDone + await driverDone(agent) + + expect(agent.status).toBe('disposed') + expect(agent.session.events.some(event => event.type === 'turn/start')).toBe(false) + expect(userTexts(agent)).toEqual([]) + expect(adapter.requests).toHaveLength(0) + }) + it('a whenIdle() waiter registered BEFORE a pre-step cancel resolves (F1 hang guard)', async () => { const adapter = new MockAdapter([textResponse('x')]) const ctx = await harness(adapter) @@ -110,7 +140,162 @@ describe('Agent.cancel()', () => { expect(agent.status).toBe('idle') }) - it('cancel() mid-step aborts the in-flight model call; the turn ends aborted', async () => { + it('cancel() between consecutive turns restores idle and leaves idle steer usable', async () => { + const adapter = new MockAdapter([textResponse('first reply'), textResponse('steer reply')]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('between-turn-cancel'), { provider: 'mock', model: 'mock' }) + + let rejectFirstFlush = true + ctx.on('session/flush', (session) => { + if (session !== agent.session || !rejectFirstFlush) return + rejectFirstFlush = false + throw new Error('first flush failed') + }) + + const cancelled = Promise.withResolvers() + ctx.on('agent/error', (subject, _turn, _step, error) => { + if (subject !== agent || error.message !== 'first flush failed') return + // The first hop runs before runLoop resumes from runTurn; the second lands + // before its resolved waitForQueued continuation checks cancellation. + queueMicrotask(() => { + queueMicrotask(() => { + agent.cancel('between turns') + cancelled.resolve(undefined) + }) + }) + }) + + const statuses: string[] = [] + ctx.on('agent/status', (subject, status) => { + if (subject === agent) statuses.push(status) + }) + + send(agent, 'first') + send(agent, 'queued tail') + await cancelled.promise + + expect(agent.status).toBe('idle') + expect(statuses).toEqual(['running', 'idle']) + expect(adapter.requests).toHaveLength(1) + expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(1) + expect(userTexts(agent)).toEqual(['first']) + + let idleResolved = false + void agent.whenIdle().then(() => { idleResolved = true }) + await Promise.resolve() + expect(idleResolved).toBe(true) + + const idle = waitForIdle(ctx, agent) + agent.steer([{ type: 'text', text: 'idle steer' }]) + await idle + + expect(statuses).toEqual(['running', 'idle', 'running', 'idle']) + expect(adapter.requests).toHaveLength(2) + expect(userTexts(agent)).toEqual(['first', 'idle steer']) + }) + + it('an idle-listener replacement keeps whenIdle pending until the replacement turn finishes', async () => { + const adapter = new MockAdapter([textResponse('first reply'), textResponse('replacement reply')]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('between-turn-idle-listener'), { provider: 'mock', model: 'mock' }) + + let rejectFirstFlush = true + ctx.on('session/flush', (session) => { + if (session !== agent.session || !rejectFirstFlush) return + rejectFirstFlush = false + throw new Error('first flush failed') + }) + + ctx.on('agent/error', (subject, _turn, _step, error) => { + if (subject !== agent || error.message !== 'first flush failed') return + queueMicrotask(() => { + queueMicrotask(() => { agent.cancel('between turns') }) + }) + }) + + const replacementRegistered = Promise.withResolvers() + let replacementObservation: Promise<{ status: string; requests: number; turns: number }> | undefined + ctx.on('agent/status', (subject, status) => { + if (subject !== agent || status !== 'idle' || replacementObservation !== undefined) return + send(agent, 'replacement') + replacementObservation = agent.whenIdle().then(() => ({ + status: agent.status, + requests: adapter.requests.length, + turns: agent.session.events.filter(event => event.type === 'turn/start').length, + })) + replacementRegistered.resolve(undefined) + }) + + send(agent, 'first') + send(agent, 'cancelled tail') + await replacementRegistered.promise + if (replacementObservation === undefined) throw new Error('idle listener did not register replacement work') + + await expect(replacementObservation).resolves.toEqual({ status: 'idle', requests: 2, turns: 2 }) + expect(userTexts(agent)).toEqual(['first', 'replacement']) + }) + + it('idle-listener cancellation settles its waiter without cancelling later work', async () => { + const adapter = new MockAdapter([textResponse('first reply'), textResponse('later reply')]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('idle-listener-cancel'), { provider: 'mock', model: 'mock' }) + + const replacementRegistered = Promise.withResolvers() + let replacementObservation: Promise<{ status: string; requests: number; turns: number }> | undefined + ctx.on('agent/status', (subject, status) => { + if (subject !== agent || status !== 'idle' || replacementObservation !== undefined) return + send(agent, 'cancelled replacement') + replacementObservation = agent.whenIdle().then(() => ({ + status: agent.status, + requests: adapter.requests.length, + turns: agent.session.events.filter(event => event.type === 'turn/start').length, + })) + agent.cancel('idle listener') + replacementRegistered.resolve(undefined) + }) + + send(agent, 'first') + await replacementRegistered.promise + if (replacementObservation === undefined) throw new Error('idle listener did not register replacement work') + + await expect(Promise.race([ + replacementObservation, + new Promise((_resolve, reject) => setTimeout(() => { reject(new Error('whenIdle hung after idle-listener cancel')) }, 1000)), + ])).resolves.toEqual({ status: 'idle', requests: 1, turns: 1 }) + + const idle = waitForIdle(ctx, agent) + send(agent, 'later') + await idle + expect(adapter.requests).toHaveLength(2) + expect(userTexts(agent)).toEqual(['first', 'later']) + }) + + it('replacement work queued after idle-listener cancellation still runs', async () => { + const adapter = new MockAdapter([textResponse('first reply'), textResponse('replacement reply')]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('idle-listener-post-cancel-send'), { provider: 'mock', model: 'mock' }) + + const replacementRegistered = Promise.withResolvers() + let replacementIdle: Promise | undefined + ctx.on('agent/status', (subject, status) => { + if (subject !== agent || status !== 'idle' || replacementIdle !== undefined) return + send(agent, 'cancelled replacement') + agent.cancel('idle listener') + send(agent, 'surviving replacement') + replacementIdle = agent.whenIdle() + replacementRegistered.resolve(undefined) + }) + + send(agent, 'first') + await replacementRegistered.promise + if (replacementIdle === undefined) throw new Error('idle listener did not register replacement work') + await replacementIdle + + expect(adapter.requests).toHaveLength(2) + expect(userTexts(agent)).toEqual(['first', 'surviving replacement']) + }) + + it('cancel() mid-step aborts the active turn and drops every queued tail item', async () => { const adapter = new MockAdapter(['hang']) const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) @@ -121,10 +306,14 @@ describe('Agent.cancel()', () => { send(agent, 'go') await new Promise(r => setTimeout(r, 30)) expect(agent.status).toBe('running') + send(agent, 'queued tail') agent.cancel('mid-step') await waitForIdle(ctx, agent) expect(reasons).toEqual([{ kind: 'aborted', reason: 'mid-step' }]) + expect(userTexts(agent)).toEqual(['go']) + expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(1) + expect(adapter.requests).toHaveLength(1) }) it('cancel() with no reason defaults to "cancelled" when aborting an in-flight step', async () => { diff --git a/packages/core/agent-loop/tests/contract-regressions.spec.ts b/packages/core/agent-loop/tests/contract-regressions.spec.ts index 80d085c32b..b436c4468b 100644 --- a/packages/core/agent-loop/tests/contract-regressions.spec.ts +++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts @@ -632,15 +632,20 @@ describe('plugin exceptions are contained', () => { expect(agent.status).toBe('idle') }) - it('a rejecting session/flush listener is reported but does not kill the agent', async () => { + it('a rejecting first-turn flush settles before the queued tail starts', async () => { const adapter = new MockAdapter([textResponse('one'), textResponse('two')]) const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - let rejectedOnce = false - ctx.on('session/flush', async () => { - if (!rejectedOnce) { - rejectedOnce = true + const firstFlush = Promise.withResolvers() + const releaseFirstFlush = Promise.withResolvers() + let flushes = 0 + ctx.on('session/flush', async (session) => { + if (session !== agent.session) return + flushes += 1 + if (flushes === 1) { + firstFlush.resolve(undefined) + await releaseFirstFlush.promise throw new Error('disk full') } }) @@ -648,18 +653,25 @@ describe('plugin exceptions are contained', () => { const errors: Error[] = [] ctx.on('agent/error', (_agent, _turn, _step, error) => void errors.push(error)) + const idle = waitForIdle(ctx, agent) send(agent, 'first') - await waitForIdle(ctx, agent) - expect(errors.map(e => e.message)).toEqual(['disk full']) - send(agent, 'second') - await waitForIdle(ctx, agent) + + await firstFlush.promise + expect(adapter.requests).toHaveLength(1) + expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(1) + + releaseFirstFlush.resolve(undefined) + await idle + + expect(errors.map(e => e.message)).toEqual(['disk full']) expect(adapter.requests).toHaveLength(2) + expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(2) }) }) describe('disposed status is part of the agent/status contract', () => { - it('disposing the fiber emits agent/status(disposed) and ends the turn with reason disposed', async () => { + it('disposing the fiber ends the active turn and never starts its queued tail', async () => { const adapter = new MockAdapter(['hang']) const ctx = await harness(adapter) @@ -675,11 +687,19 @@ describe('disposed status is part of the agent/status contract', () => { send(agent, 'go') await new Promise(r => setTimeout(r, 30)) + send(agent, 'queued tail') await fiber.dispose() await driverDone(agent) expect(statuses).toEqual(['running', 'disposed']) expect(reasons).toEqual([{ kind: 'disposed' }]) + expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(1) + const messages = agent.session.events + .filter(event => event.type === 'user/message') + .flatMap(event => event.data.content) + .flatMap(block => block.type === 'text' ? [block.text] : []) + expect(messages).toEqual(['go']) + expect(adapter.requests).toHaveLength(1) }) it('a throwing agent/status listener cannot break disposal or leak the registry entry', async () => { diff --git a/packages/core/agent-loop/tests/coverage-edges.spec.ts b/packages/core/agent-loop/tests/coverage-edges.spec.ts index a1d449d273..85f99e8a66 100644 --- a/packages/core/agent-loop/tests/coverage-edges.spec.ts +++ b/packages/core/agent-loop/tests/coverage-edges.spec.ts @@ -146,12 +146,22 @@ describe('toError normalization', () => { const errors: Error[] = [] ctx.on('agent/error', (_agent, _turn, _step, error) => void errors.push(error)) - send(agent, 'go') + send(agent, 'fails before turn start') + send(agent, 'survives as the next item') await waitForIdle(ctx, agent) expect(errors).toHaveLength(1) expect(errors[0]).toMatchObject({ message: 'naked string error', code: 'UNKNOWN' }) - expect(adapter.requests).toEqual([]) - expect(agent.session.events.some(event => event.type === 'turn/start' || event.type === 'turn/end')).toBe(false) + expect(adapter.requests).toHaveLength(1) + const starts = agent.session.events.filter(event => event.type === 'turn/start') + const ends = agent.session.events.filter(event => event.type === 'turn/end') + const messages = agent.session.events.filter(event => event.type === 'user/message') + expect(starts).toHaveLength(1) + expect(starts[0]?.type === 'turn/start' && starts[0].data.turn).toBe(1) + expect(ends).toHaveLength(1) + expect(messages).toHaveLength(1) + expect(messages[0]?.type === 'user/message' && messages[0].data.content).toEqual([ + { type: 'text', text: 'survives as the next item' }, + ]) }) it('normalizes non-Error throws from agent/request waterfall via inline toError in runStep catch', async () => { diff --git a/packages/core/agent-loop/tests/inbox.spec.ts b/packages/core/agent-loop/tests/inbox.spec.ts index 4bea62abe2..f4eea9fdd0 100644 --- a/packages/core/agent-loop/tests/inbox.spec.ts +++ b/packages/core/agent-loop/tests/inbox.spec.ts @@ -8,17 +8,17 @@ function resolverPair() { } describe('Inbox', () => { - it('enqueues and drains queued messages in FIFO order', () => { + it('dequeues one queued message at a time in FIFO order', () => { const inbox = new Inbox() inbox.enqueue({ content: [{ type: 'text', text: 'first' }], source: { kind: 'user' } }) inbox.enqueue({ content: [{ type: 'text', text: 'second' }], source: { kind: 'user' } }) expect(inbox.hasQueued).toBe(true) - const drained = inbox.drainQueued() - expect(drained).toHaveLength(2) - expect(drained[0]!.content[0]).toMatchObject({ text: 'first' }) - expect(drained[1]!.content[0]).toMatchObject({ text: 'second' }) + expect(inbox.dequeueQueued()?.content[0]).toMatchObject({ text: 'first' }) + expect(inbox.hasQueued).toBe(true) + expect(inbox.dequeueQueued()?.content[0]).toMatchObject({ text: 'second' }) expect(inbox.hasQueued).toBe(false) + expect(inbox.dequeueQueued()).toBeUndefined() }) it('pushes and drains steering messages separately from queued', () => { diff --git a/packages/core/agent-loop/tests/interception.spec.ts b/packages/core/agent-loop/tests/interception.spec.ts index 6c0757b460..83b156b0ef 100644 --- a/packages/core/agent-loop/tests/interception.spec.ts +++ b/packages/core/agent-loop/tests/interception.spec.ts @@ -99,7 +99,6 @@ describe('agent/prompt-submit', () => { additionalContexts: [{ content: [{ type: 'text', text: 'extra ctx' }], source: { kind: 'plugin', plugin: 'test' }, - envelope: 'raw', meta, }], })) @@ -113,7 +112,6 @@ describe('agent/prompt-submit', () => { expect(userMsg).toBeDefined() expect(ctxMsg?.type === 'context/message' && ctxMsg.data.content).toEqual([{ type: 'text', text: 'extra ctx' }]) expect(ctxMsg?.type === 'context/message' && ctxMsg.data.source).toEqual({ kind: 'plugin', plugin: 'test' }) - expect(ctxMsg?.type === 'context/message' && ctxMsg.data.envelope).toBe('raw') expect(ctxMsg?.type === 'context/message' && ctxMsg.data.meta).toEqual(meta) const sent = JSON.stringify(adapter.requests[0]!.messages) expect(sent).toContain('extra ctx') @@ -179,9 +177,7 @@ describe('agent/prompt-submit', () => { expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason).toEqual({ kind: 'rejected', reason: 'blocked by policy' }) }) - it('a mixed batch records a prompt/blocked for the vetoed prompt while the allowed one runs', async () => { - // Blocking one prompt in a mixed batch must persist its reason even though - // the allowed prompt keeps the turn from ending rejected. + it('adjacent blocked and allowed prompts keep independent turn outcomes', async () => { const adapter = new MockAdapter([textResponse('ran once')]) const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) @@ -194,13 +190,13 @@ describe('agent/prompt-submit', () => { const reasons: TurnEndReason[] = [] ctx.on('session/event', (_s, event: SessionEvent) => { if (event.type === 'turn/end') reasons.push(event.data.reason) }) - // both sends land before the loop drains → one batched turn + // Both sends land before the driver wakes, but each remains its own turn. send(agent, 'secret') send(agent, 'safe') await waitForIdle(ctx, agent) const log = events(agent) - // the allowed prompt became a user/message and drove exactly one model call + // The allowed prompt became a user/message and drove exactly one model call. const userMsgs = log.filter(e => e.type === 'user/message') expect(userMsgs).toHaveLength(1) expect(userMsgs[0]?.type === 'user/message' && userMsgs[0].data.content).toEqual([{ type: 'text', text: 'safe' }]) @@ -212,12 +208,14 @@ describe('agent/prompt-submit', () => { content: [{ type: 'text', text: 'secret' }], reason: 'policy: no secrets', }) - // the turn did NOT reject — a sibling was allowed — so the boundary reason - // alone would not have preserved the block - expect(reasons.some(r => r.kind === 'rejected')).toBe(false) + expect(log.filter(e => e.type === 'turn/start')).toHaveLength(2) + expect(reasons).toEqual([ + { kind: 'rejected', reason: 'policy: no secrets' }, + { kind: 'completed' }, + ]) }) - it('a throwing prompt-submit listener ends the turn balanced (error), loop survives', async () => { + it('a throwing prompt-submit listener ends its turn balanced while an adjacent message survives', async () => { const adapter = new MockAdapter([textResponse('after')]) const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) @@ -228,20 +226,31 @@ describe('agent/prompt-submit', () => { return { kind: 'allow' as const } }) const errors: Error[] = [] + const reasons: TurnEndReason[] = [] + const statuses: string[] = [] ctx.on('agent/error', (_a, _t, _s, error) => void errors.push(error)) + ctx.on('agent/status', (subject, status) => { if (subject === agent) statuses.push(status) }) + ctx.on('session/event', (session, event) => { + if (session === agent.session && event.type === 'turn/end') reasons.push(event.data.reason) + }) + const idle = waitForIdle(ctx, agent) send(agent, 'first') - await waitForIdle(ctx, agent) - expect(errors.map(e => e.message)).toEqual(['prompt hook broke']) - // turn balanced - const log = events(agent) - expect(log.filter(e => e.type === 'turn/start')).toHaveLength(1) - expect(log.filter(e => e.type === 'turn/end')).toHaveLength(1) - - // loop survives: a second prompt runs normally send(agent, 'second') - await waitForIdle(ctx, agent) - expect(adapter.requests.length).toBeGreaterThanOrEqual(1) + await idle + expect(errors.map(e => e.message)).toEqual(['prompt hook broke']) + // The failed prompt forms one balanced error turn; the adjacent prompt forms + // the following normal turn without an intermediate idle transition. + const log = events(agent) + expect(log.filter(e => e.type === 'turn/start')).toHaveLength(2) + expect(log.filter(e => e.type === 'turn/end')).toHaveLength(2) + expect(reasons).toEqual([ + { kind: 'error', step: 0, message: 'prompt hook broke' }, + { kind: 'completed' }, + ]) + expect(statuses).toEqual(['running', 'idle']) + expect(adapter.requests).toHaveLength(1) + expect(JSON.stringify(adapter.requests[0]!.messages)).toContain('second') }) }) @@ -556,7 +565,6 @@ describe('tool additionalContexts buffering across a step', () => { additionalContexts: [{ content: [{ type: 'text', text: `ctx-${exec.callId}` }], source: { kind: 'plugin', plugin: 'p' }, - envelope: 'raw', meta: { callId: exec.callId }, }], })) @@ -580,7 +588,6 @@ describe('tool additionalContexts buffering across a step', () => { .map(b => (b.type === 'text' ? b.text : '')) expect(ctxTexts).toEqual(['ctx-c1', 'ctx-c2']) const contextEvents = events(agent).filter(e => e.type === 'context/message') - expect(contextEvents.map(e => e.type === 'context/message' && e.data.envelope)).toEqual(['raw', 'raw']) expect(contextEvents.map(e => e.type === 'context/message' && e.data.meta)).toEqual([{ callId: 'c1' }, { callId: 'c2' }]) }) @@ -591,7 +598,7 @@ describe('tool additionalContexts buffering across a step', () => { name: 'composite', description: 'composite', parameters: {}, async execute(_args, exec) { exec.deferContext({ content: [{ type: 'text', text: 'nested-a' }], source: { kind: 'plugin', plugin: 'a' }, meta: { order: 1 } }) - exec.deferContext({ content: [{ type: 'text', text: 'nested-b' }], source: { kind: 'plugin', plugin: 'b' }, envelope: 'raw', meta: { order: 2 } }) + exec.deferContext({ content: [{ type: 'text', text: 'nested-b' }], source: { kind: 'plugin', plugin: 'b' }, meta: { order: 2 } }) return [{ type: 'text', text: 'outer result' }] }, })) diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts index c58479609b..dd686edfcb 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -354,14 +354,24 @@ describe('agent loop', () => { expect(flat).toContain('change of plans') }) - it('steering while idle behaves like send (starts a turn)', async () => { - const adapter = new MockAdapter([textResponse('ok')]) + it('same-tick idle steering inherits one-send-one-turn FIFO behavior', async () => { + const adapter = new MockAdapter([textResponse('first'), textResponse('second')]) const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) - agent.steer([{ type: 'text', text: 'hello' }]) - await waitForIdle(ctx, agent) - expect(agent.session.events.some(e => e.type === 'user/message')).toBe(true) + const idle = waitForIdle(ctx, agent) + agent.steer([{ type: 'text', text: 'first idle steer' }]) + agent.steer([{ type: 'text', text: 'second idle steer' }]) + await idle + + expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(2) + expect(agent.session.events + .filter(event => event.type === 'user/message') + .map(event => event.data.content)).toEqual([ + [{ type: 'text', text: 'first idle steer' }], + [{ type: 'text', text: 'second idle steer' }], + ]) + expect(adapter.requests).toHaveLength(2) }) it('inject() while idle wraps context in a one-shot turn, visible to the next request', async () => { @@ -385,10 +395,10 @@ describe('agent loop', () => { await waitForIdle(ctx, agent) const flat = JSON.stringify(adapter.requests[0]!.messages) expect(flat).toContain('file changed: a.ts') - expect(flat).toContain('') + expect(flat).not.toContain(' { + it('inject() persists structured context content verbatim with durable hidden meta', async () => { const adapter = new MockAdapter([textResponse('ok')]) const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('raw-context'), { provider: 'mock', model: 'mock' }) @@ -401,14 +411,13 @@ describe('agent loop', () => { agent.inject([{ type: 'text', text }], { source: { kind: 'plugin', plugin: 'workspace-context' }, - envelope: 'raw', meta, }) send(agent, 'go') await waitForIdle(ctx, agent) const contextEvent = agent.session.events.find(event => event.type === 'context/message') - expect(contextEvent?.type === 'context/message' && contextEvent.data).toMatchObject({ envelope: 'raw', meta }) + expect(contextEvent?.type === 'context/message' && contextEvent.data).toMatchObject({ meta }) const requestText = JSON.stringify(adapter.requests[0]!.messages) expect(requestText).toContain('Additional instructions from: pkg/AGENTS.md') expect(requestText).not.toContain(' { const first = { type: 'text' as const, text: 'mid-turn notice' } agent.inject([first], { source: { kind: 'plugin', plugin: 'x' }, - envelope: 'raw', meta, }) first.text = 'mutated after inject' @@ -458,7 +466,6 @@ describe('agent loop', () => { expect(contexts).toHaveLength(2) expect(result.seq).toBeLessThan(contexts[0]!.seq) expect(contexts[0]?.type === 'context/message' && contexts[0].data).toMatchObject({ - envelope: 'raw', meta, }) expect(contexts.flatMap(event => event.type === 'context/message' ? event.data.content : [])) @@ -925,7 +932,149 @@ describe('agent loop', () => { expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind).toBe('completed') }) - it('chains queued messages into consecutive turns', async () => { + it('keeps same-tick sends in separate turns and checkpoints before the next starts', async () => { + const adapter = new MockAdapter([textResponse('first answer'), textResponse('second answer')]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) + + const firstFlush = Promise.withResolvers() + const releaseFirstFlush = Promise.withResolvers() + let flushes = 0 + ctx.on('session/flush', async (session) => { + if (session !== agent.session) return + flushes += 1 + if (flushes === 1) { + firstFlush.resolve(undefined) + await releaseFirstFlush.promise + } + }) + + const turns: number[] = [] + ctx.on('session/event', (session, event) => { + if (session === agent.session && event.type === 'turn/start') turns.push(event.data.turn) + }) + + const idle = waitForIdle(ctx, agent) + send(agent, 'first message') + send(agent, 'second message') + + await firstFlush.promise + expect(turns).toEqual([1]) + expect(adapter.requests).toHaveLength(1) + + releaseFirstFlush.resolve(undefined) + await idle + + expect(turns).toEqual([1, 2]) + expect(flushes).toBe(2) + expect(adapter.requests).toHaveLength(2) + expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('first answer') + expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('second message') + }) + + it('holds a turn-end listener send behind the closing turn checkpoint', async () => { + const adapter = new MockAdapter([textResponse('first answer'), textResponse('second answer')]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) + + const firstFlush = Promise.withResolvers() + const releaseFirstFlush = Promise.withResolvers() + let flushes = 0 + ctx.on('session/flush', async (session) => { + if (session !== agent.session) return + flushes += 1 + if (flushes === 1) { + firstFlush.resolve(undefined) + await releaseFirstFlush.promise + } + }) + + const turns: number[] = [] + const statuses: string[] = [] + ctx.on('agent/status', (subject, status) => { + if (subject === agent) statuses.push(status) + }) + ctx.on('session/event', (session, event) => { + if (session !== agent.session) return + if (event.type === 'turn/start') turns.push(event.data.turn) + if (event.type === 'turn/end' && event.data.turn === 1) send(agent, 'turn-end listener message') + }) + + const idle = waitForIdle(ctx, agent) + send(agent, 'first message') + await firstFlush.promise + + expect(turns).toEqual([1]) + expect(adapter.requests).toHaveLength(1) + + releaseFirstFlush.resolve(undefined) + await idle + + expect(turns).toEqual([1, 2]) + expect(statuses).toEqual(['running', 'idle']) + expect(adapter.requests).toHaveLength(2) + expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('first answer') + expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('turn-end listener message') + }) + + it('keeps a reentrant agent/queued send as the next independent turn', async () => { + const adapter = new MockAdapter([textResponse('first'), textResponse('second')]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) + + let nested = false + ctx.on('agent/queued', (subject) => { + if (subject !== agent || nested) return + nested = true + send(agent, 'queued listener message') + }) + + const idle = waitForIdle(ctx, agent) + send(agent, 'outer message') + await idle + + const turns = agent.session.events.filter(event => event.type === 'turn/start') + const messages = agent.session.events + .filter(event => event.type === 'user/message') + .map(event => event.data.content) + expect(turns).toHaveLength(2) + expect(messages).toEqual([ + [{ type: 'text', text: 'outer message' }], + [{ type: 'text', text: 'queued listener message' }], + ]) + }) + + it('preserves independent turn sources across an adjacent microtask send', async () => { + const adapter = new MockAdapter([textResponse('first'), textResponse('second')]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) + + const idle = waitForIdle(ctx, agent) + agent.send([{ type: 'text', text: 'user message' }]) + await Promise.resolve() + agent.send( + [{ type: 'text', text: 'plugin message' }], + { source: { kind: 'plugin', plugin: 'test' } }, + ) + await idle + + const triggers = agent.session.events + .filter(event => event.type === 'turn/start') + .map(event => event.data.trigger) + const sources = agent.session.events + .filter(event => event.type === 'user/message') + .map(event => event.data.source) + expect(triggers).toEqual([ + { kind: 'message', source: { kind: 'user' } }, + { kind: 'message', source: { kind: 'plugin', plugin: 'test' } }, + ]) + expect(sources).toEqual([ + { kind: 'user' }, + { kind: 'plugin', plugin: 'test' }, + ]) + }) + + it('keeps a session-listener send after dequeue in the following turn', async () => { const adapter = new MockAdapter([textResponse('first'), textResponse('second')]) const ctx = await harness(adapter) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) @@ -948,6 +1097,37 @@ describe('agent loop', () => { expect(turns).toEqual([1, 2]) expect(adapter.requests).toHaveLength(2) + expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('first') + expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('second message') + }) + + it('keeps a model-adapter callback send in the following turn', async () => { + const agentRef: { current?: Agent } = {} + const adapter = new MockAdapter([ + () => { + const agent = agentRef.current + if (agent === undefined) throw new Error('model callback ran before agent setup') + send(agent, 'model callback message') + return textResponse('first') + }, + textResponse('second'), + ]) + const ctx = await harness(adapter) + const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) + agentRef.current = agent + + const idle = waitForIdle(ctx, agent) + send(agent, 'outer message') + await idle + + const messages = agent.session.events + .filter(event => event.type === 'user/message') + .map(event => event.data.content) + expect(agent.session.events.filter(event => event.type === 'turn/start')).toHaveLength(2) + expect(messages).toEqual([ + [{ type: 'text', text: 'outer message' }], + [{ type: 'text', text: 'model callback message' }], + ]) }) it('awaits session/flush at turn end (persistence checkpoint)', async () => { diff --git a/packages/core/agent-loop/tests/properties.spec.ts b/packages/core/agent-loop/tests/properties.spec.ts index ff7a6337ce..85efda0e4e 100644 --- a/packages/core/agent-loop/tests/properties.spec.ts +++ b/packages/core/agent-loop/tests/properties.spec.ts @@ -81,6 +81,21 @@ function turnNumbers(agent: Agent): number[] { .map(e => (e.data as { turn: number }).turn) } +function turnEndNumbers(agent: Agent): number[] { + return agent.session.events + .filter(e => e.type === 'turn/end') + .map(e => (e.data as { turn: number }).turn) +} + +function userMessageCountsByTurn(agent: Agent): number[] { + const counts: number[] = [] + for (const event of agent.session.events) { + if (event.type === 'turn/start') counts.push(0) + if (event.type === 'user/message') counts[counts.length - 1]! += 1 + } + return counts +} + /** Assert a status trace is a legal run: idle/running alternating, ending idle. */ function assertLegalStatusTrace(trace: string[]): void { for (let i = 1; i < trace.length; i++) { @@ -90,7 +105,7 @@ function assertLegalStatusTrace(trace: string[]): void { } describe('agent loop scheduling properties', () => { - it('a synchronous burst loses no message and uses strictly increasing turns', async () => { + it('a synchronous burst gives every message its own strictly increasing turn', async () => { await fc.assert(fc.asyncProperty( fc.array(fc.string({ minLength: 1 }), { minLength: 1, maxLength: 6 }), async (texts) => { @@ -105,8 +120,11 @@ describe('agent loop scheduling properties', () => { // No message lost: every send appears as a user/message, in order. expect(userMessageTexts(agent)).toEqual(texts) - // A synchronous burst batches into exactly one turn. - expect(turnNumbers(agent)).toEqual([1]) + // This failure-free fixture maps every item to an independent turn. + expect(turnNumbers(agent)).toEqual(texts.map((_, i) => i + 1)) + expect(turnEndNumbers(agent)).toEqual(texts.map((_, i) => i + 1)) + expect(userMessageCountsByTurn(agent)).toEqual(texts.map(() => 1)) + expect(trace).toEqual(['running', 'idle']) assertLegalStatusTrace(trace) } finally { await ctx.fiber.dispose() @@ -137,9 +155,9 @@ describe('agent loop scheduling properties', () => { ), { numRuns: 20, timeout: 2000 }) }) - it('mixed schedule (send, optionally settle) loses no message and orders turns', async () => { - // Each step is a (text, settle?) pair: settle=true awaits idle before the - // next send (own turn); settle=false sends in the same tick (batches). + it('mixed settled and same-tick sends preserve one turn per message', async () => { + // Each step optionally waits for idle before the next send; that scheduling + // choice must not change the ordinary message-to-turn mapping. const stepArb = fc.record({ text: fc.string({ minLength: 1 }), settle: fc.boolean() }) await fc.assert(fc.asyncProperty( fc.array(stepArb, { minLength: 1, maxLength: 6 }), @@ -158,14 +176,13 @@ describe('agent loop scheduling properties', () => { } await lastIdle - // No message lost or reordered, regardless of batching. + // No message is lost or reordered, regardless of driver timing. expect(userMessageTexts(agent)).toEqual(steps.map(s => s.text)) - // Turn numbers are a strictly increasing 1..N prefix (N = turn count). + // Every item forms one FIFO-ordered turn containing only that message. const turns = turnNumbers(agent) - expect(turns).toEqual(turns.map((_, i) => i + 1)) - // Every message landed in some turn; turns never exceed messages. - expect(turns.length).toBeLessThanOrEqual(steps.length) - expect(turns.length).toBeGreaterThanOrEqual(1) + expect(turns).toEqual(steps.map((_, i) => i + 1)) + expect(turnEndNumbers(agent)).toEqual(turns) + expect(userMessageCountsByTurn(agent)).toEqual(steps.map(() => 1)) } finally { await ctx.fiber.dispose() } diff --git a/packages/core/agent-loop/tests/resume.spec.ts b/packages/core/agent-loop/tests/resume.spec.ts index 57635bbe5f..0547388d96 100644 --- a/packages/core/agent-loop/tests/resume.spec.ts +++ b/packages/core/agent-loop/tests/resume.spec.ts @@ -411,7 +411,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume', await ctx.fiber.dispose() }) - it('resume of a forked session preserves the parentSession lineage and seed boundary in the header', async () => { + it('resume of a forked session preserves the lineage, seed boundary, and delegation depth in the header', async () => { // Lifecycle 1: persist a FORKED session (carries parentSession + seedLength // in its header) by creating it with a complete-turn seed — the write path // materializes the fork (header + seed) on disk. @@ -423,7 +423,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume', const { ctx: ctx1, root } = await persistentHarness(adapter1) const forked = ctx1.sessions.create(SessionId('forked-sess'), { seed, - meta: { cwd: '/w', parentSession: SessionId('parent-sess'), seedLength: seed.length }, + meta: { cwd: '/w', parentSession: SessionId('parent-sess'), seedLength: seed.length, delegationDepth: 1 }, }) await ctx1.parallel('session/flush', forked) await ctx1.fiber.dispose() @@ -447,6 +447,9 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume', expect(a2.session.header.parentSession).toBe('parent-sess') expect(a2.session.header.cwd).toBe('/w') expect(a2.session.header.seedLength).toBe(seed.length) + // The recursion budget survives resume — a dropped depth would let a + // resumed child delegate as if it were top-level. + expect(a2.session.header.delegationDepth).toBe(1) await ctx2.fiber.dispose() }) diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index c2b46f210d..23a7bfddf0 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -46,7 +46,7 @@ The lifecycle edges have two important local caveats. `agent/created` runs after Most interception points are cooperative waterfalls returning seam-specific decisions. `agent/pre-step` and `agent/post-step` are serial checkpoints around a step's durable work, while `agent/request-error` is the failed-model-request recovery waterfall: a retry opens a new numbered step after the failed step closes. `agent/turn-stop` is the terminal serial fold: it runs after ordinary continuation and steering folding, and a returned stop remains in force through turn close and flush so later steering cannot create an extra step or turn. Ordinary queued prompts remain intact. The full rationale for scoped dispatch and terminal settlement is in the [agent-scope runtime-design Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way). -`PromptDecision.additionalContexts` is an array so every injected context keeps its own source, envelope, and metadata. A `ContinuationDecision` reason is narrower: it becomes a `steering/message`, not a `context/message`, and therefore carries only content and source. +`PromptDecision.additionalContexts` is an array so every injected context keeps its own source and metadata. A `ContinuationDecision` reason is narrower: it becomes a `steering/message`, not a `context/message`, and therefore carries only content and source. Turn and step boundaries and the model token stream are durable `session/event` facts rather than mirrored `agent/*` notifications. Consumers read `turn/*`, `step/*`, and `assistant/chunk` from the session feed; tool policy and outcome observation belong to the complete pipeline documented by [`dsh-tools`](../tools/README.md). @@ -54,13 +54,15 @@ Turn and step boundaries and the model token stream are durable `session/event` The handle every plugin programs against: -- `agent.send(content, options?)` — queue a message; starts a turn when idle. Content and resolved source become one detached, deeply frozen lossless-JSON record before `agent/queued` and enqueue; invalid data throws synchronously, and caller or notification-listener in-place mutation cannot change the log or model input (`agent/prompt-submit` still rewrites by returning replacement content). -- `agent.steer(content, options?)` — steer a running turn (inject between steps); uses the same owned acceptance boundary and behaves like `send` when idle -- `agent.inject(content, options?)` — accept detached in-session context without running the model; the next request sees its `context/message`. `options.envelope` defaults to the canonical `` framing and may be `'raw'` when the caller owns a complete familiar frame; `options.meta` persists opaque JSON state without rendering it. While a turn is open it joins that turn, deferring FIFO while the current tool batch executes and draining before turn close if execution is interrupted; while idle it is wrapped in a one-shot `injection` turn and durability checkpoint ([the turn-enclosure invariant](../../../.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md)). +- `agent.send(content, options?)` — queue one independent FIFO item. If claimed, that item becomes the sole ordinary message in its turn; a claimed FIFO successor waits for that turn's checkpoint to settle. Broad cancellation, disposal, or a pre-start failure may instead drop it without a turn. Content and resolved source become one detached, deeply frozen lossless-JSON record before `agent/queued` and enqueue; invalid data throws synchronously, and caller or notification-listener in-place mutation cannot change the log or model input (`agent/prompt-submit` still rewrites by returning replacement content). The [one-send-one-turn Agent Note](../../../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md) owns the rationale. +- `agent.steer(content, options?)` — submit steering while the agent is `running`. An open turn records it at the next steering checkpoint before a request or continuation decision; policy can still stop before another step. After turn close and its checkpoint, remaining steering becomes later queued input unless terminal turn policy, cancellation, or disposal discards it. The method uses the same synchronous snapshot-and-validation boundary as `send` and delegates to `send` when idle +- `agent.inject(content, options?)` — accept detached in-session context without running the model; the next request sees its `context/message` with `content` rendered verbatim as a user-role message. `options.meta` persists opaque JSON state without rendering it. While a turn is open it joins that turn, deferring FIFO while the current tool batch executes and draining before turn close if execution is interrupted; while idle it is wrapped in a one-shot `injection` turn and durability checkpoint ([the turn-enclosure invariant](../../../.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md)). - `agent.cancel(reason?)` — cancel ALL pending work: clears the queued + steering FIFOs, aborts the in-flight step, and drops a turn about to start (the pre-step window) so a queued-but-not-started prompt never runs. A UI/ACP `session/cancel` maps to this. The single public stop primitive. Idle with nothing pending → a safe no-op. - `agent.whenIdle()` — resolve once the agent reaches quiescence after settling out of `running` (idle → immediately; disposed → awaits the loop exit). A non-owner's quiescence-observation hook: it observes the work settling WITHOUT tearing the agent down. Teardown is separate — a lifecycle owner stops and unregisters via `AgentHandle.dispose()`, which awaits the loop exit directly. - `agent.session`, `agent.status`, `agent.options`, `agent.id` +`running` describes a driver-wide drain interval, not proof that a turn is still open; it can cover turn close, the durability checkpoint, and consecutive queued turns. + ### Extension points - Agent creation: `AgentLoop.create()` is the concrete config-path implementation (in `dsh-agent-loop`), while programmatic consumers create/resume owned agents through `ctx.agents.create()` / `ctx.agents.resume()`. Replace the loop by implementing `Agent` and registering via `ctx.agents.register()`. diff --git a/packages/core/agent/src/index.ts b/packages/core/agent/src/index.ts index 75f00daebb..061ddae53f 100644 --- a/packages/core/agent/src/index.ts +++ b/packages/core/agent/src/index.ts @@ -46,15 +46,21 @@ export interface CreateAgentOptions { readonly sessionId: SessionId /** * Session creation metadata: validated absolute `cwd`, `parentSession` - * fork lineage, and the `seedLength` seed boundary. Mirrors the - * `cwd`/`parentSession`/`seedLength` fields of + * fork lineage, the `seedLength` seed boundary, and the `delegationDepth` + * recursion budget. Mirrors the + * `cwd`/`parentSession`/`seedLength`/`delegationDepth` fields of * {@link CreateSessionOptions.meta} in dsh-session (the internal-only * `createdAt`, used when reconstructing a persisted session, is deliberately * excluded — a factory caller never sets it). This is durable session data, * so the session boundary validates and snapshots it before asynchronous * setup begins. */ - readonly meta?: { readonly cwd?: string; readonly parentSession?: SessionId; readonly seedLength?: number } + readonly meta?: { + readonly cwd?: string + readonly parentSession?: SessionId + readonly seedLength?: number + readonly delegationDepth?: number + } /** * Seed events to reconstruct the child session's log from (the fork lineage * primitive). When present, the factory creates the session with this event diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts index 702861f407..3af6e76371 100644 --- a/packages/core/agent/src/types.ts +++ b/packages/core/agent/src/types.ts @@ -8,7 +8,7 @@ import type { Context } from 'cordis' import type { Scoped } from '@deepseek-ai/dsh-scope' import type { ContentBlock, LlmCallConfig, Message, MessageSource } from '@deepseek-ai/dsh-llm' -import type { ContextEnvelope, JsonValue, Session, SessionId } from '@deepseek-ai/dsh-session' +import type { JsonValue, Session, SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-system-prompt' declare module '@deepseek-ai/dsh-system-prompt' { interface AssembleContext { @@ -32,17 +32,15 @@ export interface SendOptions { /** Options specific to durable synthetic context injection. */ export interface InjectOptions extends SendOptions { - /** Keep the canonical context tag, or send caller-owned framing verbatim. */ - envelope?: ContextEnvelope /** Opaque JSON state retained in the session event but hidden from the model. */ meta?: JsonValue } /** * An agent's lifecycle state, emitted on every transition as `agent/status`: - * `idle` (parked, waiting for queued work), `running` (a turn is in progress), - * `disposed` (terminal — no transition leaves it, and `send`/`steer`/`inject` - * throw). + * `idle` (parked, waiting for queued work), `running` (the driver is draining + * work and may be closing or checkpointing a turn), `disposed` (terminal — no + * transition leaves it, and `send`/`steer`/`inject` throw). */ export type AgentStatus = 'idle' | 'running' | 'disposed' @@ -50,16 +48,15 @@ export type AgentStatus = 'idle' | 'running' | 'disposed' export interface HookContext { content: ContentBlock[] source: MessageSource - /** Keep the canonical context tag, or use caller-owned framing verbatim. */ - envelope?: ContextEnvelope /** Opaque JSON state retained in the session event but hidden from the model. */ meta?: JsonValue } /** * Prompt interception result. `allow.content` replaces the prompt and each - * `additionalContexts` entry becomes a separate context message. `block` records a - * durable `prompt/blocked`; an all-blocked batch ends a zero-step rejected turn. + * `additionalContexts` entry becomes a separate context message. `block` + * records a durable `prompt/blocked` and ends the claimed prompt's zero-step + * turn as rejected. */ export type PromptDecision = | { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: HookContext[] } @@ -97,15 +94,20 @@ export interface Agent { readonly ctx: Context /** - * Queue detached, frozen lossless-JSON input; starts a turn when idle. + * Queue one detached, frozen lossless-JSON item. If claimed, it is the sole + * ordinary message in its FIFO-ordered turn; the next claimed item waits for + * that turn's checkpoint. * Invalid input throws synchronously before notification or enqueue. */ send(content: ContentBlock[], options?: SendOptions): void /** - * Steer a running turn: content is injected between steps of the current - * turn. Uses the same owned-value and synchronous-validation boundary as - * {@link send}; when idle, behaves exactly like that method. + * Submit steering while the agent is `running`. An open turn records it at + * the next steering checkpoint before a request or continuation decision; + * policy may stop before another step. After turn close and its checkpoint, + * any remainder is queued for a later turn; terminal `agent/turn-stop`, + * cancellation, or disposal may discard it. Uses the same synchronous + * snapshot-and-validation boundary as {@link send}; when idle, delegates to it. */ steer(content: ContentBlock[], options?: SendOptions): void @@ -119,10 +121,11 @@ export interface Agent { inject(content: ContentBlock[], options?: InjectOptions): void /** - * Clear queued and steering work, including work waiting to start, and abort - * the active step. The supplied reason is preserved across pre-step and active - * cancellation windows, and `whenIdle()` resolves after cancellation reaches - * quiescence. Idle cancellation is a no-op and does not arm a later cancel. + * Clear all queued and steering work, including items waiting to start, and + * abort the active step. The supplied reason is preserved across pre-step + * and active cancellation windows, and `whenIdle()` resolves after + * cancellation reaches quiescence. Idle cancellation is a no-op and does not + * arm a later cancel. */ cancel(reason?: string): void @@ -203,10 +206,10 @@ declare module 'cordis' { */ 'agent/pre-step'(this: Scoped, agent: Agent, turn: number, step: number, signal: AbortSignal): Promise | void /** - * Allow, rewrite, or block one drained prompt before it becomes a user + * Allow, rewrite, or block one claimed prompt before it becomes a user * message. Call `next()` for the unchanged default. - * @param agent - the agent draining its inbox. - * @param content - the drained message's blocks, as queued. + * @param agent - the agent whose turn claimed the message. + * @param content - the claimed message's blocks, as queued. * @param source - the message's resolved source. * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. * @mode waterfall diff --git a/packages/core/session/README.md b/packages/core/session/README.md index a60be8da72..f9ce78ad14 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -8,7 +8,7 @@ Creates and holds event-sourced `Session` instances. Persistence is intentionall ### Public API -- `ctx.sessions.create(id?, { seed?, meta? }?)` validates and detaches durable seed/header data, fills the version and id, defaults `createdAt` to now, publishes the session, and binds it to the calling fiber. Persisted reconstruction supplies its original `createdAt` and `seedLength`. +- `ctx.sessions.create(id?, { seed?, meta? }?)` validates and detaches durable seed/header data, fills the version and id, defaults `createdAt` to now, publishes the session, and binds it to the calling fiber. Persisted reconstruction supplies its original `createdAt`, `seedLength`, and `delegationDepth`. - `ctx.sessions.flush(session)` dispatches the awaited parallel durability checkpoint through the session's captured scope. Every listener starts and the call waits for all to settle before reporting failure; unpublished, detached, and stale objects reject. - `ctx.sessions.fork(source, boundary?, childSessionId?): Session` — Resolve a live session object or id, select a seed through the inclusive `boundary` event seq (default: current last event), require that boundary to be `turn/end`, and create a live child session with lineage metadata. - `ctx.sessions.get(id: SessionId): Session | undefined` @@ -32,13 +32,13 @@ The store pairs announced creation with disposal, publishes post-commit append n Plain class (not a Cordis Service). Create via `ctx.sessions.create()`. -- `session.append(type, data, opts?)` snapshots and freezes durable data and surface metadata, validates marker shape, provenance, and complete replacement coverage, commits synchronously, then notifies observers with independent failure containment. Reentrant attached-session appends reject, and runtime checks cover widened unions and loaded logs. +- `session.append(type, data, opts?)` snapshots and freezes durable data and surface metadata, validates marker shape, provenance, complete replacement coverage, and content-only single-result `tool/result` rewrites, commits synchronously, then notifies observers with independent failure containment. Reentrant attached-session appends reject, and runtime checks cover widened unions and loaded logs. - `session.deriveMessages()` incrementally projects each new surface entry once and returns a fresh array over shared frozen messages. Assistant projections preserve provider/model provenance and adapter-private replay state. A surface rewrite rebuilds the projection; there is no raw-log fallback. - `session.deriveEventMessage(event)` is the canonical per-event projection used by reconstruction and invariants. - `session.surface` exposes the readonly `SessionSurface` view owned by the session's single incremental surface manager; `replaceGeneration` changes on every committed rewrite. - `session.events` is a cached frozen snapshot invalidated by append; accepted events remain deeply frozen. - `session.seq`, `session.id` — current sequence and readonly typed identity. -- `session.header: SessionHeader` — detached, deep-frozen creation metadata (`version`, `id`, `createdAt`, optional `cwd`/`parentSession`/`seedLength`). Construction validates the durable record and requires its id to match `session.id`. +- `session.header: SessionHeader` — detached, deep-frozen creation metadata (`version`, `id`, `createdAt`, optional `cwd`/`parentSession`/`seedLength`/`delegationDepth`). Construction validates the durable record and requires its id to match `session.id`. ### Lossless JSON utilities @@ -49,14 +49,14 @@ Durable values need one accepted representation, not a check followed by a secon - `SurfaceOp` — how an event entered the ordered surface: `'append'` (normal tail append) or `{ op: 'replace', start, end }` (replace entries from `start` through `end` inclusive — both must be valid surface seqs; `start === end` replaces one entry). Used by compaction to shadow old events without deleting them. - `SurfaceIntent` — `{ surfaceOp: SurfaceOp; sourceEventSeqs?: number[] }`, the required third parameter to `session.append()` for surface-eligible types. - `SessionSurface` — the readonly live `nodes` and `replaceGeneration` projection exposed by `session.surface`; candidate validation remains private to `Session`. -- `foldSurface(events)` — replay the canonical surface contract into detached current event sequences and actual replacement ranges. The same pass rejects non-contiguous seqs, misplaced or malformed metadata, empty or duplicate provenance, non-earlier sources, invalid positional ranges, and replacements that fail to cite every shadowed surface entry; `SurfaceManager` shares the atomic transition while retaining only its incremental sequence cache. +- `foldSurface(events)` — replay the canonical surface contract into detached current event sequences and actual replacement ranges. The same pass rejects non-contiguous seqs, misplaced or malformed metadata, empty or duplicate provenance, non-earlier sources, invalid positional ranges, replacements that fail to cite every shadowed surface entry, and a `tool/result` replacement that changes anything except one current result's `content`; `SurfaceManager` shares the atomic transition while retaining only its incremental sequence cache. - `isSurfaceEvent(event)` / `isSurfaceEligibleType(type)` — the first narrows a `SessionEvent` to a fully formed surface event; the second detects a surface-eligible event missing its marker when validating a seed or loaded log. ### Request-header reconstruction (`request-header.ts`) `request/header` records a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, or `change`. `foldRequestHeader()` selects the latest snapshot; legacy delta events and the removed `fallback` reason are rejected. `messagePrefix` remains separate from derived history. See the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md). -`context/message` defaults to the canonical tagged context projection. A producer may set `envelope: 'raw'` when its `content` already contains the complete model-facing frame, and may attach JSON `meta` for replayable plugin state; metadata remains durable but is excluded from `deriveMessages()`. +`context/message` renders its `content` verbatim as a user-role message, and may attach JSON `meta` for replayable plugin state; metadata remains durable but is excluded from `deriveMessages()`. ### Session event vocabulary (`types.ts`) @@ -73,13 +73,13 @@ Every `SessionEvent` carries two optional top-level fields (structural metadata) ### Metadata types (`types.ts`) -- `SessionHeader` — session metadata written once when published as `Session.header`, where detachment and deep-freezing enforce immutability at runtime: `{ version, id, createdAt, cwd?, parentSession?, seedLength? }`. Persistence loaders may return mutable detached copies of the same data type. Owned here (beside `SessionId`) because `Session.header` is typed by it; persistence backends re-export it rather than own it (which would force a package cycle). +- `SessionHeader` — session metadata written once when published as `Session.header`, where detachment and deep-freezing enforce immutability at runtime: `{ version, id, createdAt, cwd?, parentSession?, seedLength?, delegationDepth? }`. Persistence loaders may return mutable detached copies of the same data type. Owned here (beside `SessionId`) because `Session.header` is typed by it; persistence backends re-export it rather than own it (which would force a package cycle). ### Extension points - Persistence plugins: subscribe to `session/event` (write-behind) and drain on `session/flush` (awaited) and fiber dispose. A durable backend reads the log and reloads it into a live session; the metadata seam (`SessionHeader`, `session.header`) is what such a backend stores beside the log. - Replay/fork: `create(id, { seed })` validates and freezes a contiguous current-format log and rebuilds its surface; request headers require provider/model and assistant messages require provider/model provenance. `fork(source, boundary?, childSessionId?)` selects a completed-turn prefix and records lineage. -- Compaction: the `dsh-compact-basic` plugin appends a `user/message` with `surfaceOp: { op: 'replace', start, end }` to shadow old surface entries behind a summary checkpoint. Tool-pairing boundary policy and its cache belong to the [`dsh-compact` seam](../../compact/compact/README.md), while this package owns ordered surface membership and `replaceGeneration`. +- Compaction: `dsh-compact-basic` appends a `user/message` replacement for summary checkpoints, while `dsh-compact-tool-result-prune` appends a content-only `tool/result` replacement. Tool-pairing boundary policy and its cache belong to the [`dsh-compact` seam](../../compact/compact/README.md), while this package owns ordered surface membership, replacement validation, and `replaceGeneration`. ## Model Experience @@ -87,7 +87,7 @@ Every `SessionEvent` carries two optional top-level fields (structural metadata) #### What the model sees -The model receives projections of `user/message`, `assistant/message`, and `tool/result` surface entries verbatim. A `context/message` is a user-role message containing exactly ``, its content blocks, and ``; `steering/message` uses the identical `` / `` wrapper. Tool calls live inside assistant messages. Chunks, boundaries, usage, hook records, todo records, and other log-only events add no message. +The model receives projections of `user/message`, `assistant/message`, `tool/result`, `context/message`, and `steering/message` surface entries verbatim: each is a user- or assistant-role message carrying its content blocks unchanged. Tool calls live inside assistant messages. Chunks, boundaries, usage, hook records, todo records, and other log-only events add no message. #### Token effect diff --git a/packages/core/session/src/index.ts b/packages/core/session/src/index.ts index 16732f7903..9b2eb74a37 100644 --- a/packages/core/session/src/index.ts +++ b/packages/core/session/src/index.ts @@ -11,9 +11,9 @@ import { isAbsolute } from 'node:path' import { deepFreeze } from '@deepseek-ai/dsh-llm' import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope' import type { Scoped } from '@deepseek-ai/dsh-scope' -import type { ContentBlock, Message, MessageSource } from '@deepseek-ai/dsh-llm' +import type { Message } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, SessionId } from './types.ts' -import type { ContextEnvelope, CreateSessionOptions, EpochHeader, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SurfaceIntent, SurfaceEventType } from './types.ts' +import type { CreateSessionOptions, EpochHeader, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SurfaceIntent, SurfaceEventType } from './types.ts' import { snapshotJsonValue } from './json.ts' import { SurfaceManager } from './surface.ts' import type { SessionSurface } from './surface.ts' @@ -80,21 +80,6 @@ declare module 'cordis' { } } -/** - * Render injected context as tagged synthetic user-role content, keeping the - * canonical session vocabulary provider-neutral. Adapter-specific exceptions - * belong in the adapter. - */ -function renderTagged(tag: string, content: ContentBlock[], source: MessageSource): ContentBlock[] { - const open = `<${tag} source=${JSON.stringify(source.kind)}>` - const close = `` - return [ - { type: 'text', text: open }, - ...content, - { type: 'text', text: close }, - ] -} - /** Detach, validate, and freeze the creation metadata published by a session. */ function snapshotSessionHeader(id: SessionId, source?: SessionHeader): SessionHeader { const input: unknown = source === undefined @@ -128,6 +113,10 @@ function snapshotSessionHeader(id: SessionId, source?: SessionHeader): SessionHe && (typeof record.seedLength !== 'number' || !Number.isSafeInteger(record.seedLength) || record.seedLength < 0)) { throw new Error('session header seedLength must be a non-negative safe integer') } + if (record.delegationDepth !== undefined + && (typeof record.delegationDepth !== 'number' || !Number.isSafeInteger(record.delegationDepth) || record.delegationDepth < 0)) { + throw new Error('session header delegationDepth must be a non-negative safe integer') + } return deepFreeze(record as unknown as SessionHeader) } @@ -228,22 +217,6 @@ interface SessionEntry { /** Store attachment for the append path; module-private to keep Session store-agnostic publicly. */ const attachments = new WeakMap() -/** - * Render one context contribution exactly as it will appear in model history. - * @param content - content blocks supplied by the context producer. - * @param source - attribution used by the canonical context envelope. - * @param envelope - canonical tagged framing or caller-owned raw framing. - * @returns a detached block list ready for the derived model transcript. - */ -export function renderContextContent( - content: ContentBlock[], - source: MessageSource, - envelope: ContextEnvelope = 'context', -): ContentBlock[] { - const cloned = structuredClone(content) - return envelope === 'raw' ? cloned : renderTagged('context', cloned, source) -} - /** * An event-sourced session: an append-only log of {@link SessionEvent}s. * @@ -509,7 +482,18 @@ export class Session { // trace/replay data. switch (event.type) { - case 'user/message': { + // Injected context and mid-turn steering project identically to a user + // prompt: content verbatim, in user role. context's `source`/`meta` and + // steering's `turn` are log-only and do not reach the model. Do NOT + // re-add per-type framing (e.g. ``/``) here: framing is + // caller-owned — a producer bakes it into `content`, as workspace-context + // does with `` — or, if reintroduced, must be driven by + // the event `meta` map and a dedicated renderer, keeping this projection a + // verbatim pass-through. See the deferred design note in + // ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md + case 'user/message': + case 'context/message': + case 'steering/message': { return { role: 'user', content: event.data.content } } case 'assistant/message': { @@ -526,14 +510,6 @@ export class Session { content: [{ type: 'tool-result', toolCallId: callId, content, isError }], } } - case 'context/message': { - const { content, source, envelope } = event.data - return { role: 'user', content: renderContextContent(content, source, envelope) } - } - case 'steering/message': { - const { content, source } = event.data - return { role: 'user', content: renderTagged('steering', content, source) } - } default: // A non-surface event (boundary, chunk, log-only record) projects to // no message. Merge-extensible union: no assertNever here. @@ -586,9 +562,9 @@ export class SessionStore extends Service { * Create a session owned by the calling fiber: disposing that fiber stops * event notification and removes the session from the store. `options.seed` * populates the session with a copy of those events (replay/fork); - * `options.meta` attaches creation metadata (validated absolute `cwd`, - * `parentSession` lineage) as the immutable {@link SessionHeader} (the store - * fills `version`/`id`/`createdAt`). + * `options.meta` attaches creation metadata (validated absolute `cwd`, seed + * and parent lineage, and delegation depth) as the immutable + * {@link SessionHeader} (the store fills `version`/`id`/`createdAt`). * * For an agent whose session must be torn down IN ORDER with its loop (so the * loop's final flush is captured before the store attachment ends), do NOT use this @@ -650,6 +626,7 @@ export class SessionStore extends Service { ...meta?.cwd === undefined ? {} : { cwd: meta.cwd }, ...meta?.parentSession === undefined ? {} : { parentSession: meta.parentSession }, ...meta?.seedLength === undefined ? {} : { seedLength: meta.seedLength }, + ...meta?.delegationDepth === undefined ? {} : { delegationDepth: meta.delegationDepth }, } return new Session(sessionId, seed, header) } diff --git a/packages/core/session/src/surface.ts b/packages/core/session/src/surface.ts index cf03ae685d..90a2181d53 100644 --- a/packages/core/session/src/surface.ts +++ b/packages/core/session/src/surface.ts @@ -5,6 +5,7 @@ * @module @deepseek-ai/dsh-session/surface */ +import { isDeepStrictEqual } from 'node:util' import type { SessionEvent, SurfaceEvent, SurfaceEventType, SurfaceOp } from './types.ts' /** Runtime counterpart of the message-producing event union. */ @@ -187,11 +188,37 @@ function replacementRange( } } +/** Restrict a tool-result replacement to one current result's content. */ +function assertToolResultRewrite( + event: SessionEvent, + shadowedSeqs: readonly number[], + events: readonly SessionEvent[], +): void { + if (event.type !== 'tool/result') return + if (shadowedSeqs.length !== 1) { + throw new Error('tool/result surface replacement must rewrite exactly one current node') + } + for (const originalSeq of shadowedSeqs) { + const original = events[originalSeq] + if (original?.type !== 'tool/result') { + throw new Error('tool/result surface replacement must target a current tool/result') + } + const originalRest = { ...original.data } as Record + const replacementRest = { ...event.data } as Record + delete originalRest['content'] + delete replacementRest['content'] + if (!isDeepStrictEqual(originalRest, replacementRest)) { + throw new Error('tool/result surface replacement may change only content') + } + } +} + /** Validate one event at its replay boundary and prepare its atomic fold transition. */ function planSurfaceEvent( state: SurfaceFoldState, event: SessionEvent, expectedSeq: number, + events: readonly SessionEvent[], ): SurfacePlan | undefined { if (event.seq !== expectedSeq) { throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`) @@ -204,6 +231,7 @@ function planSurfaceEvent( } const range = replacementRange(state, surfaceOp) assertProvenance(event, range.shadowedSeqs) + assertToolResultRewrite(event, range.shadowedSeqs, events) return { kind: 'replace', seq: event.seq, @@ -218,8 +246,9 @@ function applySurfaceEvent( state: SurfaceFoldState, event: SessionEvent, expectedSeq: number, + events: readonly SessionEvent[], ): SurfaceFoldReplacement | undefined { - const plan = planSurfaceEvent(state, event, expectedSeq) + const plan = planSurfaceEvent(state, event, expectedSeq, events) if (plan?.kind === 'append') { state.nodes.push(plan.seq) } else if (plan?.kind === 'replace') { @@ -239,13 +268,13 @@ function applySurfaceEvent( * Replay a complete session log through the canonical surface fold. * @param events - session events in contiguous seq order. * @returns detached current sequences and replacement history. - * @throws when an event violates surface metadata, provenance, or range rules. + * @throws when an event violates surface metadata, provenance, range, or tool-result rewrite rules. */ export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult { const state = createFoldState() const replacements: SurfaceFoldReplacement[] = [] for (const [index, event] of events.entries()) { - const replacement = applySurfaceEvent(state, event, index) + const replacement = applySurfaceEvent(state, event, index, events) if (replacement !== undefined) replacements.push(replacement) } return { nodes: [...state.nodes], replacements } @@ -266,7 +295,7 @@ export class SurfaceManager implements SessionSurface { */ validateNext(event: SessionEvent): void { if (this._lastProcessedSeq < this.log.length - 1) this._processDelta() - planSurfaceEvent(this._state, event, this.log.length) + planSurfaceEvent(this._state, event, this.log.length, this.log) } /** Monotonic count of folded positional replacements. */ @@ -285,7 +314,7 @@ export class SurfaceManager implements SessionSurface { private _processDelta(): void { for (let i = this._lastProcessedSeq + 1; i < this.log.length; i++) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded by the loop condition - applySurfaceEvent(this._state, this.log[i]!, i) + applySurfaceEvent(this._state, this.log[i]!, i, this.log) this._lastProcessedSeq = i } } diff --git a/packages/core/session/src/types.ts b/packages/core/session/src/types.ts index dc34760e9c..af8a04571e 100644 --- a/packages/core/session/src/types.ts +++ b/packages/core/session/src/types.ts @@ -2,9 +2,6 @@ import type { Branded } from '@deepseek-ai/dsh-brand' import type { AssistantProvenance, CallId, ContentBlock, LlmCallConfig, Message, MessageSource, StreamChunk, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm' import type { JsonValue } from './json.ts' -/** Canonical context-tag framing, or caller-owned framing rendered verbatim. */ -export type ContextEnvelope = 'context' | 'raw' - /** Identifies one session in the store (and its persistence artifacts). */ export type SessionId = Branded<'SessionId'> @@ -50,6 +47,12 @@ export interface SessionHeader { * boundary lets resume and replay distinguish parent history from child work. */ readonly seedLength?: number + /** + * Delegation depth: absent (zero) for a top-level session, parent depth + 1 + * for a subagent child. Persisted so a recursion budget survives restart and + * resume — a runtime-only depth would reset a resumed child to top-level. + */ + readonly delegationDepth?: number } /** @@ -69,6 +72,7 @@ export interface CreateSessionOptions { readonly parentSession?: SessionId readonly createdAt?: number readonly seedLength?: number + readonly delegationDepth?: number } } @@ -109,8 +113,8 @@ export interface TurnEndReasonMap { /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */ 'max-tokens': { kind: 'max-tokens' } /** - * Policy blocked every prompt before the first step. The zero-step turn still - * records a balanced durable boundary and the veto reason. + * Policy blocked the turn's claimed prompt before the first step. The + * zero-step turn still records a balanced durable boundary and veto reason. */ rejected: { kind: 'rejected'; reason: string } /** @@ -179,40 +183,44 @@ export type RequestHeaderReason = 'initial' | 'resume' | 'change' */ export interface SessionEventMap { /** - * Opens turn `turn`. `trigger` records what started it — a drained message - * batch or an idle-time injection. The turn is the durability/replay + * Opens turn `turn`. `trigger` records what started it — one claimed queued + * message or an idle-time injection. The turn is the durability/replay * boundary: every event sits between a `turn/start` and its matching * `turn/end` (the turn-enclosure invariant). */ 'turn/start': { turn: number; trigger: TurnTrigger } /** * Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop - * fires the awaited `session/flush` checkpoint at every turn end, so the turn - * boundary is also the durable-commit boundary. + * awaits `session/flush` after an ordinary turn ends before claiming the next + * queued item. Success commits the turn; rejection is reported live and does + * not prevent later work. */ 'turn/end': { turn: number; reason: TurnEndReason } /** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */ 'step/start': { turn: number; step: number } /** Closes step `step` of turn `turn`. */ 'step/end': { turn: number; step: number } - /** A user-visible prompt (queued message drained at turn start). */ + /** A user-visible prompt (the queued message claimed for this turn). */ 'user/message': { content: ContentBlock[]; source: MessageSource } /** * Durable record of a prompt veto and its reason. It is log-only: the blocked - * prompt never enters the model-visible surface, including in a mixed batch. + * prompt never enters the model-visible surface, and its turn runs zero steps. */ 'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string } /** * In-session context injection (file-change notices, subdir AGENTS.md, * skill content, cron notifications, …). Rendered into the derived history - * as synthetic context — NOT a user prompt. `envelope: 'raw'` lets a caller - * own the complete model-facing frame; `meta` is durable JSON state omitted - * from the model projection. + * as a synthetic user-role message carrying `content` verbatim — NOT a + * user prompt. `meta` is durable JSON state omitted from the model + * projection; it is also the intended channel for any future framing + * directive (a producer declares the frame, a dedicated renderer applies it — + * see the deferred note in + * ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md), + * so the surface keeps projecting `content` verbatim rather than wrapping it. */ 'context/message': { content: ContentBlock[] source: MessageSource - envelope?: ContextEnvelope meta?: JsonValue } /** Raw stream chunk — token-level replay fidelity. */ diff --git a/packages/core/session/tests/session.spec.ts b/packages/core/session/tests/session.spec.ts index 3b15157bde..a17981146d 100644 --- a/packages/core/session/tests/session.spec.ts +++ b/packages/core/session/tests/session.spec.ts @@ -48,7 +48,7 @@ describe('Session', () => { expect(structuredClone(turnEnd.data.reason)).toEqual({ kind: 'max-tokens' }) }) - it('renders context and steering messages as tagged synthetic user content', () => { + it('renders context and steering messages as plain user content', () => { const session = new Session(SessionId('s2')) session.append('context/message', { content: [{ type: 'text', text: 'file changed: a.ts' }], @@ -62,12 +62,12 @@ describe('Session', () => { const [contextMessage, steeringMessage] = session.deriveMessages() expect(contextMessage!.role).toBe('user') - expect(contextMessage!.content[0]).toMatchObject({ type: 'text', text: '' }) - expect(contextMessage!.content.at(-1)).toMatchObject({ type: 'text', text: '' }) - expect(steeringMessage!.content[0]).toMatchObject({ type: 'text', text: '' }) + expect(contextMessage!.content).toEqual([{ type: 'text', text: 'file changed: a.ts' }]) + expect(steeringMessage!.role).toBe('user') + expect(steeringMessage!.content).toEqual([{ type: 'text', text: 'focus on tests' }]) }) - it('renders raw context without a generic envelope while preserving structured metadata', () => { + it('keeps context meta durable in the event while hiding it from the projection', () => { const session = new Session(SessionId('s2-raw')) const meta = { kind: 'workspace-instructions', @@ -77,7 +77,6 @@ describe('Session', () => { session.append('context/message', { content: [{ type: 'text', text: 'Additional instructions from: pkg/AGENTS.md' }], source: { kind: 'plugin', plugin: 'workspace-context' }, - envelope: 'raw', meta, }, { surfaceOp: 'append' }) @@ -882,6 +881,19 @@ describe('SessionStore', () => { }) }) + it('attaches delegationDepth from meta to the header', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + const session = ctx.sessions.create(SessionId('delegated-child'), { + meta: { parentSession: SessionId('parent'), delegationDepth: 2 }, + }) + expect(session.header).toMatchObject({ + id: 'delegated-child', + parentSession: 'parent', + delegationDepth: 2, + }) + }) + it('rejects non-JSON and invalid scalar session metadata', async () => { const ctx = new Context() await ctx.plugin(SessionStore) @@ -893,6 +905,9 @@ describe('SessionStore', () => { { meta: { seedLength: '1' }, error: /seedLength must be a non-negative safe integer/ }, { meta: { seedLength: 0.5 }, error: /seedLength must be a non-negative safe integer/ }, { meta: { seedLength: -1 }, error: /seedLength must be a non-negative safe integer/ }, + { meta: { delegationDepth: '1' }, error: /delegationDepth must be a non-negative safe integer/ }, + { meta: { delegationDepth: 0.5 }, error: /delegationDepth must be a non-negative safe integer/ }, + { meta: { delegationDepth: -1 }, error: /delegationDepth must be a non-negative safe integer/ }, ] for (const [index, { meta, error }] of cases.entries()) { diff --git a/packages/core/session/tests/surface.spec.ts b/packages/core/session/tests/surface.spec.ts index d239533476..fc8ebfcd10 100644 --- a/packages/core/session/tests/surface.spec.ts +++ b/packages/core/session/tests/surface.spec.ts @@ -369,8 +369,8 @@ describe('deriveMessages with surface', () => { s.append('steering/message', { turn: 1, content: [{ type: 'text', text: 'focus' }], source: { kind: 'user' } }, { surfaceOp: 'append' }) const messages = s.deriveMessages() expect(messages).toHaveLength(2) - expect(messages[0]!.content[0]).toMatchObject({ type: 'text', text: '' }) - expect(messages[1]!.content[0]).toMatchObject({ type: 'text', text: '' }) + expect(messages[0]!.content).toEqual([{ type: 'text', text: 'file changed' }]) + expect(messages[1]!.content).toEqual([{ type: 'text', text: 'focus' }]) }) }) diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 6f0959c54a..2cb841d0e2 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -38,7 +38,7 @@ The live registry pipeline has three transformable waterfalls followed by the ob - `ToolExecutionToken` — a fresh branded `Symbol` assigned by the registry. It supports equality correlation only and never crosses a model, log, or worker boundary. - `ToolExecution` — the pipeline-owned call: immutable `{ token, callId, name, arguments, agent?, parent? }` identity plus optional operational `signal`, which an around wrapper may add, replace, remove, and restore. A nested call's `parent` is a `ToolExecutionToken`, not an execution object. - `ToolRunContext` — the execution passed to a tool body, extending `ToolExecution` with `deferContext(context)`. Composite tools use it to ferry context produced by nested dispatches to the outer result even when the tool later throws; it never injects immediately. -- `ToolExecutionResult` — losslessly JSON-serializable outcome: `{ content, isError, error?, additionalContexts?, meta? }`. Call identity stays on the immutable `ToolExecution` supplied alongside the result instead of being duplicated on the outcome. The registry materializes and freezes the complete post-policy value before final observation. On failure with a `HarnessError`, `error: { name, code }` carries the structured failure class alongside the model-facing text. `additionalContexts` preserves each deferred or post-execute `HookContext` with its own source, envelope, and durable JSON metadata; the loop buffers the array and appends each entry as a `context/message` after all `tool/result`s in the step. +- `ToolExecutionResult` — losslessly JSON-serializable outcome: `{ content, isError, error?, additionalContexts?, meta? }`. Call identity stays on the immutable `ToolExecution` supplied alongside the result instead of being duplicated on the outcome. The registry materializes and freezes the complete post-policy value before final observation. On failure with a `HarnessError`, `error: { name, code }` carries the structured failure class alongside the model-facing text. `additionalContexts` preserves each deferred or post-execute `HookContext` with its own source and durable JSON metadata; the loop buffers the array and appends each entry as a `context/message` after all `tool/result`s in the step. - `PreToolDecision` — `{kind:'allow'}` | `{kind:'deny', reason}` | `{kind:'ask', reason?}`. Input rewrite is deliberately not offered; `ask` is serviced by [`ctx.approval`](../../ui/user-approval/README.md) when mounted and otherwise degrades to deny. - `PostToolDecision` — `{kind:'accept', content?, additionalContexts?}` (keep the call successful, optionally replacing the model-facing content) | `{kind:'block', feedback, additionalContexts?}` (turn it into an `isError` whose content is the corrective feedback). Accept preserves tool-deferred contexts before decision contexts; block discards tool-deferred contexts and exposes only contexts explicitly supplied by the blocking decision. - `ToolGuard` — `(execution) => string | undefined`; the returned string is a final monotonic denial reason evaluated after the reorderable pre-execute waterfall and before dispatch. @@ -108,7 +108,7 @@ Returning `undefined` selects generic fallback. Presenters depend only on their Under `code` or `both`, the registry exposes the reserved `run_code` transport and a deterministic TypeScript SDK for the current scope; only program output re-enters model context. Each JSON-normalized binding re-enters the complete tool pipeline sequentially with logged correlation to the outer call. Denials reject that binding, ordinary side effects are not rolled back, and sub-call `additionalContexts` are deferred through the parent result to preserve call/result adjacency. Run settlement aborts and drains outstanding bindings; failures surface as `CodeRunFailedError`. See the [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) and [code-runtime seam](../../code-runtime/README.md). Try `pnpm run demo:code-mode`. - **The SDK section** (`tools:sdk`, order 150): a lazy prompt section regenerating, at each assembly, a `declare const tools: {...}` TypeScript declaration of the calling scope's visible end capabilities (exotic names via quoted keys), plus fixed usage instructions. Deterministic — lexicographic tool order, byte-identical text for an unchanged tool set (prefix-cache-friendly). The codegen (`jsonSchemaToTs`, exported) is total: constructs outside the `defineTool` subset degrade to `unknown`, never throw. -- **The dispatch bridge** (`run_code`'s execute): every binding call is JSON-normalized before dispatch (a value that does not survive — `BigInt`, circulars — rejects that one call, so the dispatched form and logged form are the same JSON value by construction), serialized through a per-run queue (even `Promise.all` executes underlying calls one at a time in submission order), given the outer execution's opaque token as `parent`, and run through the complete pre-execute → guards → execute → post-execute → result pipeline. A denial reaches the program as a binding rejection, and each sub-call is logged as a `tool/code-dispatch` session event with deterministic id `:code:`; `deriveMessages()` does not surface that event. Token correlation lets commit-style observers defer an inner success until the final `run_code` result without exposing the live outer execution; ordinary tool side effects are not rolled back. Every sub-call `additionalContexts` entry is deferred through the outer `ToolRunContext` in dispatch order; the loop appends those contexts only after the parent `run_code` result, preserving adjacency and retaining each source/envelope/meta even when the program later fails. +- **The dispatch bridge** (`run_code`'s execute): every binding call is JSON-normalized before dispatch (a value that does not survive — `BigInt`, circulars — rejects that one call, so the dispatched form and logged form are the same JSON value by construction), serialized through a per-run queue (even `Promise.all` executes underlying calls one at a time in submission order), given the outer execution's opaque token as `parent`, and run through the complete pre-execute → guards → execute → post-execute → result pipeline. A denial reaches the program as a binding rejection, and each sub-call is logged as a `tool/code-dispatch` session event with deterministic id `:code:`; `deriveMessages()` does not surface that event. Token correlation lets commit-style observers defer an inner success until the final `run_code` result without exposing the live outer execution; ordinary tool side effects are not rolled back. Every sub-call `additionalContexts` entry is deferred through the outer `ToolRunContext` in dispatch order; the loop appends those contexts only after the parent `run_code` result, preserving adjacency and retaining each source/meta even when the program later fails. - **Settlement discipline**: the bridge owns a run-scoped abort that follows the outer signal in and fires when the run settles for any reason, so a budget expiry aborts an in-flight sub-tool instead of orphaning it; the bridge then drains its queue BEFORE returning, so every `tool/code-dispatch` lands inside the open turn. A failed run throws `CodeRunFailedError` (`code: 'CODE_RUN_FAILED'`, message = the failure kind + captured logs), which the pipeline converts to a structured `isError` the model self-corrects from. ### Parallel execution diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 4e7ae12898..18fa286a16 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -235,8 +235,8 @@ export interface ToolExecution extends ToolExecutionInput { export interface ToolRunContext extends ToolExecution { /** * Defer one nested-dispatch context until this tool's final result reaches - * the agent loop. Contexts retain their individual source, envelope, and - * metadata and are emitted in call order. + * the agent loop. Contexts retain their individual source and metadata and + * are emitted in call order. */ deferContext(context: HookContext): void } @@ -958,14 +958,19 @@ export class ToolRegistry extends Service { // Freeze the remaining mutable signal slot before observers receive the // shared WeakMap-keyable execution object. Object.freeze(exec) + const { name: toolName, callId } = exec + const reportFailure = (error: unknown): void => { + this.ctx.logger.warn(`tool "${toolName}" (${callId}): tools/result observer failed: ${errorMessage(error)}`) + } const callbacks = this.ctx.events.dispatch('emit', [ scopeTarget(this, exec.agent), 'tools/result', exec, result, ]) for (const callback of callbacks) { try { - callback(exec, result) + const returned: unknown = callback(exec, result) + void Promise.resolve(returned).catch(reportFailure) } catch (error: unknown) { - this.ctx.logger.warn(`tool "${exec.name}" (${exec.callId}): tools/result observer failed: ${errorMessage(error)}`) + reportFailure(error) } } } diff --git a/packages/core/tools/tests/code-mode.spec.ts b/packages/core/tools/tests/code-mode.spec.ts index cfe317ef02..227ff98129 100644 --- a/packages/core/tools/tests/code-mode.spec.ts +++ b/packages/core/tools/tests/code-mode.spec.ts @@ -487,7 +487,6 @@ describe('the run_code dispatch bridge', () => { additionalContexts: [{ content: [{ type: 'text' as const, text: `context for ${exec.callId}` }], source: { kind: 'plugin' as const, plugin: 'test' }, - envelope: 'raw' as const, meta: { callId: exec.callId }, }], }) @@ -505,13 +504,11 @@ describe('the run_code dispatch bridge', () => { { content: [{ type: 'text', text: 'context for call-1:code:1' }], source: { kind: 'plugin', plugin: 'test' }, - envelope: 'raw', meta: { callId: 'call-1:code:1' }, }, { content: [{ type: 'text', text: 'context for call-1:code:2' }], source: { kind: 'plugin', plugin: 'test' }, - envelope: 'raw', meta: { callId: 'call-1:code:2' }, }, ]) diff --git a/packages/core/tools/tests/scoped.spec.ts b/packages/core/tools/tests/scoped.spec.ts index 49ffc0bac9..843aeb3837 100644 --- a/packages/core/tools/tests/scoped.spec.ts +++ b/packages/core/tools/tests/scoped.spec.ts @@ -582,13 +582,18 @@ describe('scoped execution dispatch', () => { ctx.on('tools/result', () => { throw { toString: () => { throw new Error('coercion trap') } } }) + ctx.on('tools/result', () => Promise.reject(new Error('async observer failure')) as never) ctx.on('tools/result', (_exec, result) => { seen.push(result.isError) }) const result = await ctx.tools.execute({ callId: CallId('final'), name: 't', arguments: {}, agent: key }) + await Promise.resolve() expect(result).toMatchObject({ isError: true, content: [{ type: 'text', text: 'outer failure' }] }) expect(seen).toEqual([true, true]) expect(dispatchModes).toEqual(['emit']) - expect(warn).toHaveBeenCalledOnce() - expect(String(warn.mock.calls[0]?.[0])).toContain('') + expect(warn).toHaveBeenCalledTimes(2) + expect(warn.mock.calls.map(call => String(call[0]))).toEqual(expect.arrayContaining([ + expect.stringContaining(''), + expect.stringContaining('async observer failure'), + ])) }) }) diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts index b936887bc9..6c35f4f549 100644 --- a/packages/core/tools/tests/tools.spec.ts +++ b/packages/core/tools/tests/tools.spec.ts @@ -384,7 +384,7 @@ describe('ToolRegistry', () => { parameters: {}, async execute(_args, exec) { exec.deferContext({ content: [{ type: 'text', text: 'nested-1' }], source: { kind: 'plugin', plugin: 'nested-1' }, meta: { n: 1 } }) - exec.deferContext({ content: [{ type: 'text', text: 'nested-2' }], source: { kind: 'plugin', plugin: 'nested-2' }, envelope: 'raw' }) + exec.deferContext({ content: [{ type: 'text', text: 'nested-2' }], source: { kind: 'plugin', plugin: 'nested-2' } }) return [{ type: 'text', text: 'done' }] }, })) @@ -418,7 +418,6 @@ describe('ToolRegistry', () => { { kind: 'plugin', plugin: 'post' }, ]) expect(result.additionalContexts?.[0]?.meta).toEqual({ n: 1 }) - expect(result.additionalContexts?.[1]?.envelope).toBe('raw') }) it('keeps deferred contexts when a composite tool throws, but drops them when the outer call is blocked', async () => { diff --git a/packages/examples/acp-demo/README.md b/packages/examples/acp-demo/README.md index a68fe145c3..e70881af8c 100644 --- a/packages/examples/acp-demo/README.md +++ b/packages/examples/acp-demo/README.md @@ -32,12 +32,13 @@ Because the package wires no logger entry, an ACP leaf has **nothing to get wron | `toolOrder` | — | explicit model-facing tool order (a name list with one `''` rest entry; absent — lexicographic; an unregistered name fails each turn at prompt assembly), routed to `dsh-system-prompt` | | `dshHome` | `$DSH_HOME` or `~/.dsh` | Harness home exposed to model bash and used by local skill discovery | | `tools` | `{ mode: 'native' }` | tool-registry presentation config (`native` / `code` / `both`), routed through `dsh-agent-spine-demo` | +| `workspaceContext` | (required) | workspace-instruction byte budget/config, or `false`; routed to the providerless-safe `dsh-workspace-context` plugin | | `skills` | owner defaults | registry-cache, local-provider, and model-facing skill-tool config, routed through `dsh-agent-spine-demo` | | `toolBash` | owner defaults | model-facing bash config routed through `dsh-agent-spine-demo`, including bash's producer-local `enableRunInBackground` | | `toolTasks` | owner defaults | generic `task_output` wait bounds routed through `dsh-agent-spine-demo` | | `persistenceRoot` | `./.sessions` | the JSONL backend's root directory | -The leaf supplies the swappable backends: an LLM adapter (`llm-deepseek` for the real model, `llm-replay` for keyless snapshot replay) and a bash executor. +The leaf supplies the swappable backends: an LLM adapter (`llm-deepseek` for the real model, `llm-replay` for keyless snapshot replay), a bash executor, and optionally a `ctx.fs` provider. Workspace context becomes a no-op without `ctx.fs`; the shipped [`examples/acp-agent/cordis.yml`](../../../examples/acp-agent/cordis.yml) selects `dsh-sandbox-policy`, `dsh-fs-sandbox`, `dsh-fs-policy`, and `dsh-tool-fs` so baseline instructions and model-facing `read`/`write`/`edit` share one provider, sandbox mode, workspace root, and observed-version policy. ## The bin diff --git a/packages/examples/agent-spine-demo/src/index.ts b/packages/examples/agent-spine-demo/src/index.ts index 74ba5e0cc9..2d489435a5 100644 --- a/packages/examples/agent-spine-demo/src/index.ts +++ b/packages/examples/agent-spine-demo/src/index.ts @@ -140,7 +140,7 @@ export function apply(ctx: Context, config: Config): void { const nestedDshHome = config.skills?.local?.dshHome if (config.dshHome !== undefined && nestedDshHome !== undefined && resolveDshHome(config.dshHome) !== resolveDshHome(nestedDshHome)) { - throw new Error('agent-core: dshHome and skills.local.dshHome must resolve to the same directory') + throw new Error('agent-spine-demo: dshHome and skills.local.dshHome must resolve to the same directory') } const dshHome = resolveDshHome(config.dshHome ?? nestedDshHome) diff --git a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts index bc12706e93..b1faf901ba 100644 --- a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts +++ b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts @@ -280,7 +280,7 @@ describe('dsh-agent-spine-demo bundle', () => { workspaceContext: false, skills: { local: { dshHome: '/nested-dsh-home' } }, }) - }).toThrow(/must resolve to the same directory/) + }).toThrow('agent-spine-demo: dshHome and skills.local.dshHome must resolve to the same directory') }) it('places workspace instructions before the skill catalog in the session prefix', async () => { diff --git a/packages/examples/stdio-demo/tests/built-bin.e2e.ts b/packages/examples/stdio-demo/tests/built-bin.e2e.ts index d3e0a32d09..c2bb459cc9 100644 --- a/packages/examples/stdio-demo/tests/built-bin.e2e.ts +++ b/packages/examples/stdio-demo/tests/built-bin.e2e.ts @@ -104,8 +104,8 @@ async function makeConsumer( return dir } -/** Run the built bin in `cwd` against `configArg` with one stdin line; resolve with stdout/stderr + exit code. */ -function runBuiltBin(cwd: string, configArg: string, line: string): Promise<{ stdout: string; code: number; stderr: string }> { +/** Run the built bin in `cwd` against `configArg` with piped stdin; resolve with stdout/stderr + exit code. */ +function runBuiltBin(cwd: string, configArg: string, input: string): Promise<{ stdout: string; code: number; stderr: string }> { return new Promise((resolve, reject) => { // --expose-internals: the cordis Loader resolves bare plugin specifiers via // its internal module loader (active only under this flag); demo:echo passes @@ -128,7 +128,7 @@ function runBuiltBin(cwd: string, configArg: string, line: string): Promise<{ st }, 25_000) child.on('exit', (code) => { clearTimeout(timer); resolve({ stdout, code: code ?? -1, stderr }) }) child.on('error', (err) => { clearTimeout(timer); reject(err) }) - child.stdin.write(`${line}\n`) + child.stdin.write(`${input}\n`) child.stdin.end() }) } @@ -167,6 +167,17 @@ describe.skipIf(!existsSync(stdioBin))('dsh-stdio-demo BUILT bin (node lib/bin.j expect(code).toBe(0) }, 30_000) + it('runs two synchronously piped lines as two ordinary turns', async () => { + consumer = await makeConsumer('TWO-TURNS ready.') + const { stdout, code, stderr } = await runBuiltBin(consumer, './cordis.yml', 'first\nsecond') + expect(stderr).not.toContain('UNHANDLED') + expect(stdout).toContain('[main turn 1]') + expect(stdout).toContain('You said: "first"') + expect(stdout).toContain('[main turn 2]') + expect(stdout).toContain('You said: "second"') + expect(code).toBe(0) + }, 30_000) + it('boots when optional spill plugins are loaded from a built consumer install', async () => { consumer = await makeConsumer( 'SPILL-OK ready.', diff --git a/packages/fs/README.md b/packages/fs/README.md index ba57eede25..f1025879ac 100644 --- a/packages/fs/README.md +++ b/packages/fs/README.md @@ -6,11 +6,12 @@ The filesystem stack: a provider seam (text IO + atomic mutation with an optiona |---|---|---| | `fs/` | Provider seam: text IO + atomic mutation primitives (optional version guard); owns the `fs/*` policy events | `ctx.fs` | | `fs-local/` | Local-filesystem `FileSystem` implementation | (registers `ctx.fs`) | +| `fs-sandbox/` | Sandbox-enforcing `FileSystem`: extends `fs-local` and fences write/edit by the per-call sandbox mode (read-only denies, workspace-write contains to the workspace + temp roots), reads pass through | (registers `ctx.fs`) | | `fs-policy/` | Policy gate plugin: observed-state + read-before-edit + version-guarded write/edit, via the `fs/*` event gate | (no service — `fs/*` listeners) | -| `tool-fs/` | Model-facing `read`/`write`/`edit` tools AND the executor (reads via `ctx.fs`, owns read windowing, dispatches `fs/*`) | (registers on `ctx.tools`) | +| `tool-fs/` | Model-facing `read`/`write`/`edit` tools AND the executor (reads via `ctx.fs`, owns read windowing, dispatches `fs/*`); advertises the sandbox escalation fields when the mounted `ctx.fs` confines | (registers on `ctx.tools`) | | `tool-fs-search/` | Model-facing `glob`/`grep` discovery tools when `rg` is available on the bash executor `PATH`, backed by fixed ripgrep commands through `ctx.bash`, NOT by `ctx.fs` provider methods | (registers on `ctx.tools`) | -The interface lives at `fs/fs/`. A sandboxed, remote, or project-scoped filesystem backend can replace `fs-local` without touching the seam, the policy gate, or the model-facing tool schemas. The policy (`fs-policy/`) is a plugin that participates only through the `fs/*` event gate, not a service the tool injects — so dropping it gracefully loses the policy and leaves the unconstrained bare provider rather than breaking the tool. A deployment that loads `tool-fs/` is expected to also load it. Discovery (`tool-fs-search/`) deliberately does NOT extend the provider seam: search is a process-backed `rg` workflow on the bash executor, so filesystem backends stay free of a universal search contract; its tools register only when that executor can find `rg`, and its results are follow-up-readable when the bash workdir and the `read` root are the same workspace (the co-located deployment its README documents). +The interface lives at `fs/fs/`. A sandboxed, remote, or project-scoped filesystem backend can replace `fs-local` without touching the seam, the policy gate, or the model-facing tool schemas — `fs-sandbox` is the first such replacement (an in-process path fence over the shared sandbox mode; see [the cross-family fs sandbox Agent Note](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md)). The policy (`fs-policy/`) is a plugin that participates only through the `fs/*` event gate, not a service the tool injects — so dropping it gracefully loses the policy and leaves the unconstrained bare provider rather than breaking the tool. A deployment that loads `tool-fs/` is expected to also load it. The mode fence and the read-before-edit gate are orthogonal and compose. Discovery (`tool-fs-search/`) deliberately does NOT extend the provider seam: search is a process-backed `rg` workflow on the bash executor, so filesystem backends stay free of a universal search contract; its tools register only when that executor can find `rg`, and its results are follow-up-readable when the bash workdir and the `read` root are the same workspace (the co-located deployment its README documents). ## No timeouts on file IO diff --git a/packages/fs/fs-sandbox/README.md b/packages/fs/fs-sandbox/README.md new file mode 100644 index 0000000000..685ab838c8 --- /dev/null +++ b/packages/fs/fs-sandbox/README.md @@ -0,0 +1,33 @@ +# dsh-fs-sandbox — the sandbox-enforcing filesystem backend + +`SandboxedFileSystem` extends [`LocalFileSystem`](../fs-local/README.md) and registers as `ctx.fs`. It inherits every text-storage mechanic verbatim (resolve, stat, read/stream, list, the atomic write, the read-match-write edit critical section) and adds only a per-call MODE fence on `writeText`/`editText`. Reads always pass through — every mode permits reading. + +Loading it INSTEAD OF `dsh-fs-local`, together with a [`ctx.sandboxPolicy`](../../sandbox/sandbox-policy/README.md), is the whole swap; the model-facing tools (`dsh-tool-fs`) are untouched. Injects `sandboxPolicy` for the default mode and the `workspace-write` boundary root — the SAME policy home bash reads, so the two families never confine to different roots. + +## The fence + +The per-call mode is the tool-stamped effective mode (session override or escalation grant), falling back to the deployment default: + +- `read-only` — denies every mutation with the structured `FS_SANDBOX_DENIED`. +- `workspace-write` — allows a mutation only when the target canonicalizes under a writable root: the workspace root plus the platform temp areas (`/tmp`, `os.tmpdir()`), the SAME set the Seatbelt profile grants, derived from the one [`writableRoots`](../../sandbox/README.md) function so the fs fence and the bash runner cannot drift. The target is re-canonicalized immediately before delegating, so an ancestor symlink swapped since the tool resolved it is caught. +- `danger-full-access` — delegates unfenced. + +## Threat model: a policy fence, not a kernel boundary + +The fence is a check in TRUSTED code over a MODEL-CONTROLLED path — the operations are the seam's own (open, rename), only the target path is untrusted, so canonicalize-then-contain is the complete answer to this surface. This mirrors the `code-runtime` stance: containment, not a security boundary. Kernel-grade isolation of untrusted CODE stays `ctx.bash`'s job ([`dsh-bash-sandbox`](../../bash/bash-sandbox/README.md)). The residual TOCTOU (an ancestor symlink swapped between the containment re-check and the syscall) is narrowed by re-canonicalizing immediately before the write and is accepted for this threat model; a kernel-tight boundary needs `openat2`-class primitives not worth their portability cost here. + +A denial is a structured `FsError` (`FS_SANDBOX_DENIED`, carrying the effective mode) — no stderr text inference (unlike bash's kernel denials), because an in-process fence knows exactly what it refused. The model-facing `[sandbox: file access denied under mode]` marker and the one-approved-wider retry live in the tool layer (`dsh-tool-fs`), exactly as bash's do. See [the cross-family fs sandbox RFC](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md). + +## Model Experience + +Indirectly, through `dsh-tool-fs`, which renders this backend's `FS_SANDBOX_DENIED` refusals as the `[sandbox: file access denied under mode]` marker plus the same-turn escalation hint. + +#### KV Cache effect + +No direct invalidation; the named consumer owns any request-prefix changes. + +## Known Limitations and Deferred Work + +- **A policy fence, not a kernel boundary** — the check is trusted code over a model-controlled path, so the residual resolve-to-syscall TOCTOU is narrowed (by the in-place re-canonicalization) but not eliminated; adversarial host processes are out of scope. Kernel-grade isolation of untrusted code stays `ctx.bash`'s. +- **Fence-vs-runner parity is derived, not asserted** — the writable set comes from `writableRoots`, shared with the Seatbelt profile and pinned by a parity test; a runner profile that changed its writable set without that function would drift. +- **Requires `ctx.sandboxPolicy`** — the backend reads the default mode and workspace root from it and does not confine without it composed. diff --git a/packages/fs/fs-sandbox/package.json b/packages/fs/fs-sandbox/package.json new file mode 100644 index 0000000000..e0fc7656ef --- /dev/null +++ b/packages/fs/fs-sandbox/package.json @@ -0,0 +1,38 @@ +{ + "name": "@deepseek-ai/dsh-fs-sandbox", + "description": "Sandbox-enforcing implementation of the DeepSeek Harness filesystem seam: fences write/edit by the per-call sandbox mode (read-only denies mutation, workspace-write contains it to the workspace + temp roots) while reads pass through", + "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" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", + "src" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "@deepseek-ai/dsh-fs": "^0.0.1", + "@deepseek-ai/dsh-fs-local": "^0.0.1", + "@deepseek-ai/dsh-sandbox": "^0.0.1", + "@deepseek-ai/dsh-sandbox-policy": "^0.0.1", + "cordis": "^4.0.0-rc.7" + }, + "devDependencies": { + "@deepseek-ai/dsh-fs": "workspace:^", + "@deepseek-ai/dsh-fs-local": "workspace:^", + "@deepseek-ai/dsh-sandbox": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", + "cordis": "^4.0.0-rc.7" + } +} diff --git a/packages/fs/fs-sandbox/src/index.ts b/packages/fs/fs-sandbox/src/index.ts new file mode 100644 index 0000000000..314778968e --- /dev/null +++ b/packages/fs/fs-sandbox/src/index.ts @@ -0,0 +1,157 @@ +/** + * `SandboxedFileSystem`: the sandbox-enforcing implementation of the + * `@deepseek-ai/dsh-fs` provider seam. It extends `LocalFileSystem` so all + * text-storage mechanics — resolve, stat, read/stream, list, the atomic + * write and the read-match-write edit critical section — are the local + * implementation's, verbatim; this package adds only the per-call MODE fence + * on the two mutations. Reads pass through untouched: every mode permits + * reading. + * + * The fence is a policy check in TRUSTED code over a MODEL-CONTROLLED path, + * NOT a kernel boundary — the operations are the seam's own (open, rename), + * and only the target path is untrusted, so canonicalize-then-contain is the + * complete answer to this surface. Kernel-grade isolation of untrusted CODE + * stays `ctx.bash`'s job (`@deepseek-ai/dsh-bash-sandbox`). This mirrors the + * `code-runtime` stance: containment, not a security boundary. The residual + * TOCTOU (an ancestor symlink swapped between the containment re-check and the + * syscall) is narrowed by re-canonicalizing immediately before delegating and + * is accepted for this threat model. + * + * Per-call mode: `read-only` denies every mutation; `workspace-write` allows a + * mutation only when the target canonicalizes under the workspace root or a + * platform temp area (the SAME writable-root set the Seatbelt profile grants, + * derived from the one `writableRoots` function so bash and fs cannot drift); + * `danger-full-access` delegates unfenced. A denial throws the structured + * `FS_SANDBOX_DENIED` — no text inference is needed (unlike bash's kernel + * stderr), because an in-process fence knows exactly what it refused. The + * escalation retry lives in the tool layer (`@deepseek-ai/dsh-tool-fs`), + * exactly as bash's does. + * + * @module @deepseek-ai/dsh-fs-sandbox + */ + +import { sep } from 'node:path' +import { Context } from 'cordis' +import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local' +import type { Config as LocalConfig } from '@deepseek-ai/dsh-fs-local' +import { FsError } from '@deepseek-ai/dsh-fs' +import type { FsEditOutcome, FsEditRequest, FsTarget, FsVersion, FsWriteIntent, FsWriteOutcome } from '@deepseek-ai/dsh-fs' +import { writableRoots } from '@deepseek-ai/dsh-sandbox' +import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' +import type {} from '@deepseek-ai/dsh-sandbox-policy' + +/** + * Plugin config: the local backend's knobs, verbatim (only `cwd`, the resolve + * base for relative paths). The sandbox default (mode + `workspace-write` + * boundary root) is NOT here — it lives on `ctx.sandboxPolicy`, the one home + * both enforcing families share. + */ +export type Config = LocalConfig + +/** Whether `path` is `root` itself or lies beneath it (both already canonical). */ +function isUnder(path: string, root: string): boolean { + if (path === root) return true + const prefix = root.endsWith(sep) ? root : root + sep + return path.startsWith(prefix) +} + +/** + * Sandbox-enforcing filesystem backend. Registers as `ctx.fs` (loading it + * INSTEAD OF `dsh-fs-local`, together with a `ctx.sandboxPolicy`, is the whole + * swap — the model-facing tools are untouched). Its configured default mode is + * the fallback exposed by {@link sandboxMode}; `dsh-tool-fs` folds a session's + * `sandbox/mode` override and stamps the effective mode onto each mutation, + * while an approved escalation may stamp a strictly wider mode for one call. + */ +export class SandboxedFileSystem extends LocalFileSystem { + static inject = ['sandboxPolicy'] + + private readonly defaultMode: SandboxMode + /** + * The canonical roots a `workspace-write` mutation may land under, computed + * once (the workspace root and platform temp areas are fixed for the + * provider's lifetime): the same set {@link writableRoots} gives every + * enforcement dialect, so the fs fence and the bash runner agree. + */ + private readonly writableRoots: string[] + + constructor(ctx: Context, config: Config) { + super(ctx, config) + this.defaultMode = ctx.sandboxPolicy.defaultMode + this.writableRoots = writableRoots({ mode: 'workspace-write', workspaceRoot: ctx.sandboxPolicy.workspaceRoot }) + } + + /** The deployment default mode — the capability fact the tool layer reads to advertise escalation. */ + override get sandboxMode(): SandboxMode { + return this.defaultMode + } + + /** + * Fence the write by the per-call mode, then delegate to the inherited + * atomic write. See {@link checkedTarget}. + * @param target - the resolved target to write. + * @param content - the full new file content. + * @param expected - the write intent guarding the write; omit for unconditional. + * @param signal - aborts before the atomic rename takes effect. + * @param sandboxMode - the per-call mode; omit to use the deployment default. + * @returns the write outcome from the inherited backend. + */ + override async writeText( + target: FsTarget, + content: string, + expected?: FsWriteIntent, + signal?: AbortSignal, + sandboxMode?: SandboxMode, + ): Promise { + return super.writeText(await this.checkedTarget(target, sandboxMode), content, expected, signal) + } + + /** + * Fence the edit by the per-call mode, then delegate to the inherited + * atomic edit. See {@link checkedTarget}. + * @param target - the resolved target to edit. + * @param edit - the literal search/replace request. + * @param expected - the version guard; omit for an unconditional edit. + * @param signal - aborts before the atomic rename takes effect. + * @param sandboxMode - the per-call mode; omit to use the deployment default. + * @returns the edit outcome from the inherited backend. + */ + override async editText( + target: FsTarget, + edit: FsEditRequest, + expected?: { version: FsVersion }, + signal?: AbortSignal, + sandboxMode?: SandboxMode, + ): Promise { + return super.editText(await this.checkedTarget(target, sandboxMode), edit, expected, signal) + } + + /** + * Enforce the per-call mode against `target` and return the EXACT target the + * mutation must use, so the checked identity is the mutated one (no + * check-here-write-there TOCTOU). `read-only` denies; `workspace-write` + * re-canonicalizes NOW (`resolve` realpaths the deepest existing ancestor, + * reflecting a concurrently swapped symlink), requires containment under a + * writable root, and returns THAT fresh target; `danger-full-access` returns + * the caller's target unfenced. Throws the structured `FS_SANDBOX_DENIED` on + * refusal — the tool layer maps it to the model-facing `[sandbox: …]` marker + * and the escalation hint. + */ + private async checkedTarget(target: FsTarget, sandboxMode?: SandboxMode): Promise { + const mode = sandboxMode ?? this.defaultMode + if (mode === 'danger-full-access') return target + if (mode === 'read-only') { + throw new FsError(`cannot write "${target.displayPath}": file access denied under read-only mode`, 'FS_SANDBOX_DENIED') + } + // workspace-write: containment on the FRESH canonical path (catches a + // symlink ancestor swapped since the tool resolved this target), and the + // mutation delegates with THIS fresh target — never the stale one. + const fresh = await this.resolve(target.displayPath) + if (!this.writableRoots.some(root => isUnder(fresh.targetKey, root))) { + throw new FsError(`cannot write "${target.displayPath}": file access denied under workspace-write mode`, 'FS_SANDBOX_DENIED') + } + return fresh + } +} + +export default SandboxedFileSystem diff --git a/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts b/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts new file mode 100644 index 0000000000..12f0abb0df --- /dev/null +++ b/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts @@ -0,0 +1,237 @@ +/** + * Tests for the sandbox-enforcing filesystem backend: the per-call mode fence + * on write/edit (read-only denies, workspace-write contains, danger-full-access + * passes through), reads always passing through, the capability fact, and the + * containment matrix — `..` traversal, absolute paths outside, and symlink + * escapes (a symlinked directory inside the workspace pointing out, and a new + * file created under one). The fence is exercised on a real filesystem: a + * denied write leaves no file on disk. + */ + +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises' +import { existsSync } from 'node:fs' +import { homedir, tmpdir } from 'node:os' +import { join } from 'node:path' +import { Context } from 'cordis' +import { FsError, FsTargetKey } from '@deepseek-ai/dsh-fs' +import type { FsTarget } from '@deepseek-ai/dsh-fs' +import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy' +import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' +import { SandboxedFileSystem } from '@deepseek-ai/dsh-fs-sandbox' + +let base: string +let workspace: string +let outside: string +let ctx: Context +let fs: SandboxedFileSystem +let fiber: Awaited> + +async function boot(mode: SandboxMode): Promise { + ctx = new Context() + await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: workspace }) + fiber = await ctx.plugin(SandboxedFileSystem, { cwd: workspace }) + fs = ctx.fs as SandboxedFileSystem +} + +beforeEach(async () => { + // Base under HOME, deliberately NOT tmpdir: `workspace-write` grants /tmp and + // os.tmpdir() (parity with the bash runner), so an "outside" dir under tmpdir + // would be legitimately writable. Sibling dirs under HOME are outside every + // grant, so containment failures are real denials. (The bwrap e2e roots its + // workspaces under HOME for the same reason.) + base = await mkdtemp(join(homedir(), '.dsh-fssbx-')) + workspace = join(base, 'ws') + outside = join(base, 'out') + await mkdir(workspace) + await mkdir(outside) +}) +afterEach(async () => { + await fiber?.dispose() + await rm(base, { recursive: true, force: true }) +}) + +/** Resolve a path through the backend and return its target. */ +function target(path: string): Promise { + return fs.resolve(path) +} + +describe('the capability fact', () => { + it('reports the deployment default mode (what the tool layer advertises against)', async () => { + await boot('workspace-write') + expect(fs.sandboxMode).toBe('workspace-write') + }) +}) + +describe('read-only', () => { + beforeEach(() => boot('read-only')) + + it('denies write, leaving no file on disk', async () => { + const path = join(workspace, 'denied.txt') + await expect(fs.writeText(await target(path), 'x')).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' }) + expect(existsSync(path)).toBe(false) + }) + + it('denies edit of an existing file (the content is unchanged)', async () => { + const path = join(workspace, 'file.txt') + await writeFile(path, 'original') + await expect(fs.editText(await target(path), { oldString: 'original', newString: 'changed', replaceAll: false })) + .rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' }) + expect(await readFile(path, 'utf8')).toBe('original') + }) + + it('allows reads (every mode permits reading)', async () => { + const path = join(workspace, 'readable.txt') + await writeFile(path, 'hello') + expect(await fs.readText(await target(path))).toBe('hello') + }) +}) + +describe('workspace-write containment', () => { + beforeEach(() => boot('workspace-write')) + + it('a write under the workspace lands', async () => { + const path = join(workspace, 'nested', 'ok.txt') + const outcome = await fs.writeText(await target(path), 'inside') + expect(outcome.operation).toBe('create') + expect(await readFile(path, 'utf8')).toBe('inside') + }) + + it('a write to the platform temp area lands (parity with the bash runner grant)', async () => { + const path = join(await mkdtemp(join(tmpdir(), 'dsh-fssbx-tmp-')), 'temp.txt') + await fs.writeText(await target(path), 'temp') + expect(await readFile(path, 'utf8')).toBe('temp') + }) + + it('an absolute path outside the workspace is denied, no file created', async () => { + const path = join(outside, 'escape.txt') + await expect(fs.writeText(await target(path), 'x')).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' }) + expect(existsSync(path)).toBe(false) + }) + + it('a `..` traversal out of the workspace is denied', async () => { + const path = join(workspace, '..', 'sibling-escape.txt') + await expect(fs.writeText(await target(path), 'x')).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' }) + expect(existsSync(join(workspace, '..', 'sibling-escape.txt'))).toBe(false) + }) + + it('a symlinked directory inside the workspace pointing OUT is denied (canonicalized before containment)', async () => { + // workspace/link -> outside ; writing workspace/link/f.txt would land in outside/f.txt. + await symlink(outside, join(workspace, 'link')) + const path = join(workspace, 'link', 'f.txt') + await expect(fs.writeText(await target(path), 'x')).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' }) + expect(existsSync(join(outside, 'f.txt'))).toBe(false) + }) + + it('a NEW file created under a symlinked-out directory is denied (deepest-ancestor realpath)', async () => { + await symlink(outside, join(workspace, 'link')) + const path = join(workspace, 'link', 'newdir', 'deep.txt') + await expect(fs.writeText(await target(path), 'x')).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' }) + expect(existsSync(join(outside, 'newdir'))).toBe(false) + }) + + it('an edit outside the workspace is denied; the original is untouched', async () => { + const path = join(outside, 'file.txt') + await writeFile(path, 'original') + await expect(fs.editText(await target(path), { oldString: 'original', newString: 'x', replaceAll: false })) + .rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' }) + expect(await readFile(path, 'utf8')).toBe('original') + }) + + it('an edit inside the workspace lands', async () => { + const path = join(workspace, 'edit.txt') + await writeFile(path, 'original') + const outcome = await fs.editText(await target(path), { oldString: 'original', newString: 'changed', replaceAll: false }) + expect(outcome.after).toBe('changed') + expect(await readFile(path, 'utf8')).toBe('changed') + }) + + it('mutates the freshly checked identity, not a stale outside targetKey (TOCTOU direction)', async () => { + // A target whose displayPath is inside the workspace but whose targetKey is + // a STALE outside path — as if an ancestor symlink pointed out at the tool's + // resolve() and was swapped in before the write. The fence re-resolves + // displayPath (now inside) AND delegates with that fresh target, so the byte + // lands inside and the stale outside path is never written. + const insidePath = join(workspace, 'landed.txt') + const staleTarget: FsTarget = { displayPath: insidePath, targetKey: FsTargetKey(join(outside, 'escaped.txt')) } + await fs.writeText(staleTarget, 'inside') + expect(await readFile(insidePath, 'utf8')).toBe('inside') + expect(existsSync(join(outside, 'escaped.txt'))).toBe(false) + }) + + it('the workspace root itself passes the fence (path equal to a writable root), failing only on file type', async () => { + // isUnder's path-equals-root branch: the fence allows the root, and the + // write then fails because the root is a directory, not a regular file. + await expect(fs.writeText(await target(workspace), 'x')).rejects.toMatchObject({ code: 'FS_NOT_REGULAR_FILE' }) + }) +}) + +describe('workspace-write with the filesystem root as the workspace (a root ending in the path separator)', () => { + it('grants writes anywhere: containment against `/` allows any absolute path', async () => { + // A degenerate but valid config — workspaceRoot '/'. It exercises isUnder's + // separator-suffixed-root branch: `/` already ends in the separator, so the + // prefix stays `/` and every absolute path is contained. + const rootCtx = new Context() + await rootCtx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: '/' }) + const rootFiber = await rootCtx.plugin(SandboxedFileSystem, { cwd: workspace }) + const rootFs = rootCtx.fs as SandboxedFileSystem + try { + const path = join(base, 'anywhere.txt') // under HOME, outside /tmp — allowed only via the `/` root + await rootFs.writeText(await rootFs.resolve(path), 'anywhere') + expect(await readFile(path, 'utf8')).toBe('anywhere') + } finally { + await rootFiber.dispose() + } + }) +}) + +describe('danger-full-access', () => { + beforeEach(() => boot('danger-full-access')) + + it('writes anywhere, unfenced', async () => { + const path = join(outside, 'free.txt') + await fs.writeText(await target(path), 'free') + expect(await readFile(path, 'utf8')).toBe('free') + }) +}) + +describe('the per-call mode override (escalation)', () => { + it('a workspace-write stamp on a read-only default lets a contained write land for that call only', async () => { + await boot('read-only') + const path = join(workspace, 'escalated.txt') + // Default read-only would deny; the per-call workspace-write stamp allows it (contained). + await fs.writeText(await target(path), 'granted', undefined, undefined, 'workspace-write') + expect(await readFile(path, 'utf8')).toBe('granted') + // A neighboring plain call still runs under the read-only default. + await expect(fs.writeText(await target(join(workspace, 'plain.txt')), 'x')) + .rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' }) + }) + + it('a danger-full-access stamp bypasses the fence for that call', async () => { + await boot('read-only') + const path = join(outside, 'granted-full.txt') + await fs.writeText(await target(path), 'full', undefined, undefined, 'danger-full-access') + expect(await readFile(path, 'utf8')).toBe('full') + }) +}) + +describe('registration and HMR safety', () => { + it('registers as ctx.fs and unregisters cleanly from a child fiber', async () => { + await boot('workspace-write') + expect(ctx.fs).toBeInstanceOf(SandboxedFileSystem) + await fiber.dispose() + expect(ctx.get('fs')).toBeUndefined() + // Re-mount below the disposed one to prove no lingering registration. + fiber = await ctx.plugin(SandboxedFileSystem, { cwd: workspace }) + expect(ctx.fs).toBeInstanceOf(SandboxedFileSystem) + }) +}) + +describe('FsError identity', () => { + it('the denial is a structured FsError distinct from a host permission error', async () => { + await boot('read-only') + const error = await fs.writeText(await target(join(workspace, 'x.txt')), 'x').catch((e: unknown) => e) + expect(error).toBeInstanceOf(FsError) + expect((error as FsError).code).toBe('FS_SANDBOX_DENIED') + }) +}) diff --git a/packages/fs/fs-sandbox/tsconfig.json b/packages/fs/fs-sandbox/tsconfig.json new file mode 100644 index 0000000000..c9e2f629d5 --- /dev/null +++ b/packages/fs/fs-sandbox/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../fs" + }, + { + "path": "../fs-local" + }, + { + "path": "../../sandbox/sandbox" + }, + { + "path": "../../sandbox/sandbox-policy" + } + ] +} diff --git a/packages/fs/fs/package.json b/packages/fs/fs/package.json index f1efde152a..a88e89b72f 100644 --- a/packages/fs/fs/package.json +++ b/packages/fs/fs/package.json @@ -24,11 +24,13 @@ "peerDependencies": { "@deepseek-ai/dsh-brand": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", + "@deepseek-ai/dsh-sandbox": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-sandbox": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/fs/fs/src/index.ts b/packages/fs/fs/src/index.ts index 8466962f5a..0279273d40 100644 --- a/packages/fs/fs/src/index.ts +++ b/packages/fs/fs/src/index.ts @@ -7,6 +7,7 @@ */ import { Context, Service } from 'cordis' +import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' import type { FsDirEntry, FsEditOutcome, @@ -82,6 +83,23 @@ export abstract class FileSystem extends Service { super(ctx, 'fs') } + /** + /** + * The sandbox mode this backend enforces on mutations BY DEFAULT, or + * `undefined` when it does not confine at all — the capability fact the tool + * layer reads to advertise the escalation fields honestly (mirrors + * `BashExecutor.sandboxMode`). The base class and the bare local backend + * report `undefined`; a sandboxing backend (`@deepseek-ai/dsh-fs-sandbox`) + * overrides it with the deployment default. A session override may make the + * effective mode narrower or wider, so strict escalation widening is checked + * per call rather than encoded in this default-relative fact. + * @returns the configured default mode of a sandboxing backend; `undefined` + * for a backend that never confines. + */ + get sandboxMode(): SandboxMode | undefined { + return undefined + } + /** * Resolve a model/plugin-supplied path into a stable {@link FsTarget}. May perform I/O (a * remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence @@ -152,9 +170,18 @@ export abstract class FileSystem extends Service { * @param content - the full new file content. * @param expected - the write intent guarding the write; omit for unconditional. * @param signal - aborts before the atomic rename takes effect. + * @param sandboxMode - the per-call sandbox mode this write runs under; a + * sandboxing backend fences the write by it, the bare backend ignores it. + * Omit to leave the backend its own default. * @returns the outcome, including the version the write produced. */ - abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal): Promise + abstract writeText( + target: FsTarget, + content: string, + expected?: FsWriteIntent, + signal?: AbortSignal, + sandboxMode?: SandboxMode, + ): Promise /** * Atomically edit literal text. When supplied, the version guard is checked @@ -164,9 +191,18 @@ export abstract class FileSystem extends Service { * @param edit - the literal search/replace request. * @param expected - the version guard; omit for an unconditional edit. * @param signal - aborts before the atomic rename takes effect. + * @param sandboxMode - the per-call sandbox mode this edit runs under; a + * sandboxing backend fences the edit by it, the bare backend ignores it. + * Omit to leave the backend its own default. * @returns the outcome, including the version the edit produced. */ - abstract editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal): Promise + abstract editText( + target: FsTarget, + edit: FsEditRequest, + expected?: { version: FsVersion }, + signal?: AbortSignal, + sandboxMode?: SandboxMode, + ): Promise } export default FileSystem diff --git a/packages/fs/fs/src/types.ts b/packages/fs/fs/src/types.ts index c76e88da4b..f5753f09bd 100644 --- a/packages/fs/fs/src/types.ts +++ b/packages/fs/fs/src/types.ts @@ -168,6 +168,7 @@ export type FsErrorCode = | 'FS_NOT_TEXT' | 'FS_NOT_REGULAR_FILE' | 'FS_PERMISSION_DENIED' + | 'FS_SANDBOX_DENIED' | 'FS_IO_ERROR' | 'FS_STALE_VERSION' | 'FS_NOT_OBSERVED' diff --git a/packages/fs/fs/tsconfig.json b/packages/fs/fs/tsconfig.json index a352aea65a..eb981277c7 100644 --- a/packages/fs/fs/tsconfig.json +++ b/packages/fs/fs/tsconfig.json @@ -9,6 +9,7 @@ { "path": "../../../vendor/cosmokit" }, { "path": "../../../vendor/cordis" }, { "path": "../../util/brand" }, - { "path": "../../llm/llm" } + { "path": "../../llm/llm" }, + { "path": "../../sandbox/sandbox" } ] } diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index f9ef3136bf..2b8d0871ad 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -28,9 +28,12 @@ "peerDependencies": { "@deepseek-ai/dsh-fs": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", + "@deepseek-ai/dsh-sandbox": "^0.0.1", + "@deepseek-ai/dsh-sandbox-policy": "^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-approval": "^0.0.1", "cordis": "^4.0.0-rc.7" }, "devDependencies": { @@ -42,9 +45,12 @@ "@deepseek-ai/dsh-fs-local": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", + "@deepseek-ai/dsh-sandbox": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", + "@deepseek-ai/dsh-user-approval": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/fs/tool-fs/src/edit.ts b/packages/fs/tool-fs/src/edit.ts index bc6f7a8fb8..46655f80b7 100644 --- a/packages/fs/tool-fs/src/edit.ts +++ b/packages/fs/tool-fs/src/edit.ts @@ -13,6 +13,7 @@ import type {} from '@deepseek-ai/dsh-fs' import type {} from '@deepseek-ai/dsh-system-prompt' import { computeHunkDiffs, diffsFromMeta, type FsDiffMeta } from './diff.ts' import { sessionResolveOptions } from './session-cwd.ts' +import type { FsSandboxSurface } from './sandbox.ts' /** Validated `edit` arguments after defaulting. */ interface EditInput { @@ -22,6 +23,20 @@ interface EditInput { replaceAll: boolean } +/** + * The `edit` tool's validated argument shape: the base parameters plus the two + * escalation fields, advertised only under a confining `ctx.fs` (absent from + * the schema otherwise, so the validator rejects them before `execute`). + */ +interface EditToolArgs { + file_path: string + old_string: string + new_string: string + replace_all?: boolean + sandbox_permissions?: string + justification?: string +} + /** * Validate value constraints the schema DSL can't express: a non-blank * `file_path`, a non-empty `old_string`, and `old_string !== new_string` @@ -56,8 +71,9 @@ export function formatEditOutput(displayPath: string, replaceAll: boolean): stri /** * Register the `edit` tool and its system-prompt guidance. * @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service. + * @param sandbox - the shared sandbox-escalation surface (advertisement, mode stamping, denial mapping). */ -export function applyEditTool(ctx: Context): void { +export function applyEditTool(ctx: Context, sandbox: FsSandboxSurface): void { ctx.systemPrompt.section({ name: 'tool:edit', order: 102, @@ -72,20 +88,31 @@ export function applyEditTool(ctx: Context): void { old_string: { type: 'string', required: true, description: 'Literal text to replace. Must match exactly.' }, new_string: { type: 'string', required: true, 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.escalationModes.length > 0 ? sandbox.schemaFields() : {}, }, - async execute(args, exec): Promise<{ content: ContentBlock[]; meta?: FsDiffMeta }> { + async execute(args: EditToolArgs, exec): Promise<{ content: ContentBlock[]; meta?: FsDiffMeta }> { const input = parseEditArgs(args) + // Resolve the per-call sandbox mode (escalation grant > session override + // > backend default) BEFORE anything executes. + const sandboxMode = await sandbox.stampMode('edit', args, exec) const target = await ctx.fs.resolve(input.filePath, sessionResolveOptions(exec)) // Single-slot decision: the policy plugin returns { version: vObserved } or // throws FS_NOT_OBSERVED; the bare default is undefined (unconditional edit). // No stat — the bare default never manufactures a version basis. const intent = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined) - const outcome = await ctx.fs.editText( - target, - { oldString: input.oldString, newString: input.newString, replaceAll: input.replaceAll }, - intent, - exec.signal, - ) + let outcome + try { + outcome = await ctx.fs.editText( + target, + { oldString: input.oldString, newString: input.newString, replaceAll: input.replaceAll }, + intent, + exec.signal, + sandboxMode, + ) + } catch (error: unknown) { + // A sandbox denial becomes the shared [sandbox: …] marker; any other error passes through. + throw sandbox.mapError(error, sandboxMode) + } // Record the observed version (a no-op when no policy plugin listens). ctx.emit('fs/observed', target, outcome.version, exec) // An edit necessarily changes content, so result metadata carries at least one applied hunk. diff --git a/packages/fs/tool-fs/src/index.ts b/packages/fs/tool-fs/src/index.ts index d29e4ae733..c7c217b609 100644 --- a/packages/fs/tool-fs/src/index.ts +++ b/packages/fs/tool-fs/src/index.ts @@ -7,10 +7,12 @@ import type { Context } from 'cordis' import z from 'schemastery' +import type {} from '@deepseek-ai/dsh-user-approval' import { applyReadTool, READ_LIMIT, STREAM_MIN_SIZE } from './read.ts' import { applyWriteTool } from './write.ts' import { applyEditTool } from './edit.ts' import { READ_MAX_BYTES, READ_MAX_LINE_LENGTH } from './read-render.ts' +import { FsSandboxSurface } from './sandbox.ts' /** Cordis plugin name used by loader diagnostics. */ export const name = 'tool-fs' @@ -61,6 +63,10 @@ export function apply(ctx: Context, config: Config): void { maxBytes: resolved.readMaxBytes, streamMinSize: resolved.readStreamMinSize, }) - applyWriteTool(ctx) - applyEditTool(ctx) + // One escalation surface shared by both mutating tools: advertisement gating, + // per-call mode stamping, and denial-marker mapping, all keyed off whether + // the mounted ctx.fs confines (ctx.fs.sandboxMode). + const sandbox = new FsSandboxSurface(ctx) + applyWriteTool(ctx, sandbox) + applyEditTool(ctx, sandbox) } diff --git a/packages/fs/tool-fs/src/sandbox.ts b/packages/fs/tool-fs/src/sandbox.ts new file mode 100644 index 0000000000..f58f9d6b13 --- /dev/null +++ b/packages/fs/tool-fs/src/sandbox.ts @@ -0,0 +1,135 @@ +/** + * The sandbox-escalation surface shared by the `write` and `edit` tools: the + * per-call mode stamp, the advertised escalation fields, and the denial-marker + * mapping — all delegating the vocabulary and the fail-closed approval + * sequence to `@deepseek-ai/dsh-sandbox` (the same pieces `@deepseek-ai/dsh-tool-bash` + * uses), so bash and fs escalate identically. Built ONCE per plugin from + * `ctx.fs.sandboxMode` (the capability fact — is a confining backend mounted?) + * and shared by both mutating tools. + * + * @module @deepseek-ai/dsh-tool-fs/sandbox + */ + +import type { Context } from 'cordis' +import type { ToolExecution } from '@deepseek-ai/dsh-tools' +import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' +import { ESCALATION_TARGETS, approveEscalation, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs } from '@deepseek-ai/dsh-sandbox' +import { effectiveSandboxMode } from '@deepseek-ai/dsh-sandbox-policy' +import { FsError } from '@deepseek-ai/dsh-fs' + +/** The two escalation arguments a mutating tool may carry (advertised only under a confining backend). */ +export interface FsEscalationArgs { + sandbox_permissions?: string + justification?: string +} + +/** The schema fields for the escalation arguments, spread into a tool's `parameters` when a confining backend is mounted. */ +export interface EscalationSchemaFields { + sandbox_permissions: { type: 'string'; enum: string[]; description: string } + justification: { type: 'string'; description: string } +} + +/** + * The filesystem escalation surface: advertisement gating, per-call mode + * stamping (folding the session's `sandbox/mode` override), the one-approved + * wider retry, and denial-marker mapping. A pure product of `ctx` at plugin + * apply time. + */ +export class FsSandboxSurface { + /** The escalation targets this composition advertises (`[]` when no confining backend is mounted). */ + readonly escalationModes: readonly SandboxMode[] + /** The backend's default mode, or `undefined` when `ctx.fs` does not confine. */ + private readonly defaultMode: SandboxMode | undefined + + constructor(private readonly ctx: Context) { + this.defaultMode = ctx.fs.sandboxMode + this.escalationModes = this.defaultMode === undefined ? [] : ESCALATION_TARGETS + } + + /** + * The escalation schema fields for a mutating tool's `parameters`. Call it + * only under a confining backend (guard on {@link escalationModes}); the + * enum pins the closed target vocabulary, the strict-wider check happens per + * call at execution. + * @returns the two escalation parameter specs. + */ + schemaFields(): EscalationSchemaFields { + return { + sandbox_permissions: { + type: 'string', + enum: [...this.escalationModes], + 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.', + }, + justification: { + type: 'string', + description: 'Required with sandbox_permissions: one sentence for the user explaining ' + + 'why this exact file operation needs the wider access.', + }, + } + } + + /** + * The session's standing mode override for an ordinary (non-escalating) + * call — the `sandbox/mode` fold of the calling agent's log. Undefined for a + * non-confining backend and for agent-less callers. + */ + private sessionOverride(exec: ToolExecution): SandboxMode | undefined { + if (this.defaultMode === undefined || exec.agent === undefined) return undefined + return effectiveSandboxMode(exec.agent.session.events) + } + + /** + * The mode to STAMP onto this mutation: an approved escalation grant (a + * strictly wider retry resolved through `ctx.approval` before anything + * executes), else the session's standing override, else `undefined` (the + * backend applies its own default). Validates the escalation argument + * pairing first. + * @param toolName - the mutating tool's name, for the approval audit trail. + * @param args - the call's escalation arguments. + * @param exec - the tool-execution context (agent, callId, signal). + * @returns the mode to pass to the mutation, or undefined for the backend default. + */ + async stampMode(toolName: string, args: FsEscalationArgs, exec: ToolExecution): Promise { + validateEscalationArgs(args.sandbox_permissions, args.justification) + if (args.sandbox_permissions === undefined || args.justification === undefined) { + return this.sessionOverride(exec) + } + if (this.escalationModes.length === 0) { + throw new Error('sandbox_permissions is not available in this composition (no sandboxing filesystem to escalate)') + } + const effectiveMode = (this.sessionOverride(exec) ?? this.defaultMode) as SandboxMode + return approveEscalation( + { requestedMode: args.sandbox_permissions, justification: args.justification, effectiveMode, subject: 'operation' }, + { + approver: this.ctx.get('approval'), + agent: exec.agent, + callId: exec.callId, + toolName, + ...exec.signal ? { signal: exec.signal } : {}, + }, + ) + } + + /** + * Map a thrown provider error for the model: a `FS_SANDBOX_DENIED` becomes a + * `FsError` whose text is the shared `[sandbox: …]` denial marker plus the + * same-turn escalation hint, so a policy denial reads identically to bash's + * WHILE keeping the structured `FS_SANDBOX_DENIED` code — `ToolRegistry` + * populates `result.error` only for `HarnessError` instances, so a plain + * `Error` would strip the code retry/observers key off. Any other error + * passes through unchanged. A `FS_SANDBOX_DENIED` only arises under a + * confining backend, which always advertises the escalation fields, so the + * hint always applies here. + * @param error - the error thrown by the mutation. + * @param stampedMode - the mode stamped onto the call (names the mode in the marker). + * @returns the error to throw — the marker `FsError` for a sandbox denial, else the original. + */ + mapError(error: unknown, stampedMode: SandboxMode | undefined): unknown { + if (!(error instanceof FsError) || error.code !== 'FS_SANDBOX_DENIED') return error + // A FS_SANDBOX_DENIED only arises under a confining backend, so defaultMode + // (hence the resolved mode) is defined here. + const mode = (stampedMode ?? this.defaultMode) as SandboxMode + return new FsError(`${sandboxDenialMarker(mode)}\n${escalationHintMarker('operation')}`, 'FS_SANDBOX_DENIED', { cause: error }) + } +} diff --git a/packages/fs/tool-fs/src/write.ts b/packages/fs/tool-fs/src/write.ts index 8ab3ce0f62..3f23e9b5ea 100644 --- a/packages/fs/tool-fs/src/write.ts +++ b/packages/fs/tool-fs/src/write.ts @@ -14,6 +14,7 @@ import type {} from '@deepseek-ai/dsh-fs' import type {} from '@deepseek-ai/dsh-system-prompt' import { computeHunkDiffs, diffsFromMeta, type FsDiffMeta } from './diff.ts' import { sessionResolveOptions } from './session-cwd.ts' +import type { FsSandboxSurface } from './sandbox.ts' /** * Validate value constraints the schema DSL can't express: only a non-blank @@ -41,11 +42,24 @@ ${verb} file ` } +/** + * The `write` tool's validated argument shape: the base parameters plus the + * two escalation fields, advertised only under a confining `ctx.fs` (absent + * from the schema otherwise, so the validator rejects them before `execute`). + */ +interface WriteToolArgs { + file_path: string + content: string + sandbox_permissions?: string + justification?: string +} + /** * Register the `write` tool and its system-prompt guidance. * @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service. + * @param sandbox - the shared sandbox-escalation surface (advertisement, mode stamping, denial mapping). */ -export function applyWriteTool(ctx: Context): void { +export function applyWriteTool(ctx: Context, sandbox: FsSandboxSurface): void { ctx.systemPrompt.section({ name: 'tool:write', order: 101, @@ -58,14 +72,26 @@ export function applyWriteTool(ctx: Context): void { parameters: { file_path: { type: 'string', required: true, description: 'Path to write, resolved by the filesystem backend.' }, content: { type: 'string', required: true, description: 'Full UTF-8 text content to write.' }, + ...sandbox.escalationModes.length > 0 ? sandbox.schemaFields() : {}, }, - async execute(args, exec): Promise<{ content: ContentBlock[]; meta?: FsDiffMeta }> { + async execute(args: WriteToolArgs, exec): Promise<{ content: ContentBlock[]; meta?: FsDiffMeta }> { const input = parseWriteArgs(args) + // Resolve the per-call sandbox mode (escalation grant > session override + // > backend default) BEFORE anything executes; an escalating call + // resolves approval here and throws its distinct text on any non-grant. + const sandboxMode = await sandbox.stampMode('write', args, exec) const target = await ctx.fs.resolve(input.filePath, sessionResolveOptions(exec)) // Single-slot decision: the policy plugin produces createIfAbsent/ // replaceIfVersion; the bare default is undefined (unconditional). No stat. const intent = await ctx.waterfall('fs/write-intent', target, exec, () => undefined) - const outcome = await ctx.fs.writeText(target, input.content, intent, exec.signal) + let outcome: FsWriteOutcome + try { + outcome = await ctx.fs.writeText(target, input.content, intent, exec.signal, sandboxMode) + } catch (error: unknown) { + // A sandbox denial becomes the shared [sandbox: …] marker (the model + // recognizes it from bash); any other error passes through. + throw sandbox.mapError(error, sandboxMode) + } // Record the observed version (a no-op when no policy plugin listens). ctx.emit('fs/observed', target, outcome.version, exec) // Overwrites carry applied hunks. Creates have no prior text, so result presentation uses diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts index f08db928af..4d80061584 100644 --- a/packages/fs/tool-fs/tests/tools.spec.ts +++ b/packages/fs/tool-fs/tests/tools.spec.ts @@ -24,6 +24,8 @@ import * as ToolFs from '@deepseek-ai/dsh-tool-fs' import { STREAM_MIN_SIZE } from '../src/read.ts' import { formatReadOutput } from '../src/read-render.ts' import type { FileReadOutcome } from '../src/read-render.ts' +import ApprovalService from '@deepseek-ai/dsh-user-approval' +import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' /** An in-memory fake provider; a test can arm a rejection on any primitive. */ class FakeFs extends FileSystem { @@ -580,3 +582,163 @@ describe('read caps are plugin config', () => { expect('default' in ToolFs).toBe(false) }) }) + +describe('sandbox escalation surface (write/edit)', () => { + /** A confining fake `ctx.fs`: reports a default mode, records the per-call mode stamped, and can arm a sandbox denial. */ + class SandboxingFakeFs extends FakeFs { + stamped: (SandboxMode | undefined)[] = [] + override get sandboxMode(): SandboxMode { + return 'workspace-write' + } + override async writeText( + target: FsTarget, + content: string, + expected?: FsWriteIntent, + _signal?: AbortSignal, + sandboxMode?: SandboxMode, + ): Promise { + this.stamped.push(sandboxMode) + return super.writeText(target, content, expected) + } + override async editText( + target: FsTarget, + edit: FsEditRequest, + expected?: { version: FsVersion }, + _signal?: AbortSignal, + sandboxMode?: SandboxMode, + ): Promise { + this.stamped.push(sandboxMode) + return super.editText(target, edit, expected) + } + } + + async function setupConfining(opts: { approval?: boolean } = {}) { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(SandboxingFakeFs) + await ctx.plugin(FsPolicy) + if (opts.approval === true) await ctx.plugin(ApprovalService) + await ctx.plugin(ToolFs) + return { ctx, fs: ctx.fs as SandboxingFakeFs } + } + + /** A fake agent whose session records appends (the approval audit surface), mid-turn, carrying the given events for the fold. */ + function escalationAgent(events: Array<{ type: string; data?: Record }> = []): object { + return { + id: 'agent-fs-esc', + session: { + header: { version: 0, id: 'sess-fs-esc', createdAt: 0 }, + events: [{ type: 'turn/start' }, ...events], + append: (type: string, data: Record) => { events.push({ type, data }) }, + }, + } + } + + function fsSchema(ctx: Context, name: 'write' | 'edit') { + const schema = ctx.tools.schemas().find(s => s.name === name) + if (!schema) throw new Error(`${name} tool not registered`) + return schema as unknown as { parameters: { properties: Record } } + } + + it('advertises no escalation fields under a non-confining backend', async () => { + const { ctx } = await setup() + expect(ctx.fs.sandboxMode).toBeUndefined() + for (const name of ['write', 'edit'] as const) { + const props = fsSchema(ctx, name).parameters.properties + expect(props['sandbox_permissions']).toBeUndefined() + expect(props['justification']).toBeUndefined() + } + }) + + it('advertises the closed target vocabulary on write and edit under a confining backend', async () => { + const { ctx } = await setupConfining() + for (const name of ['write', 'edit'] as const) { + const props = fsSchema(ctx, name).parameters.properties + expect(props['sandbox_permissions']?.enum).toEqual(['workspace-write', 'danger-full-access']) + expect(props['justification']).toBeDefined() + } + }) + + it('a plain write stamps nothing (backend default) and no session override folds without one', async () => { + const { ctx, fs } = await setupConfining() + await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent()) + expect(fs.stamped).toEqual([undefined]) + }) + + it('a standing session override folds onto the stamp', async () => { + const { ctx, fs } = await setupConfining() + await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent([{ type: 'sandbox/mode', data: { mode: 'read-only' } }])) + expect(fs.stamped).toEqual(['read-only']) + }) + + it('a denied write maps to the shared marker plus the escalation hint (isError)', async () => { + const { ctx, fs } = await setupConfining() + fs.rejectWith = new FsError('denied', 'FS_SANDBOX_DENIED') + const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent()) + expect(result.isError).toBe(true) + expect(text(result)).toContain('[sandbox: file access denied under workspace-write mode]') + expect(text(result)).toContain('retry this exact operation once with sandbox_permissions') + }) + + it('a non-FS_SANDBOX_DENIED provider error passes through unchanged', async () => { + const { ctx, fs } = await setupConfining() + fs.rejectWith = new FsError('boom', 'FS_IO_ERROR') + const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent()) + expect(result.isError).toBe(true) + expect(text(result)).toContain('boom') + expect(text(result)).not.toContain('[sandbox:') + }) + + it('an approved escalation stamps the granted mode onto that write', async () => { + const { ctx, fs } = await setupConfining({ approval: true }) + ctx.on('approval/request', () => Promise.resolve('allowed-once' as const)) + // Pass a signal so the escalation ask forwards it to the approval request + // (the request rides the tool-execution abort signal). + await ctx.tools.execute({ + callId: CallId('call-fs-esc-grant'), + name: 'write', + arguments: { file_path: 'a.txt', content: 'x', sandbox_permissions: 'danger-full-access', justification: 'the test needs it' }, + agent: escalationAgent() as never, + signal: new AbortController().signal, + }) + expect(fs.stamped).toEqual(['danger-full-access']) + }) + + it('a rejected escalation fails closed with its own text and never mutates', async () => { + const { ctx, fs } = await setupConfining({ approval: true }) + ctx.on('approval/request', () => Promise.resolve('rejected' as const)) + const result = await call(ctx, 'edit', { file_path: 'a.txt', old_string: 'x', new_string: 'y', sandbox_permissions: 'danger-full-access', justification: 'the test needs it' }, escalationAgent()) + expect(result.isError).toBe(true) + expect(text(result)).toContain('the user rejected escalating this operation to "danger-full-access"') + expect(fs.stamped).toEqual([]) + }) + + it('escalation without an approval service fails closed', async () => { + const { ctx } = await setupConfining() + const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'x', sandbox_permissions: 'danger-full-access', justification: 'why' }, escalationAgent()) + expect(result.isError).toBe(true) + expect(text(result)).toContain('no approval service is composed') + }) + + it('escalation with an approval service but no agent fails closed', async () => { + const { ctx } = await setupConfining({ approval: true }) + const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'x', sandbox_permissions: 'danger-full-access', justification: 'why' }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('no agent to route it through') + }) + + it('rejects the escalation argument pairing (one field without the other)', async () => { + const { ctx } = await setupConfining() + const missing = await call(ctx, 'write', { file_path: 'a.txt', content: 'x', sandbox_permissions: 'workspace-write' }, escalationAgent()) + expect(missing.isError).toBe(true) + expect(text(missing)).toContain('sandbox_permissions requires a justification') + }) + + it('sandbox_permissions under a non-confining backend fails closed (unadvertised field still reaches execute)', async () => { + const { ctx } = await setup() + const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'x', sandbox_permissions: 'workspace-write', justification: 'why' }, escalationAgent()) + expect(result.isError).toBe(true) + expect(text(result)).toContain('not available in this composition') + }) +}) diff --git a/packages/fs/tool-fs/tsconfig.json b/packages/fs/tool-fs/tsconfig.json index f0133b1d2b..d2adddae03 100644 --- a/packages/fs/tool-fs/tsconfig.json +++ b/packages/fs/tool-fs/tsconfig.json @@ -13,6 +13,9 @@ { "path": "../../core/tools" }, { "path": "../../core/system-prompt" }, { "path": "../fs" }, - { "path": "../fs-policy" } + { "path": "../fs-policy" }, + { "path": "../../sandbox/sandbox" }, + { "path": "../../sandbox/sandbox-policy" }, + { "path": "../../ui/user-approval" } ] } diff --git a/packages/goal/goal/README.md b/packages/goal/goal/README.md index 175d0d0d79..8446d262f6 100644 --- a/packages/goal/goal/README.md +++ b/packages/goal/goal/README.md @@ -19,7 +19,7 @@ Event-sourced same-session goal state. The service retains one current completio At most one goal is current. Creation produces an active revision-one goal and arms it. A non-complete goal must be edited, transitioned, or cleared; a completed goal may be replaced by a globally fresh id. Edits retain phase, blocker reason, and activation. Pause, completion, blocking, and clear disarm activation. A block records a policy-owned lower-kebab-case code plus a normalized free-form explanation; provider limits, configured budgets, execution errors, and requests for human input all use this one durable phase rather than multiplying lifecycle states. Resume accepts a stopped phase or a disarmed active goal only while the configured round cap has remaining capacity; it clears any former blocker reason. An active armed goal rejects the redundant operation. -Every non-clear mutation appends a complete versioned snapshot through `agent.inject()`; clear appends a revisioned tombstone. The raw `context/message`, its `{ kind: 'goal' }` source, and its metadata must agree exactly. Replay rejects malformed shapes, source/content drift, discontinuous revisions, illegal lifecycle transitions, non-monotonic per-goal timestamps, and non-sequential goal rounds. Mutation timestamps clamp against the preceding goal update when wall time moves backward. +Every non-clear mutation appends a complete versioned snapshot through `agent.inject()`; clear appends a revisioned tombstone. The `context/message` content projected verbatim to the model, its `{ kind: 'goal' }` source, and its metadata must agree exactly. Replay rejects malformed shapes, source/content drift, discontinuous revisions, illegal lifecycle transitions, non-monotonic per-goal timestamps, and non-sequential goal rounds. Mutation timestamps clamp against the preceding goal update when wall time moves backward. Injection may append immediately or wait in an active tool-batch FIFO. The service overlays accepted pending changes in memory and reconciles each exact payload when it enters the log, so consecutive model-tool mutations see their own latest revisions without treating an unlogged cache as durable state. Reentrant append observers see each accepted mutation exactly once, and incremental replay retains its cursor at the first corrupt event. `goal/changed` fires after the append or enqueue succeeds; listener failures are contained. diff --git a/packages/goal/goal/src/fold.ts b/packages/goal/goal/src/fold.ts index 5f80e7fd4c..fe88ebcdba 100644 --- a/packages/goal/goal/src/fold.ts +++ b/packages/goal/goal/src/fold.ts @@ -325,8 +325,7 @@ export function decodeGoalEvent(event: ContextMessageEvent): GoalChangeMeta | un if (source === undefined || source.goalId !== ref.id || source.revision !== ref.revision || source.round !== 0) { throw new Error(`goal change at session event ${event.seq} has mismatched source attribution`) } - if (event.data.envelope !== 'raw' - || JSON.stringify(event.data.content) !== JSON.stringify(renderGoalChange(change))) { + if (JSON.stringify(event.data.content) !== JSON.stringify(renderGoalChange(change))) { throw new Error(`goal change at session event ${event.seq} has mismatched model-visible content`) } return change diff --git a/packages/goal/goal/src/index.ts b/packages/goal/goal/src/index.ts index dc485bcc8a..dedfc385b6 100644 --- a/packages/goal/goal/src/index.ts +++ b/packages/goal/goal/src/index.ts @@ -482,7 +482,6 @@ export class GoalService extends Service { try { agent.inject(renderGoalChange(change), { source: { kind: 'goal', goalId: ref.id, revision: ref.revision, round: 0 }, - envelope: 'raw', meta, }) } catch (error: unknown) { diff --git a/packages/goal/goal/src/render.ts b/packages/goal/goal/src/render.ts index f84a1b51ea..955c9a070c 100644 --- a/packages/goal/goal/src/render.ts +++ b/packages/goal/goal/src/render.ts @@ -6,7 +6,7 @@ import type { GoalChangeMeta } from './types.ts' /** * Render a complete goal snapshot or clear tombstone without hidden prose. * @param change - durable goal change metadata. - * @returns the single raw context block logged for model reconstruction. + * @returns the single context block logged and projected verbatim for model reconstruction. */ export function renderGoalChange(change: GoalChangeMeta): ContentBlock[] { const payload = change.operation === 'clear' diff --git a/packages/goal/goal/tests/goal.e2e.ts b/packages/goal/goal/tests/goal.e2e.ts index 0942248b78..eb6eae665a 100644 --- a/packages/goal/goal/tests/goal.e2e.ts +++ b/packages/goal/goal/tests/goal.e2e.ts @@ -117,7 +117,6 @@ describe('goal domain through a real cordis.yml and stdio process', () => { maxGoalRounds: 7, }, }) - expect(context.data.envelope).toBe('raw') expect(context.data.content).toEqual(renderGoalChange(change)) expect(JSON.stringify(context)).not.toContain('activation') expect(events.filter(event => event.type === 'user/message' diff --git a/packages/goal/goal/tests/goal.spec.ts b/packages/goal/goal/tests/goal.spec.ts index eac96e0374..57b2b5917b 100644 --- a/packages/goal/goal/tests/goal.spec.ts +++ b/packages/goal/goal/tests/goal.spec.ts @@ -38,7 +38,6 @@ function appendInjection(session: Session, content: ContentBlock[], options?: In const context = { content, source, - ...options?.envelope === undefined ? {} : { envelope: options.envelope }, ...options?.meta === undefined ? {} : { meta: options.meta }, } const last = session.events.at(-1) @@ -111,7 +110,7 @@ function appendRound(session: Session, ref: GoalRef, round: number): void { } describe('GoalService creation and replay', () => { - it('applies the configured default and writes one balanced raw context snapshot', async () => { + it('applies the configured default and writes one balanced verbatim context snapshot', async () => { vi.useFakeTimers() vi.setSystemTime(1_700_000_000_000) const { ctx, agent, session } = await harness({ defaultMaxGoalRounds: 17 }) @@ -136,7 +135,6 @@ describe('GoalService creation and replay', () => { const context = session.events[1] expect(context?.type).toBe('context/message') if (context?.type !== 'context/message') throw new Error('expected goal context') - expect(context.data.envelope).toBe('raw') expect(context.data.source).toEqual({ kind: 'goal', goalId: goal.id, revision: 1, round: 0 }) const change = decodeGoalChange(context.data.meta) if (change === undefined) throw new Error('expected decoded goal change') @@ -506,7 +504,7 @@ describe('GoalService mutations', () => { const turn = nextTurn(session) session.append('turn/start', { turn, trigger: { kind: 'injection', source } }) session.append('context/message', { - content: renderGoalChange(change), source, envelope: 'raw', meta: change as never, + content: renderGoalChange(change), source, meta: change as never, }, { surfaceOp: 'append' }) session.append('turn/end', { turn, reason: { kind: 'completed' } }) @@ -537,12 +535,10 @@ describe('GoalService mutations', () => { } appendInjection(session, renderGoalChange(change), { source: { kind: 'goal', goalId: change.goal.id, revision: 1, round: 0 }, - envelope: 'raw', meta: change as never, }) appendInjection(session, [{ type: 'text', text: 'corrupt' }], { source: { kind: 'goal', goalId: change.goal.id, revision: 2, round: 0 }, - envelope: 'raw', meta: { ...change, operation: 'edit', extra: true } as never, }) @@ -574,7 +570,7 @@ describe('goal replay validation', () => { function appendChange( session: Session, change: GoalChangeMeta, - overrides: { content?: ContentBlock[]; source?: MessageSource; envelope?: 'raw' } = {}, + overrides: { content?: ContentBlock[]; source?: MessageSource } = {}, ): void { const source = overrides.source ?? { kind: 'goal', @@ -587,13 +583,12 @@ describe('goal replay validation', () => { session.append('context/message', { content: overrides.content ?? renderGoalChange(change), source, - envelope: overrides.envelope ?? 'raw', meta: change as never, }, { surfaceOp: 'append' }) session.append('turn/end', { turn, reason: { kind: 'completed' } }) } - function oneChange(change: GoalChangeMeta, overrides: { content?: ContentBlock[]; source?: MessageSource; envelope?: 'raw' } = {}) { + function oneChange(change: GoalChangeMeta, overrides: { content?: ContentBlock[]; source?: MessageSource } = {}) { const session = new Session(SessionId(`validation-${Math.random()}`)) appendChange(session, change, overrides) return session.events @@ -783,7 +778,7 @@ describe('goal replay validation', () => { const turn = nextTurn(session) session.append('turn/start', { turn, trigger: { kind: 'injection', source } }) session.append('context/message', { - content: [{ type: 'text', text: 'missing' }], source, envelope: 'raw', + content: [{ type: 'text', text: 'missing' }], source, }, { surfaceOp: 'append' }) session.append('turn/end', { turn, reason: { kind: 'completed' } }) expect(() => foldGoal(session.events)).toThrow('lacks goal change metadata') @@ -822,21 +817,13 @@ describe('goal replay validation', () => { })).toThrow('positive safe integer') }) - it('rejects source, content, and envelope drift from the durable metadata', () => { + it('rejects source and content drift from the durable metadata', () => { const change = snapshotChange() expect(() => foldGoal(oneChange(change, { source: { kind: 'plugin', plugin: 'wrong' } }))).toThrow('mismatched source') expect(() => foldGoal(oneChange(change, { source: { kind: 'goal', goalId: change.goal.id, revision: 1, round: -1 }, }))).toThrow('source is invalid') expect(() => foldGoal(oneChange(change, { content: [{ type: 'text', text: 'wrong' }] }))).toThrow('model-visible content') - const events = oneChange(change) - const context = events.find(event => event.type === 'context/message') - if (context?.type !== 'context/message') throw new Error('expected context') - const altered = structuredClone(events) - const clonedContext = altered.find(event => event.type === 'context/message') - if (clonedContext?.type !== 'context/message') throw new Error('expected cloned context') - delete (clonedContext.data as { envelope?: string }).envelope - expect(() => foldGoal(altered)).toThrow('model-visible content') }) it('folds a clear tombstone after a snapshot', () => { @@ -853,7 +840,7 @@ describe('goal replay validation', () => { const turn = nextTurn(session) session.append('turn/start', { turn, trigger: { kind: 'injection', source } }) session.append('context/message', { - content: renderGoalChange(clear), source, envelope: 'raw', meta: clear as never, + content: renderGoalChange(clear), source, meta: clear as never, }, { surfaceOp: 'append' }) session.append('turn/end', { turn, reason: { kind: 'completed' } }) expect(foldGoal(session.events)).toEqual({ diff --git a/packages/guard/repeat-tool-guard/README.md b/packages/guard/repeat-tool-guard/README.md index 30944c2254..ef5e4e5846 100644 --- a/packages/guard/repeat-tool-guard/README.md +++ b/packages/guard/repeat-tool-guard/README.md @@ -30,7 +30,7 @@ The chain key is `(tool name, canonical arguments)` — canonicalization is a de ## Reminder delivery -Reminders ride the post-execute decision's `additionalContexts` (source `{kind: 'plugin', plugin: 'repeat-tool-guard'}`), never a `content` replacement: the `tool/result` event stays the tool's own output for audit. The loop buffers the context and appends it as a `context/message` after the step's tool results, which the session renders as the tagged synthetic-user envelope — so the reminder is model-visible, source-attributed, and reconstructable from the session log with no new session event. The guard always delegates via `next()` and prepends its reminder to the downstream decision's context array (both variants — a blocked call still gets the nudge); every entry retains its own source, envelope, and metadata. +Reminders ride the post-execute decision's `additionalContexts` (source `{kind: 'plugin', plugin: 'repeat-tool-guard'}`), never a `content` replacement: the `tool/result` event stays the tool's own output for audit. The loop buffers the context and appends it as a `context/message` after the step's tool results, which the session renders as a plain synthetic user message — so the reminder is model-visible, source-attributed, and reconstructable from the session log with no new session event. The guard always delegates via `next()` and prepends its reminder to the downstream decision's context array (both variants — a blocked call still gets the nudge); every entry retains its own source and metadata. ## Testing diff --git a/packages/guard/repeat-tool-guard/src/index.ts b/packages/guard/repeat-tool-guard/src/index.ts index dc9a047a4f..0c630686b4 100644 --- a/packages/guard/repeat-tool-guard/src/index.ts +++ b/packages/guard/repeat-tool-guard/src/index.ts @@ -140,7 +140,7 @@ function validateThresholds(values: number[]): number[] { /** * Prepend the guard's reminder while preserving every downstream context's - * source, envelope, and metadata. + * source and metadata. */ function prependContext(ours: HookContext, theirs: HookContext[] | undefined): HookContext[] { return [ours, ...theirs ?? []] diff --git a/packages/hooks/hooks-claude/tests/coverage-cases.ts b/packages/hooks/hooks-claude/tests/coverage-cases.ts index ec7d18b4c4..870d369784 100644 --- a/packages/hooks/hooks-claude/tests/coverage-cases.ts +++ b/packages/hooks/hooks-claude/tests/coverage-cases.ts @@ -508,7 +508,6 @@ export function defineCoverageCases(group: CoverageGroup): void { additionalContexts: [{ content: [{ type: 'text' as const, text: 'from-downstream' }], source: { kind: 'plugin' as const, plugin: 'policy' }, - envelope: 'raw' as const, meta: { owner: 'policy' }, }], })) @@ -527,7 +526,6 @@ export function defineCoverageCases(group: CoverageGroup): void { { kind: 'plugin', plugin: 'hooks-claude' }, { kind: 'plugin', plugin: 'policy' }, ]) - expect(contexts[1]?.type === 'context/message' && contexts[1].data.envelope).toBe('raw') expect(contexts[1]?.type === 'context/message' && contexts[1].data.meta).toEqual({ owner: 'policy' }) }) @@ -561,7 +559,6 @@ export function defineCoverageCases(group: CoverageGroup): void { additionalContexts: [{ content: [{ type: 'text' as const, text: 'downstream-note' }], source: { kind: 'plugin' as const, plugin: 'policy' }, - envelope: 'raw' as const, meta: { owner: 'policy' }, }], })) @@ -574,7 +571,6 @@ export function defineCoverageCases(group: CoverageGroup): void { { kind: 'plugin', plugin: 'hooks-claude' }, { kind: 'plugin', plugin: 'policy' }, ]) - expect(contexts[1]?.type === 'context/message' && contexts[1].data.envelope).toBe('raw') expect(contexts[1]?.type === 'context/message' && contexts[1].data.meta).toEqual({ owner: 'policy' }) }) diff --git a/packages/hooks/hooks-codex/tests/coverage-cases.ts b/packages/hooks/hooks-codex/tests/coverage-cases.ts index a7f2e1e0ac..e02ea52df1 100644 --- a/packages/hooks/hooks-codex/tests/coverage-cases.ts +++ b/packages/hooks/hooks-codex/tests/coverage-cases.ts @@ -125,7 +125,6 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro additionalContexts: [{ content: [{ type: 'text' as const, text: 'from-downstream' }], source: { kind: 'plugin' as const, plugin: 'policy' }, - envelope: 'raw' as const, meta: { owner: 'policy' }, }], })) @@ -140,7 +139,6 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro { kind: 'plugin', plugin: 'hooks-codex' }, { kind: 'plugin', plugin: 'policy' }, ]) - expect(contexts[1]?.type === 'context/message' && contexts[1].data.envelope).toBe('raw') expect(contexts[1]?.type === 'context/message' && contexts[1].data.meta).toEqual({ owner: 'policy' }) }) }) @@ -171,7 +169,6 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro additionalContexts: [{ content: [{ type: 'text' as const, text: 'downstream-note' }], source: { kind: 'plugin' as const, plugin: 'policy' }, - envelope: 'raw' as const, meta: { owner: 'policy' }, }], })) @@ -183,7 +180,6 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro { kind: 'plugin', plugin: 'hooks-codex' }, { kind: 'plugin', plugin: 'policy' }, ]) - expect(contexts[1]?.type === 'context/message' && contexts[1].data.envelope).toBe('raw') expect(contexts[1]?.type === 'context/message' && contexts[1].data.meta).toEqual({ owner: 'policy' }) }) diff --git a/packages/sandbox/README.md b/packages/sandbox/README.md index 93287f299f..990da84d1a 100644 --- a/packages/sandbox/README.md +++ b/packages/sandbox/README.md @@ -1,12 +1,13 @@ # sandbox/ — process-sandbox capability family -The confinement half of the [capability-seam split](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md): an abstract provider interface and platform backends. Consumers hand `ctx.sandbox` the exact argv they are about to spawn and spawn the returned (wrapped) argv instead; policy (`SandboxPolicy`: mode + workspace root) rides each call, so different consumers confine under different policies at the same instant. All **product** packages. +The confinement half of the [capability-seam split](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md): an abstract provider interface, platform backends, and the shared policy home. Consumers hand `ctx.sandbox` the exact argv they are about to spawn and spawn the returned (wrapped) argv instead; policy (`SandboxPolicy`: mode + workspace root) rides each call, so different consumers confine under different policies at the same instant. All **product** packages. | Package | Role | ctx key | |---|---|---| -| `sandbox/` | Abstract process-sandbox seam (the `SandboxProvider` contract + the mode/enforcement/policy vocabulary) | `ctx.sandbox` | +| `sandbox/` | Abstract process-sandbox seam (the `SandboxProvider` contract + the mode/enforcement/policy vocabulary) plus the shared ESCALATION kit (`approveEscalation`, the strictly-wider ladder, the denial/hint markers) and the `writableRoots` derivation every enforcement dialect shares | `ctx.sandbox` | | `sandbox-local/` | Local backends by platform chain: Linux `bwrap` else the `landlock-run` launcher (the npm-distributed [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run) family, built and released from its own repository), darwin `sandbox-exec`/Seatbelt — multi-candidate chains functionally probed, sole candidates selected directly, verdict cached, fail-closed | (registers `ctx.sandbox`) | +| `sandbox-policy/` | The policy home: the deployment default (mode + `workspace-write` boundary root) and the per-session `sandbox/mode` override (event + fold + write path). Both enforcing families read it, so bash and fs can never confine to different roots | `ctx.sandboxPolicy` | The seam confines SAME-WORLD subprocesses only (shared filesystem and kernel). Containers, microVMs, and remote executors are NOT backends here — they replace whole capability implementations (`ctx.bash`, `ctx.fs`) as environment-coherent groups; the boundary is recorded in [the sandbox Agent Note](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). -Consumers today: [`bash/bash-sandbox`](../bash/bash-sandbox/) (wraps `['bash', '-c', command]`; see [the acp-agent example's default composition](../../examples/acp-agent/) for the composed leaf). In-process tools (fs/web) cannot be confined by an OS wrapper — their sandbox semantics are policy at their own seams (the sandbox Agent Note's cross-family phase). +Consumers today: [`bash/bash-sandbox`](../bash/bash-sandbox/) (wraps `['bash', '-c', command]` through `ctx.sandbox`) and [`fs/fs-sandbox`](../fs/fs-sandbox/) (an in-process path fence, not an argv wrapper — reads `ctx.sandboxPolicy` and enforces the shared mode on write/edit). The cross-family boundary is the sandbox Agent Note's [cross-family fs sandbox](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) phase; the shared vocabulary lets both families teach the model one denial marker and one escalation flow. diff --git a/packages/sandbox/sandbox-local/src/profiles.ts b/packages/sandbox/sandbox-local/src/profiles.ts index 9303583cbb..cee0f00852 100644 --- a/packages/sandbox/sandbox-local/src/profiles.ts +++ b/packages/sandbox/sandbox-local/src/profiles.ts @@ -4,9 +4,8 @@ * @module @deepseek-ai/dsh-sandbox-local/profiles */ -import { realpathSync } from 'node:fs' -import { tmpdir } from 'node:os' import { grantArgs as landlockGrantArgs } from 'node-addon-landlock-run' +import { writableRoots } from '@deepseek-ai/dsh-sandbox' import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox' /** @@ -36,31 +35,23 @@ export function landlockProfileArgs(policy: SandboxPolicy): string[] { return landlockGrantArgs({ readOnly: ['/'], readWrite }) } -/** Resolve a granted root to the canonical path the Seatbelt kernel sees. */ -function canonicalPath(path: string): string { - try { - return realpathSync(path) - } catch { - // Missing or unreadable roots stay as spelled; an unresolved root grants - // nothing until it exists, which is the conservative outcome. - return path - } -} - /** Quote one path as an SBPL string literal. */ function sbplString(path: string): string { return `"${path.replaceAll('\\', String.raw`\\`).replaceAll('"', String.raw`\"`)}"` } /** - * Build the sandbox-exec arguments and SBPL profile for one policy. + * Build the sandbox-exec arguments and SBPL profile for one policy. The + * writable roots come from the shared {@link writableRoots} helper (canonical, + * deduplicated) so the Seatbelt grant and the in-process fs fence + * (`@deepseek-ai/dsh-fs-sandbox`) can never drift apart. * @param policy - file-effect policy to express as an SBPL profile. * @returns sandbox-exec arguments before the trailing separator and command argv. */ export function seatbeltProfileArgs(policy: SandboxPolicy): string[] { const forms = ['(version 1)', '(allow default)', '(deny file-write*)', `(allow file-write* (literal ${sbplString('/dev/null')}))`] - if (policy.mode === 'workspace-write') { - const roots = [...new Set([policy.workspaceRoot, '/tmp', tmpdir()].map(canonicalPath))] + const roots = writableRoots(policy) + if (roots.length > 0) { forms.push(`(allow file-write* ${roots.map(root => `(subpath ${sbplString(root)})`).join(' ')})`) } return ['-p', forms.join(' ')] diff --git a/packages/sandbox/sandbox-policy/README.md b/packages/sandbox/sandbox-policy/README.md new file mode 100644 index 0000000000..4ea562b376 --- /dev/null +++ b/packages/sandbox/sandbox-policy/README.md @@ -0,0 +1,36 @@ +# dsh-sandbox-policy — the sandbox policy home (`ctx.sandboxPolicy`) + +The single owner of the deployment's sandbox policy: the file-effect [`SandboxMode`](../sandbox/README.md) a session starts from, the `workspace-write` boundary root, and the per-session `sandbox/mode` override every enforcing capability family reads. + +## Why a shared home + +Two families enforce the same mode vocabulary: the sandboxed bash executor (`@deepseek-ai/dsh-bash-sandbox`) and the sandboxed filesystem provider (`@deepseek-ai/dsh-fs-sandbox`). If each held its own `mode` + `workspaceRoot` config, the two could drift into a split world — bash confined to one root while fs fences another, exactly what [the sandbox RFC](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) warns against. Both inject `ctx.sandboxPolicy` and read the SAME default instead. The [cross-family fs sandbox RFC](../../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) records the decision. + +## Config + +- `mode` — the deployment default `SandboxMode` (`read-only` / `workspace-write` / `danger-full-access`), validated at load. Default `read-only` (fail-safe). +- `workspaceRoot` — the absolute directory `workspace-write` may write under. Default `process.cwd()`, resolved absolute either way. + +## Surface + +- `ctx.sandboxPolicy.defaultMode` / `ctx.sandboxPolicy.workspaceRoot` — the deployment default the enforcing implementations read for their resolve fallback and boundary. +- `effectiveSandboxMode(events)` — the pure fold of a session's `sandbox/mode` events (the last switch wins, or `undefined`). The tool layers apply it to stamp each call, so neither the executor nor the provider depends on session events. +- `setSandboxMode(session, mode)` — THE write path for a per-session override: appends exactly one `sandbox/mode` event. The switch IS its event; nothing mutates the mode out of band. +- `SANDBOX_MODES` — every mode, for option advertisement and runtime validation. + +## The per-session store + +A runtime switch (an ACP `session/set_config_option`, a test scenario) is one log-only `sandbox/mode` event on the session it applies to. `effective = fold(events) ?? the deployment default`, so an override survives restart by replay, two sessions never see each other's state, and there is no external config store. The event is log-only (the `approval/*` precedent): the model learns the mode from the enforcing tools' denial markers, never from the event. Execution honors the fold in each tool layer, weakest-precedence beneath an escalation grant. + +## Model Experience + +Indirectly, through `dsh-tool-bash` and `dsh-tool-fs`, which render the effective mode this service holds in their `[sandbox: …]` denial markers and escalation prompts; the `sandbox/mode` event itself never reaches the model. + +#### KV Cache effect + +No direct invalidation; the named consumers own any request-prefix changes, and the mode is deliberately absent from the prompt. + +## Known Limitations and Deferred Work + +- **`workspaceRoot` is process-wide and fixed for the service's lifetime** — a per-session workspace root is a deferred phase of the sandbox RFC; this package centralizing the root is its groundwork, not its design. +- **File-effect modes only** — `SandboxMode` governs file effects; network and process policy are outside its vocabulary, so no knob here restricts them. diff --git a/packages/sandbox/sandbox-policy/package.json b/packages/sandbox/sandbox-policy/package.json new file mode 100644 index 0000000000..e5a318e3e1 --- /dev/null +++ b/packages/sandbox/sandbox-policy/package.json @@ -0,0 +1,37 @@ +{ + "name": "@deepseek-ai/dsh-sandbox-policy", + "description": "Sandbox policy home (ctx.sandboxPolicy) for the DeepSeek Harness: the deployment default mode + workspace root and the per-session sandbox/mode override, shared by every enforcing capability family", + "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" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", + "src" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "@deepseek-ai/dsh-sandbox": "^0.0.1", + "@deepseek-ai/dsh-session": "^0.0.1", + "cordis": "^4.0.0-rc.7" + }, + "dependencies": { + "schemastery": "^3.18.0" + }, + "devDependencies": { + "@deepseek-ai/dsh-sandbox": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "cordis": "^4.0.0-rc.7" + } +} diff --git a/packages/sandbox/sandbox-policy/src/index.ts b/packages/sandbox/sandbox-policy/src/index.ts new file mode 100644 index 0000000000..cd7a1545a8 --- /dev/null +++ b/packages/sandbox/sandbox-policy/src/index.ts @@ -0,0 +1,84 @@ +/** + * The sandbox POLICY home (`ctx.sandboxPolicy`): the single owner of the + * deployment's sandbox default — the file-effect {@link SandboxMode} a session + * starts from and the `workspace-write` boundary root — plus the per-session + * override kit (the `sandbox/mode` event, its fold, and its write path, from + * `./session-mode.ts`). + * + * Both enforcing capability families read the SAME policy here: the sandboxed + * bash executor (`@deepseek-ai/dsh-bash-sandbox`) and the sandboxed filesystem + * provider (`@deepseek-ai/dsh-fs-sandbox`) inject `ctx.sandboxPolicy` for the + * default mode and workspace root, so bash and fs can never confine to + * different roots — the split world the sandbox RFC warns about. The default + * lives here rather than on either executor's config precisely because it is + * one fact two families share. + * + * This service holds only the DEFAULT; the per-session fold + * ({@link effectiveSandboxMode}) is a pure function the tool layers apply to + * stamp each call, so neither the executor nor the provider depends on session + * events. + * + * @module @deepseek-ai/dsh-sandbox-policy + */ + +import { resolve } from 'node:path' +import { Context, Service } from 'cordis' +import z from 'schemastery' +import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' + +export { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from './session-mode.ts' + +declare module 'cordis' { + interface Context { + sandboxPolicy: SandboxPolicyService + } +} + +/** + * Plugin config: the deployment's sandbox default. All optional — `Config` + * supplies the defaults (`mode: 'read-only'` is the fail-safe default; a + * deployment that wants a workspace-writable agent opts in explicitly). The + * runner choice is NOT here (it is the `ctx.sandbox` provider's config), nor + * is any per-family knob: this is the one shared policy home. + */ +export interface Config { + /** File-sandbox mode a session starts from (default: `read-only`). */ + mode?: SandboxMode + /** + * Absolute root directory `workspace-write` may write under (default: + * `process.cwd()`). Both enforcing families fence against this SAME root. + */ + workspaceRoot?: string +} + +/** + * The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment + * default mode and workspace root; enforcing implementations read + * {@link defaultMode} and {@link workspaceRoot}, and the tool layers fold each + * session's `sandbox/mode` override with {@link effectiveSandboxMode} on top. + */ +export class SandboxPolicyService extends Service { + // Inline schema call: the config catalog walks `static Config` statically. + static Config: z = z.object({ + mode: z.union(['read-only', 'workspace-write', 'danger-full-access'] as const).default('read-only'), + // No schema default: process.cwd() is resolved in the constructor so the + // stored root is always absolute regardless of how it was supplied. + workspaceRoot: z.string(), + }) + + /** The deployment default mode — the fallback beneath a session override. */ + readonly defaultMode: SandboxMode + /** The absolute `workspace-write` boundary root both families fence against. */ + readonly workspaceRoot: string + + constructor(ctx: Context, config: Config) { + super(ctx, 'sandboxPolicy') + // schemastery (static Config) already filled `mode`; the cast records that + // runtime fact. `workspaceRoot` has NO schema default, so its fallback to + // the process cwd is real branching, resolved absolute either way. + this.defaultMode = config.mode as SandboxMode + this.workspaceRoot = resolve(config.workspaceRoot ?? process.cwd()) + } +} + +export default SandboxPolicyService diff --git a/packages/sandbox/sandbox-policy/src/session-mode.ts b/packages/sandbox/sandbox-policy/src/session-mode.ts new file mode 100644 index 0000000000..62be36501f --- /dev/null +++ b/packages/sandbox/sandbox-policy/src/session-mode.ts @@ -0,0 +1,68 @@ +/** + * Per-session sandbox-mode override: the session log as the store. A runtime + * switch (an ACP `session/set_config_option`, a test scenario) is recorded as + * one `sandbox/mode` event on the session it applies to; + * `effective = fold(events) ?? the deployment default`, so an override + * survives restart by replay, two sessions can never see each other's state, + * and there is no external config store. The event is log-only (the + * `approval/*` precedent): the model learns the mode from the boundary + * markers in the enforcing tools, never from the event itself. EXECUTION + * honors the fold in each tool layer — it stamps the effective mode onto the + * per-call policy carrier (a bash request's `sandboxMode`, an fs mutation's + * `sandboxMode`), weakest-precedence beneath an escalation grant. + * + * The override is policy state shared by every enforcing family (bash and + * filesystem alike), so it lives here in the policy package rather than in any + * one capability's seam. + * + * @module dsh-sandbox-policy/session-mode + */ + +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' + +declare module '@deepseek-ai/dsh-session' { + interface SessionEventMap { + /** + * The session's sandbox mode was switched — log-only (like `approval/*`; + * NOT a surface event, carries no `surfaceOp`): durable and replayable, + * never in the model transcript. The LAST such event is the session's + * override ({@link effectiveSandboxMode}); who asked for it is derivable + * from position (an event after the log's last `request/header*` was a + * runtime switch by the user; see the tool layer's narrator). + */ + 'sandbox/mode': { mode: SandboxMode } + } +} + +/** Every {@link SandboxMode}, for option advertisement and runtime validation of untrusted mode strings. */ +export const SANDBOX_MODES: readonly SandboxMode[] = ['read-only', 'workspace-write', 'danger-full-access'] + +/** + * The session's sandbox-mode override: the last `sandbox/mode` event in the + * log, or undefined when the session never switched (callers apply the + * deployment default). The pure fold — resume needs no catch-up machinery + * because replaying the log IS the state. + * @param events - session events in log order (other event types are skipped). + * @returns the mode of the last switch event, or undefined without one. + */ +export function effectiveSandboxMode(events: readonly SessionEvent[]): SandboxMode | undefined { + for (let index = events.length - 1; index >= 0; index -= 1) { + const event = events[index] as SessionEvent + if (event.type === 'sandbox/mode') return event.data.mode + } + return undefined +} + +/** + * THE write path for a session's sandbox-mode override: appends exactly one + * `sandbox/mode` event — the switch IS its event; nothing mutates mode state + * out of band. Takes effect on the session's next confined call (bash or fs) + * — the consumers fold on every read. + * @param session - the session the override belongs to. + * @param mode - the mode every subsequent confined call in this session runs + * under (until the next switch). + */ +export function setSandboxMode(session: Session, mode: SandboxMode): void { + session.append('sandbox/mode', { mode }) +} diff --git a/packages/sandbox/sandbox-policy/tests/policy.spec.ts b/packages/sandbox/sandbox-policy/tests/policy.spec.ts new file mode 100644 index 0000000000..52476fdece --- /dev/null +++ b/packages/sandbox/sandbox-policy/tests/policy.spec.ts @@ -0,0 +1,67 @@ +/** + * Tests for the sandbox-policy home: the deployment default (mode + + * workspaceRoot) the service exposes, and the per-session `sandbox/mode` + * override kit (fold + write path) both enforcing families read. + */ + +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { Session, SessionId } from '@deepseek-ai/dsh-session' +import SandboxPolicyService, { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy' + +async function mounted(config: { mode?: 'read-only' | 'workspace-write' | 'danger-full-access'; workspaceRoot?: string } = {}) { + const ctx = new Context() + await ctx.plugin(SandboxPolicyService, config) + return ctx +} + +describe('SandboxPolicyService', () => { + it('defaults to read-only under the process cwd', async () => { + const ctx = await mounted() + expect(ctx.sandboxPolicy.defaultMode).toBe('read-only') + expect(ctx.sandboxPolicy.workspaceRoot).toBe(resolve(process.cwd())) + }) + + it('carries a configured mode and resolves the workspace root absolute', async () => { + const ctx = await mounted({ mode: 'workspace-write', workspaceRoot: '/ws/../ws/./sub' }) + expect(ctx.sandboxPolicy.defaultMode).toBe('workspace-write') + expect(ctx.sandboxPolicy.workspaceRoot).toBe(resolve('/ws/../ws/./sub')) + }) + + it('rejects a mode outside the closed vocabulary at load', async () => { + const ctx = new Context() + // schemastery rejects the union violation when the plugin loads. + await expect(ctx.plugin(SandboxPolicyService, { mode: 'yolo' as never })).rejects.toThrow() + }) + + it('unregisters cleanly from a child fiber (HMR safety)', async () => { + const ctx = new Context() + const fiber = await ctx.plugin(SandboxPolicyService, {}) + expect(ctx.sandboxPolicy).toBeDefined() + await fiber.dispose() + expect(ctx.get('sandboxPolicy')).toBeUndefined() + }) +}) + +describe('the sandbox/mode session kit', () => { + it('SANDBOX_MODES lists every mode for advertisement and validation', () => { + expect(SANDBOX_MODES).toEqual(['read-only', 'workspace-write', 'danger-full-access']) + }) + + it('effectiveSandboxMode folds to the last switch, or undefined without one', () => { + const session = new Session(SessionId('sess-fold')) + expect(effectiveSandboxMode(session.events)).toBeUndefined() + setSandboxMode(session, 'workspace-write') + setSandboxMode(session, 'read-only') + expect(effectiveSandboxMode(session.events)).toBe('read-only') + }) + + it('setSandboxMode appends exactly one sandbox/mode event per switch', () => { + const session = new Session(SessionId('sess-write')) + setSandboxMode(session, 'danger-full-access') + const modeEvents = session.events.filter(e => e.type === 'sandbox/mode') + expect(modeEvents).toHaveLength(1) + expect(modeEvents[0]?.data).toEqual({ mode: 'danger-full-access' }) + }) +}) diff --git a/packages/sandbox/sandbox-policy/tsconfig.json b/packages/sandbox/sandbox-policy/tsconfig.json new file mode 100644 index 0000000000..fc0c96c6de --- /dev/null +++ b/packages/sandbox/sandbox-policy/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../sandbox" + }, + { + "path": "../../core/session" + } + ] +} diff --git a/packages/sandbox/sandbox/src/escalation.ts b/packages/sandbox/sandbox/src/escalation.ts new file mode 100644 index 0000000000..e0b9a2ce63 --- /dev/null +++ b/packages/sandbox/sandbox/src/escalation.ts @@ -0,0 +1,189 @@ +/** + * The escalation vocabulary and choreography shared by every sandbox-enforcing + * tool family (`@deepseek-ai/dsh-tool-bash`, `@deepseek-ai/dsh-tool-fs`): the + * strictly-wider ladder, the argument-pairing validation, the model-facing + * denial/hint markers, and {@link approveEscalation} — the ordered fail-closed + * sequence that resolves a `sandbox_permissions` request through a + * user-approval channel BEFORE anything executes. One home keeps the two + * families' approval ordering and verbatim error texts from drifting apart. + * + * The channel is a minimal STRUCTURAL function shape ({@link EscalationAsk}), + * not the approval service type: the tool layer — which owns the agent, the + * call id, and the tool name — closes over `ctx.approval.request(...)` and + * hands the closure down, so this package never depends on the approval or + * agent packages. + * + * @module dsh-sandbox/escalation + */ + +import { assertNever } from '@deepseek-ai/dsh-llm' +import type { SandboxMode } from './index.ts' + +/** + * The strictly-wider table: what a call whose effective mode is the key may + * escalate TO. Checked at EXECUTION, never baked into a tool schema — the + * schema's enum is {@link ESCALATION_TARGETS}, because schemas are + * registry-global while the effective mode is per-call truth. + */ +export const WIDER_MODES: Record = { + 'read-only': ['workspace-write', 'danger-full-access'], + 'workspace-write': ['danger-full-access'], +} + +/** + * The closed escalation-target vocabulary — every mode a call could ever + * escalate TO (`read-only` is the floor; nothing escalates to it). Advertised + * whenever the mounted capability confines: cutting the enum down to the modes + * wider than the composition's DEFAULT would strand a session whose effective + * mode sits below it (a `danger-full-access` default would advertise nothing + * while a narrower-switched session stays confined with no lever). + */ +export const ESCALATION_TARGETS: readonly SandboxMode[] = ['workspace-write', 'danger-full-access'] + +/** + * Validate the escalation argument pairing a tool schema cannot express: + * `sandbox_permissions` and `justification` travel together — an approval + * prompt without a reason, or a reason driving nothing, is a malformed ask — + * and the justification must be a non-empty sentence. + * @param sandboxPermissions - the raw `sandbox_permissions` argument, if given. + * @param justification - the raw `justification` argument, if given. + */ +export function validateEscalationArgs(sandboxPermissions: string | undefined, justification: string | undefined): void { + if (sandboxPermissions !== undefined && justification === undefined) { + throw new Error('invalid escalation: sandbox_permissions requires a justification') + } + if (justification !== undefined && sandboxPermissions === undefined) { + throw new Error('invalid escalation: justification is only valid together with sandbox_permissions') + } + if (justification !== undefined && justification.trim().length === 0) { + throw new Error('invalid justification: expected a non-empty sentence') + } +} + +/** + * The model-facing denial marker — the one vocabulary both enforcing families + * teach and report, so the model recognizes a policy denial identically + * whether the kernel refused a bash file effect or the filesystem provider's + * fence refused a mutation. + * @param mode - the mode the denied call ran under. + * @returns the marker line, exactly as the model sees it. + */ +export function sandboxDenialMarker(mode: SandboxMode): string { + return `[sandbox: file access denied under ${mode} mode]` +} + +/** + * The same-turn escalation hint that rides a denial when the composition + * advertises the escalation fields — the nudge lives at the decision point so + * the sanctioned retry does not depend on the model recalling the tool + * description. + * @param subject - the family's noun for the denied action (`command` for + * bash, `operation` for a filesystem mutation). + * @returns the hint line, exactly as the model sees it. + */ +export function escalationHintMarker(subject: string): string { + return `[sandbox: escalation available — retry this exact ${subject} once with sandbox_permissions (the narrowest wider mode that suffices) + justification; the approval prompt asks the user]` +} + +/** + * The closed outcome vocabulary of one escalation ask — structurally identical + * to the approval seam's `ApprovalOutcome` so an `ApprovalService.request` + * return is assignable without this package importing it. + */ +export type EscalationOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable' + +/** + * The minimal approval-request shape {@link approveEscalation} needs — + * structurally the approval seam's `ApprovalService`, generic over the agent + * type `A` and call-id type `C` so this package resolves escalations through + * `ctx.approval` without importing the approval or agent packages (the tool + * layer infers `A`/`C` as its own `Agent`/`CallId`). + */ +export interface EscalationApprover { + /** + * Ask the human to approve one action, resolving to a closed outcome. + * @param req - the audit-self-contained request (agent, tool, call id, reason, optional signal). + * @returns the human's decision as a closed {@link EscalationOutcome}. + */ + request(req: { agent: A; toolName: string; callId: C; reason: string; signal?: AbortSignal }): Promise +} + +/** + * The approval ingredients an escalating tool hands {@link approveEscalation}: + * the approval requester (`ctx.approval`, or `undefined` when none is + * composed), the calling agent (or `undefined` for an agent-less execution), + * and the call's identity. The tool layer holds all of these; this package + * only judges them. + */ +export interface EscalationApproval { + /** The approval requester (`ctx.approval`), or `undefined` when none is composed. */ + approver: EscalationApprover | undefined + /** The calling agent, or `undefined` for an agent-less execution (fails closed). */ + agent: A | undefined + /** The tool-call id the approval prompt attaches to. */ + callId: C + /** The tool name recorded on the approval request. */ + toolName: string + /** The tool-execution abort signal the approval request rides, when present. */ + signal?: AbortSignal +} + +/** One escalation request, as {@link approveEscalation} judges it. */ +export interface EscalationRequest { + /** The requested target mode (schema-pinned to {@link ESCALATION_TARGETS} when advertised). */ + requestedMode: string + /** The model's one-sentence reason, shown verbatim to the user inside the audit reason. */ + justification: string + /** The call's effective mode (session override ?? composition default) the request must strictly widen. */ + effectiveMode: SandboxMode + /** The family's noun for the escalated action in user-facing texts (`command` for bash, `operation` for fs). */ + subject: string +} + +/** + * Resolve a sandbox-escalation request BEFORE anything executes: check strict + * widening against the call's effective mode, then resolve the approval + * channel, then map every outcome — the ordered fail-closed sequence both + * enforcing families share. Returns the granted mode to stamp onto exactly + * this call; throws the distinct verbatim text for every other path (a + * non-widening request, a missing approval service, an agent-less execution, + * a rejection, a cancellation, an unanswerable ask) — the tool registry turns + * the throw into the call's isError result, and nothing has run. A + * non-widening request never prompts a human. + * @param request - the escalation to judge (see {@link EscalationRequest}). + * @param approval - the approval ingredients the tool holds (see {@link EscalationApproval}). + * @returns the granted mode, consumed by the one call that asked. + */ +export async function approveEscalation(request: EscalationRequest, approval: EscalationApproval): Promise { + const { requestedMode: mode, effectiveMode, justification, subject } = request + // Strict widening is an EXECUTION check against the call's effective mode — + // deliberately not a schema constraint (the enum is the closed target + // vocabulary; the effective mode is per-call truth). + if (!(WIDER_MODES[effectiveMode] ?? []).includes(mode as SandboxMode)) { + throw new Error(`sandbox escalation to "${mode}" is not strictly wider than this call's current "${effectiveMode}" mode`) + } + if (approval.approver === undefined) { + throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval service is composed`) + } + if (approval.agent === undefined) { + throw new Error(`sandbox escalation to "${mode}" requires approval, but the call has no agent to route it through`) + } + // Self-contained for the audit trail: approval/asked stores this reason, + // and the target mode is part of the grant's identity. + const outcome = await approval.approver.request({ + agent: approval.agent, + toolName: approval.toolName, + callId: approval.callId, + reason: `escalate sandbox to ${mode}: ${justification}`, + ...approval.signal ? { signal: approval.signal } : {}, + }) + switch (outcome) { + // The schema enum already pinned `mode` to the closed target vocabulary; + // the check above proved it is strictly wider. + case 'allowed-once': return mode as SandboxMode + case 'rejected': throw new Error(`the user rejected escalating this ${subject} to "${mode}"`) + case 'cancelled': throw new Error(`approval for escalating to "${mode}" was cancelled`) + case 'unavailable': throw new Error(`sandbox escalation to "${mode}" requires approval, but no approval channel is available`) + default: return assertNever(outcome, 'EscalationOutcome') + } +} diff --git a/packages/sandbox/sandbox/src/index.ts b/packages/sandbox/sandbox/src/index.ts index 75cea5ecfe..d54ef581ab 100644 --- a/packages/sandbox/sandbox/src/index.ts +++ b/packages/sandbox/sandbox/src/index.ts @@ -8,6 +8,17 @@ import { Context, Service } from 'cordis' import { HarnessError } from '@deepseek-ai/dsh-llm' +export { + ESCALATION_TARGETS, + WIDER_MODES, + approveEscalation, + escalationHintMarker, + sandboxDenialMarker, + validateEscalationArgs, +} from './escalation.ts' +export type { EscalationApproval, EscalationApprover, EscalationOutcome, EscalationRequest } from './escalation.ts' +export { canonicalPath, writableRoots } from './roots.ts' + /** * File-effect policy for confined processes. `read-only` permits only required * sinks such as `/dev/null`; `workspace-write` also permits the workspace and a diff --git a/packages/sandbox/sandbox/src/roots.ts b/packages/sandbox/sandbox/src/roots.ts new file mode 100644 index 0000000000..2d70148cdf --- /dev/null +++ b/packages/sandbox/sandbox/src/roots.ts @@ -0,0 +1,51 @@ +/** + * The writable-root derivation shared by every enforcement dialect that + * expresses a mode as a canonical allow-list: `workspace-write` means "the + * workspace root plus the platform temp areas", and this module is that + * meaning's one home. The Seatbelt profile + * (`@deepseek-ai/dsh-sandbox-local`) and the in-process filesystem fence + * (`@deepseek-ai/dsh-fs-sandbox`) both derive their allow-list here, so "the + * write tool cannot write /tmp but bash can" asymmetries cannot arise between + * them. The bwrap and Landlock dialects keep their own grant spellings (an + * ephemeral `/tmp` mount, launcher-owned flags) — the honest per-runner + * differences recorded in the sandbox RFC — with parity pinned by test. + * + * @module dsh-sandbox/roots + */ + +import { realpathSync } from 'node:fs' +import { tmpdir } from 'node:os' +import type { SandboxPolicy } from './index.ts' + +/** + * Resolve a granted root to the path the enforcement layer actually compares: + * canonical (symlinks resolved), because both Seatbelt filters and the fs + * fence's containment check match resolved paths — `/tmp` IS `/private/tmp` + * on darwin, and an as-spelled grant would match nothing. + * @param path - the root as configured or platform-reported. + * @returns the canonical path, or the spelling as-is when resolution fails + * (a missing root matches nothing until it exists — the conservative + * outcome; inventing a fallback would grant a path the caller never named). + */ +export function canonicalPath(path: string): string { + try { + return realpathSync(path) + } catch { + // realpathSync failed: the path (or a prefix) is missing or unreadable. + return path + } +} + +/** + * The roots one confined execution may WRITE under — the mode's meaning as a + * canonical, deduplicated allow-list. `read-only` allows nothing; + * `workspace-write` allows the policy's workspace root, the host `/tmp`, and + * the per-user platform temp dir (`os.tmpdir()` — the real temp area for + * mkstemp-family tools; omitting it would deny what the mode promises). + * @param policy - the file-effect policy to derive the allow-list from. + * @returns the canonical writable roots; empty exactly under `read-only`. + */ +export function writableRoots(policy: SandboxPolicy): string[] { + if (policy.mode !== 'workspace-write') return [] + return [...new Set([policy.workspaceRoot, '/tmp', tmpdir()].map(canonicalPath))] +} diff --git a/packages/sandbox/sandbox/tests/escalation.spec.ts b/packages/sandbox/sandbox/tests/escalation.spec.ts new file mode 100644 index 0000000000..15810d09d5 --- /dev/null +++ b/packages/sandbox/sandbox/tests/escalation.spec.ts @@ -0,0 +1,111 @@ +/** + * Tests for the shared escalation vocabulary and choreography: the strictly- + * wider ladder, the argument-pairing validation, the model-facing markers, and + * {@link approveEscalation}'s ordered fail-closed sequence. Both enforcing tool + * families (`dsh-tool-bash`, `dsh-tool-fs`) delegate here, so the ordering and + * verbatim texts are pinned once, next to the vocabulary that owns them. + */ + +import { describe, expect, it } from 'vitest' +import { + ESCALATION_TARGETS, + WIDER_MODES, + approveEscalation, + escalationHintMarker, + sandboxDenialMarker, + validateEscalationArgs, +} from '@deepseek-ai/dsh-sandbox' +import type { EscalationApprover, EscalationOutcome } from '@deepseek-ai/dsh-sandbox' + +describe('the strictly-wider ladder', () => { + it('read-only escalates to either wider mode; workspace-write only to full access', () => { + expect(WIDER_MODES['read-only']).toEqual(['workspace-write', 'danger-full-access']) + expect(WIDER_MODES['workspace-write']).toEqual(['danger-full-access']) + expect(WIDER_MODES['danger-full-access']).toBeUndefined() + }) + + it('the target enum is the closed set every session could escalate TO (read-only is the floor)', () => { + expect(ESCALATION_TARGETS).toEqual(['workspace-write', 'danger-full-access']) + }) +}) + +describe('validateEscalationArgs', () => { + it('accepts neither field, or both with a non-empty justification', () => { + expect(() => { validateEscalationArgs(undefined, undefined) }).not.toThrow() + expect(() => { validateEscalationArgs('workspace-write', 'because the workspace needs it') }).not.toThrow() + }) + + it('rejects one field without the other, and a blank justification', () => { + expect(() => { validateEscalationArgs('workspace-write', undefined) }).toThrow(/requires a justification/) + expect(() => { validateEscalationArgs(undefined, 'orphan reason') }).toThrow(/only valid together with sandbox_permissions/) + expect(() => { validateEscalationArgs('workspace-write', ' ') }).toThrow(/non-empty sentence/) + }) +}) + +describe('the model-facing markers', () => { + it('the denial marker names the mode', () => { + expect(sandboxDenialMarker('read-only')).toBe('[sandbox: file access denied under read-only mode]') + expect(sandboxDenialMarker('workspace-write')).toBe('[sandbox: file access denied under workspace-write mode]') + }) + + it('the hint marker names the family subject', () => { + expect(escalationHintMarker('command')).toContain('retry this exact command once with sandbox_permissions') + expect(escalationHintMarker('operation')).toContain('retry this exact operation once with sandbox_permissions') + }) +}) + +describe('approveEscalation', () => { + const req = (over: Partial[0]> = {}) => ({ + requestedMode: 'workspace-write', + justification: 'the user asked to write in the workspace', + effectiveMode: 'read-only' as const, + subject: 'command', + ...over, + }) + /** An approver that records the request and returns a fixed outcome. */ + const approver = (outcome: EscalationOutcome, sink?: (req: unknown) => void): EscalationApprover => ({ + request: async (request) => { sink?.(request); return outcome }, + }) + const ingredients = (over: Partial[1]> = {}) => ({ + approver: approver('allowed-once'), + agent: {}, + callId: 'call-1', + toolName: 'bash', + ...over, + }) + + it('grants: returns the requested mode, asking through the approver with the audit reason', async () => { + const seen: { reason?: string }[] = [] + const granted = await approveEscalation(req(), ingredients({ approver: approver('allowed-once', r => seen.push(r as { reason?: string })) })) + expect(granted).toBe('workspace-write') + expect(seen[0]?.reason).toBe('escalate sandbox to workspace-write: the user asked to write in the workspace') + }) + + it('a non-widening request fails closed with its own text and never asks', async () => { + const seen: unknown[] = [] + const spy = ingredients({ approver: approver('allowed-once', r => seen.push(r)) }) + await expect(approveEscalation(req({ requestedMode: 'read-only' }), spy)) + .rejects.toThrow(/not strictly wider than this call's current "read-only" mode/) + await expect(approveEscalation(req({ requestedMode: 'workspace-write', effectiveMode: 'danger-full-access' as never }), spy)) + .rejects.toThrow(/not strictly wider/) + expect(seen).toEqual([]) + }) + + it('a missing approval service and an agent-less call each fail closed with distinct text', async () => { + await expect(approveEscalation(req(), ingredients({ approver: undefined }))).rejects.toThrow(/no approval service is composed/) + await expect(approveEscalation(req(), ingredients({ agent: undefined }))).rejects.toThrow(/no agent to route it through/) + }) + + it('maps each non-grant outcome to its distinct verbatim text (subject in the rejection)', async () => { + await expect(approveEscalation(req({ subject: 'operation' }), ingredients({ approver: approver('rejected') }))) + .rejects.toThrow('the user rejected escalating this operation to "workspace-write"') + await expect(approveEscalation(req(), ingredients({ approver: approver('cancelled') }))) + .rejects.toThrow('approval for escalating to "workspace-write" was cancelled') + await expect(approveEscalation(req(), ingredients({ approver: approver('unavailable') }))) + .rejects.toThrow('no approval channel is available') + }) + + it('an outcome outside the closed union trips the exhaustiveness guard (defensive)', async () => { + await expect(approveEscalation(req(), ingredients({ approver: approver('bogus' as never) }))).rejects.toThrow() + }) +}) diff --git a/packages/sandbox/sandbox/tests/roots.spec.ts b/packages/sandbox/sandbox/tests/roots.spec.ts new file mode 100644 index 0000000000..fd0d2cd7bd --- /dev/null +++ b/packages/sandbox/sandbox/tests/roots.spec.ts @@ -0,0 +1,39 @@ +/** + * Tests for the writable-root derivation: the mode's meaning as a canonical + * allow-list. Pinned here so the fs fence and the Seatbelt profile — both + * deriving from `writableRoots` — cannot drift. + */ + +import { realpathSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { mkdtempSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox' + +describe('canonicalPath', () => { + it('resolves symlinks (an existing path realpaths)', () => { + const dir = mkdtempSync(join(tmpdir(), 'dsh-roots-')) + expect(canonicalPath(dir)).toBe(realpathSync(dir)) + }) + + it('returns the spelling as-is when the path cannot be resolved (conservative — matches nothing until it exists)', () => { + expect(canonicalPath('/does/not/exist/anywhere-xyz')).toBe('/does/not/exist/anywhere-xyz') + }) +}) + +describe('writableRoots', () => { + it('read-only grants nothing', () => { + expect(writableRoots({ mode: 'read-only', workspaceRoot: process.cwd() })).toEqual([]) + }) + + it('workspace-write grants the workspace root plus the platform temp areas, canonical and deduplicated', () => { + const ws = mkdtempSync(join(tmpdir(), 'dsh-ws-')) + const roots = writableRoots({ mode: 'workspace-write', workspaceRoot: ws }) + expect(roots).toContain(realpathSync(ws)) + expect(roots).toContain(canonicalPath('/tmp')) + expect(roots).toContain(realpathSync(tmpdir())) + // Deduplicated after canonicalization (/tmp and os.tmpdir() may coincide). + expect(new Set(roots).size).toBe(roots.length) + }) +}) diff --git a/packages/session-persistence/session-persistence-jsonl/README.md b/packages/session-persistence/session-persistence-jsonl/README.md index cf733b85d4..d6130d414a 100644 --- a/packages/session-persistence/session-persistence-jsonl/README.md +++ b/packages/session-persistence/session-persistence-jsonl/README.md @@ -10,7 +10,7 @@ The JSONL durable session-persistence backend — a concrete `SessionPersistence .jsonl # header line + one SessionEvent per line (verbatim) ``` -- The first `.jsonl` line is the immutable `SessionHeader` tagged `{ type: 'session', version, id, cwd?, createdAt, parentSession?, seedLength? }`; every subsequent line is one `SessionEvent` JSON, **verbatim including `assistant/chunk`** so `seq` stays contiguous (`events[i].seq === i`). +- The first `.jsonl` line is the immutable `SessionHeader` tagged `{ type: 'session', version, id, cwd?, createdAt, parentSession?, seedLength?, delegationDepth }`. `delegationDepth` is required on disk and is `0` for a top-level session; a missing or invalid value rejects the log. Every subsequent line is one `SessionEvent` JSON, **verbatim including `assistant/chunk`** so `seq` stays contiguous (`events[i].seq === i`). - Session ids are unvalidated branded strings, so they are percent-encoded to a single safe path segment before use (no traversal, no collision). ## Config diff --git a/packages/session-persistence/session-persistence-jsonl/src/format.ts b/packages/session-persistence/session-persistence-jsonl/src/format.ts index 39bdecf751..3349eac12e 100644 --- a/packages/session-persistence/session-persistence-jsonl/src/format.ts +++ b/packages/session-persistence/session-persistence-jsonl/src/format.ts @@ -25,6 +25,7 @@ export interface HeaderLine { cwd?: string parentSession?: SessionId seedLength?: number + delegationDepth: number } /** @@ -41,6 +42,7 @@ export function toHeaderLine(header: SessionHeader): HeaderLine { ...header.cwd !== undefined ? { cwd: header.cwd } : {}, ...header.parentSession !== undefined ? { parentSession: header.parentSession } : {}, ...header.seedLength !== undefined ? { seedLength: header.seedLength } : {}, + delegationDepth: header.delegationDepth ?? 0, } } @@ -57,6 +59,7 @@ export function fromHeaderLine(line: HeaderLine): SessionHeader { ...line.cwd !== undefined ? { cwd: line.cwd } : {}, ...line.parentSession !== undefined ? { parentSession: line.parentSession } : {}, ...line.seedLength !== undefined ? { seedLength: line.seedLength } : {}, + delegationDepth: line.delegationDepth, } } @@ -68,6 +71,10 @@ function isHeaderLine(value: unknown): value is HeaderLine { && typeof (value as { version?: unknown }).version === 'number' && typeof (value as { id?: unknown }).id === 'string' && typeof (value as { createdAt?: unknown }).createdAt === 'number' + && typeof (value as { delegationDepth?: unknown }).delegationDepth === 'number' + && Number.isSafeInteger((value as { delegationDepth: number }).delegationDepth) + && (value as { delegationDepth: number }).delegationDepth >= 0 + && !Object.is((value as { delegationDepth: number }).delegationDepth, -0) ) } diff --git a/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts b/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts index e7dc469132..9a236c5397 100644 --- a/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts +++ b/packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts @@ -461,9 +461,30 @@ describe('SessionPersistenceJsonl: scanLog unit', () => { expect(() => scanLog(Buffer.from('{"type":"event"}\n'))).toThrow(/session header/) }) + it.each([ + ['missing', undefined], + ['a string', '1'], + ['fractional', 1.5], + ['negative', -1], + ])('rejects a session header with %s delegationDepth', (_label, delegationDepth) => { + const log = JSON.stringify({ + type: 'session', + version: 0, + id: 'invalid-depth', + createdAt: 1, + ...delegationDepth === undefined ? {} : { delegationDepth }, + }) + '\n' + expect(() => scanLog(Buffer.from(log))).toThrow(/session header/) + }) + + it('rejects a session header with negative-zero delegationDepth', () => { + const log = '{"type":"session","version":0,"id":"invalid-depth","createdAt":1,"delegationDepth":-0}\n' + expect(() => scanLog(Buffer.from(log))).toThrow(/session header/) + }) + it('a seq gap after the last turn/end bounds the preserved tail (torn fragment tolerated)', () => { const log = [ - JSON.stringify({ type: 'session', version: 0, id: 'g', createdAt: 1 }), + JSON.stringify({ type: 'session', version: 0, id: 'g', createdAt: 1, delegationDepth: 0 }), JSON.stringify({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } }), JSON.stringify({ type: 'step/start', seq: 2, time: 2, data: { turn: 1, step: 1 } }), // gap: missing seq 1 ].join('\n') + '\n' @@ -475,7 +496,7 @@ describe('SessionPersistenceJsonl: scanLog unit', () => { it('rejects a seq gap BEFORE a later committed turn/end (committed data damaged)', () => { const log = [ - JSON.stringify({ type: 'session', version: 0, id: 'g2', createdAt: 1 }), + JSON.stringify({ type: 'session', version: 0, id: 'g2', createdAt: 1, delegationDepth: 0 }), JSON.stringify({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } }), JSON.stringify({ type: 'step/start', seq: 2, time: 2, data: { turn: 1, step: 1 } }), // gap: missing seq 1 JSON.stringify({ type: 'turn/end', seq: 3, time: 3, data: { turn: 1, reason: { kind: 'completed' } } }), @@ -487,7 +508,7 @@ describe('SessionPersistenceJsonl: scanLog unit', () => { it('rejects a corrupt line BEFORE a later committed turn/end (committed data damaged)', () => { const log = [ - JSON.stringify({ type: 'session', version: 0, id: 'c', createdAt: 1 }), + JSON.stringify({ type: 'session', version: 0, id: 'c', createdAt: 1, delegationDepth: 0 }), '{not json', // corrupt, sits in the committed region (a turn/end follows) JSON.stringify({ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } }), ].join('\n') + '\n' @@ -495,7 +516,7 @@ describe('SessionPersistenceJsonl: scanLog unit', () => { }) it('a header-only log (no event lines at all) preserves nothing — committedBytes is the header', () => { - const log = JSON.stringify({ type: 'session', version: 0, id: 'h0', createdAt: 1 }) + '\n' + const log = JSON.stringify({ type: 'session', version: 0, id: 'h0', createdAt: 1, delegationDepth: 0 }) + '\n' const scanned = scanLog(Buffer.from(log)) expect(scanned.events).toEqual([]) // committedBytes falls back to the header line's end (no preserved events). @@ -504,7 +525,7 @@ describe('SessionPersistenceJsonl: scanLog unit', () => { it('a corrupt line after the last turn/end bounds the preserved tail', () => { const log = [ - JSON.stringify({ type: 'session', version: 0, id: 'c2', createdAt: 1 }), + JSON.stringify({ type: 'session', version: 0, id: 'c2', createdAt: 1, delegationDepth: 0 }), JSON.stringify({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } }), '{not json', // corrupt crash fragment, no turn/end committed ].join('\n') + '\n' @@ -515,7 +536,7 @@ describe('SessionPersistenceJsonl: scanLog unit', () => { it('tolerates a seq gap AFTER a turn/end (uncommitted tail)', () => { const log = [ - JSON.stringify({ type: 'session', version: 0, id: 't', createdAt: 1 }), + JSON.stringify({ type: 'session', version: 0, id: 't', createdAt: 1, delegationDepth: 0 }), JSON.stringify({ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } }), JSON.stringify({ type: 'turn/end', seq: 1, time: 2, data: { turn: 1, reason: { kind: 'completed' } } }), JSON.stringify({ type: 'step/start', seq: 9, time: 3, data: { turn: 2, step: 1 } }), // gap in uncommitted tail @@ -593,7 +614,7 @@ describe('SessionPersistenceJsonl: edge cases', () => { // `readFirstLine` accumulates chunks before `list()` parses it. const bucket = join(root, '_no-cwd') await mkdir(bucket, { recursive: true }) - const bigHeader = JSON.stringify({ type: 'session', version: 0, id: 'big', createdAt: 1, pad: 'x'.repeat(9000) }) + const bigHeader = JSON.stringify({ type: 'session', version: 0, id: 'big', createdAt: 1, delegationDepth: 0, pad: 'x'.repeat(9000) }) await writeFile(join(bucket, 'big.jsonl'), bigHeader + '\n') const ids = (await ctx.sessionPersistence.list()).map(x => x.id) expect(ids).toContain('big') diff --git a/packages/session-persistence/session-persistence-sqlite/src/index.ts b/packages/session-persistence/session-persistence-sqlite/src/index.ts index 4661b41309..edba26d9ae 100644 --- a/packages/session-persistence/session-persistence-sqlite/src/index.ts +++ b/packages/session-persistence/session-persistence-sqlite/src/index.ts @@ -253,14 +253,15 @@ export class SessionPersistenceSqlite extends SessionPersistence implements Pers */ private writeRow(meta: SessionHeader): void { this.db.prepare(` - INSERT INTO sessions (id, version, created_at, cwd, parent_session, seed_length) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO sessions (id, version, created_at, cwd, parent_session, seed_length, delegation_depth) + VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET version = excluded.version, created_at = excluded.created_at, cwd = excluded.cwd, parent_session = excluded.parent_session, - seed_length = excluded.seed_length + seed_length = excluded.seed_length, + delegation_depth = excluded.delegation_depth `).run( meta.id, meta.version, @@ -268,6 +269,7 @@ export class SessionPersistenceSqlite extends SessionPersistence implements Pers meta.cwd ?? null, meta.parentSession ?? null, meta.seedLength ?? null, + meta.delegationDepth ?? null, ) } } diff --git a/packages/session-persistence/session-persistence-sqlite/src/schema.ts b/packages/session-persistence/session-persistence-sqlite/src/schema.ts index adb23cbb43..4a7f12e759 100644 --- a/packages/session-persistence/session-persistence-sqlite/src/schema.ts +++ b/packages/session-persistence/session-persistence-sqlite/src/schema.ts @@ -15,7 +15,7 @@ import type { SessionEvent, SessionId, SessionHeader, SurfaceOp } from '@deepsee * layout; orthogonal to a session's own `version` (which versions the EVENT * vocabulary, stored per session in the `sessions` row). */ -export const SCHEMA_VERSION = 4 +export const SCHEMA_VERSION = 5 /** * A row of the `sessions` table — the out-of-log metadata ({@link SessionHeader}). @@ -31,6 +31,7 @@ export interface SessionRow { cwd: string | null parent_session: string | null seed_length: number | null + delegation_depth: number | null } /** An `events` table row: one `SessionEvent` mapped 1:1 (`data` is JSON text). */ @@ -83,9 +84,10 @@ export function openDatabase(path: string, journalMode: JournalMode): DatabaseSy id TEXT PRIMARY KEY, version INTEGER NOT NULL, created_at INTEGER NOT NULL, - cwd TEXT, - parent_session TEXT, - seed_length INTEGER + cwd TEXT, + parent_session TEXT, + seed_length INTEGER, + delegation_depth INTEGER ) STRICT `) db.exec(` @@ -116,6 +118,7 @@ export function rowToMeta(row: SessionRow): SessionHeader { ...row.cwd !== null ? { cwd: row.cwd } : {}, ...row.parent_session !== null ? { parentSession: row.parent_session as SessionId } : {}, ...row.seed_length !== null ? { seedLength: row.seed_length } : {}, + ...row.delegation_depth !== null ? { delegationDepth: row.delegation_depth } : {}, } } diff --git a/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts b/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts index f26edfa54d..83d16b3315 100644 --- a/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts +++ b/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts @@ -385,7 +385,7 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => { }) it('exposes the schema version constant', () => { - expect(SCHEMA_VERSION).toBe(4) + expect(SCHEMA_VERSION).toBe(5) }) }) diff --git a/packages/session-persistence/session-persistence/README.md b/packages/session-persistence/session-persistence/README.md index ca04cd1ab8..6d9e1392fe 100644 --- a/packages/session-persistence/session-persistence/README.md +++ b/packages/session-persistence/session-persistence/README.md @@ -2,7 +2,7 @@ The abstract durable session-persistence seam (`ctx.sessionPersistence`). Defines WHAT a persistence backend does — durably store, reload, and list sessions — without saying HOW. Mirrors the `dsh-bash` capability-seam template ([capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract service here, a concrete implementation in a sibling package, consumers that inject the interface. -The persisted unit IS the existing `SessionEvent` (event-sourced model — the log is the single source of truth), so there is no parallel "persisted message" type. Metadata that is NOT replayable conversation state (format version, cwd, lineage, seed boundary) travels separately as `SessionHeader`, owned by `dsh-session` and re-exported here. +The persisted unit IS the existing `SessionEvent` (event-sourced model — the log is the single source of truth), so there is no parallel "persisted message" type. Metadata that is NOT replayable conversation state (format version, cwd, lineage, seed boundary, delegation depth) travels separately as `SessionHeader`, owned by `dsh-session` and re-exported here. ## Service API (`ctx.sessionPersistence`) @@ -51,7 +51,7 @@ Three backends run these suites: an in-memory reference (in `tests/`), `dsh-sess ## Metadata and location types -Re-exported from `dsh-session`: `SessionHeader` (immutable session metadata: `version`, `id`, `createdAt`, `cwd?`, `parentSession?`, `seedLength?`). `SessionLocation` is `{ readonly kind: string; readonly path: string }`; its path is an absolute backend target, not proof that the artifact exists or contains an unflushed turn. +Re-exported from `dsh-session`: `SessionHeader` (immutable session metadata: `version`, `id`, `createdAt`, `cwd?`, `parentSession?`, `seedLength?`, `delegationDepth?`). `SessionLocation` is `{ readonly kind: string; readonly path: string }`; its path is an absolute backend target, not proof that the artifact exists or contains an unflushed turn. ## Model Experience diff --git a/packages/session-persistence/session-persistence/tests/coordinator-contract.ts b/packages/session-persistence/session-persistence/tests/coordinator-contract.ts index defcaefb6d..5ef2ce2fad 100644 --- a/packages/session-persistence/session-persistence/tests/coordinator-contract.ts +++ b/packages/session-persistence/session-persistence/tests/coordinator-contract.ts @@ -106,6 +106,27 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise< } }) + it('round-trips the delegation depth through persistence', async () => { + // A subagent child's recursion budget lives in its header; a reload that + // dropped it would reset the child to top-level and un-bound maxDepth + // (JSONL stores it in the header line; SQLite uses `delegation_depth`). + const fix = await makeFixture() + const { ctx, fiber } = await freshCtx(fix) + try { + const session = ctx.sessions.create(SessionId('delegated-child'), { + meta: { cwd: WORK, parentSession: SessionId('root'), delegationDepth: 2 }, + }) + send(session, oneTurnLog()) + await ctx.parallel('session/flush', session) + + const loaded = await ctx.sessionPersistence.load(SessionId('delegated-child')) + expect(loaded.meta.delegationDepth).toBe(2) + } finally { + await fiber.dispose() + await fix.cleanup() + } + }) + it('source-frozen events cannot be mutated after buffering and persist unchanged', async () => { const fix = await makeFixture() const { ctx, fiber } = await freshCtx(fix) diff --git a/packages/subagent/subagent-inprocess/README.md b/packages/subagent/subagent-inprocess/README.md index 0503db9dc4..d51fc53e5d 100644 --- a/packages/subagent/subagent-inprocess/README.md +++ b/packages/subagent/subagent-inprocess/README.md @@ -8,7 +8,7 @@ This package is the shared run driver for the two in-process providers. Spawn pa The driver follows this sequence: -1. Validate the parent depth and optional absolute `maxDepth`, then derive child depth as parent depth plus one. +1. Validate the parent depth and optional absolute `maxDepth`, then derive child depth as parent depth plus one and persist it in the child session header. 2. Call `parent.ctx.agents.create` directly, passing the required request signal into the factory's creation transaction. 3. During that transaction's unpublished setup window, install the requested persona, tool restriction, and structured-output runtime. 4. Publish the child, retain the returned `AgentHandle`, and drive one task with `child.send(prompt)` followed by `child.whenIdle()`. @@ -26,7 +26,7 @@ After fulfillment, the caller owns the run. Provider-plugin unload does not revo `InProcessRunOptions` is `{ seed?: SessionEvent[] }`. Spawn omits it. Fork supplies a balanced completed-turn prefix and records its length so the result reader never mistakes a seeded parent message for child output. -Depth enforcement is internal to `startInProcessRun`: it reads `AgentOptions.subagentDepth`, treats absence as top-level depth zero, rejects malformed stored values, and reports an attempted child depth above `maxDepth`. An unrepresentable depth above the safe-integer domain is a `RangeError`. +Depth enforcement is internal to `startInProcessRun`: it reads the parent depth via `delegationDepthOf` (the persisted `SessionHeader.delegationDepth` is authoritative; runtime `AgentOptions.subagentDepth` may deepen but never lower it, so a resumed child keeps its budget), treats absence as top-level depth zero, rejects malformed stored values, and reports an attempted child depth above `maxDepth`. An unrepresentable depth above the safe-integer domain is a `RangeError`. The child depth is written to the child header, so it survives persistence and resume. ## Structured output diff --git a/packages/subagent/subagent-inprocess/src/index.ts b/packages/subagent/subagent-inprocess/src/index.ts index 6e62ee5127..f0e9731c9d 100644 --- a/packages/subagent/subagent-inprocess/src/index.ts +++ b/packages/subagent/subagent-inprocess/src/index.ts @@ -12,7 +12,7 @@ import type { Context } from 'cordis' import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent' import { SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session' import type { ContentBlock } from '@deepseek-ai/dsh-llm' -import { assertSubagentMaxDepth } from '@deepseek-ai/dsh-subagent' +import { assertSubagentMaxDepth, delegationDepthOf } from '@deepseek-ai/dsh-subagent' import type { SubagentResult, SubagentRun, SubagentStartRequest, SubagentStopReason } from '@deepseek-ai/dsh-subagent' import { attachStructuredRuntime, @@ -24,27 +24,6 @@ export { STRUCTURED_OUTPUT_INSTRUCTION, } from './structured.ts' -declare module '@deepseek-ai/dsh-agent' { - interface AgentOptions { - /** Delegation depth: zero for a top-level agent and parent depth + 1 for a child. */ - subagentDepth?: number - } -} - -/** - * Read an agent's delegation depth, treating absence as top-level depth zero. - * @param agent - the agent whose options carry the depth. - * @returns its non-negative safe-integer depth. - */ -function depthOf(agent: Agent): number { - const depth = agent.options.subagentDepth - if (depth === undefined) return 0 - if (!Number.isSafeInteger(depth) || depth < 0 || Object.is(depth, -0)) { - throw new TypeError('agent subagentDepth must be a non-negative safe integer') - } - return depth -} - /** Thrown when starting a child would exceed the requested depth cap. */ class SubagentDepthError extends Error { constructor(public readonly attemptedDepth: number, public readonly maxDepth: number) { @@ -96,7 +75,7 @@ export async function startInProcessRun( assertSubagentMaxDepth(request.maxDepth) if (request.signal.aborted) throw prePublicationAbort() const parent = request.parent - const childDepth = depthOf(parent) + 1 + const childDepth = delegationDepthOf(parent) + 1 if (!Number.isSafeInteger(childDepth)) { throw new RangeError('subagent child depth exceeds the safe-integer range') } @@ -133,6 +112,8 @@ export async function startInProcessRun( meta: { ...parentHeader.cwd !== undefined ? { cwd: parentHeader.cwd } : {}, parentSession: parentHeader.id, + // Durable: the recursion budget must survive persistence and resume. + delegationDepth: childDepth, ...seedLength > 0 ? { seedLength } : {}, }, ...options.seed !== undefined ? { seed: options.seed } : {}, diff --git a/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts b/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts index 13029ef3e7..e82c1ad028 100644 --- a/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts +++ b/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts @@ -58,6 +58,43 @@ describe('startInProcessRun', () => { await run.dispose() }) + it('persists the child depth in its session header', async () => { + const { ctx, parent } = await setup([textResponse('child answer')]) + const run = await startInProcessRun(request(parent), {}) + await run.result + // The recursion budget is durable session data, not only runtime options — + // a depth that lived only in AgentOptions would reset to 0 on resume. + expect(ctx.agents.get(run.id)!.session.header.delegationDepth).toBe(1) + await run.dispose() + }) + + it('counts a RESUMED child by its persisted header depth, not the absent runtime depth', async () => { + // Resume rebuilds runtime options, so the durable header must keep this + // depth-1 child from delegating as though it were top-level. + const { ctx } = await setup([textResponse('unused')]) + const resumed = (await ctx.agents.create({ + sessionId: SessionId('resumed-child'), + meta: { parentSession: SessionId('root'), delegationDepth: 1 }, + agentOptions: { provider: 'mock', model: 'mock' }, + signal: new AbortController().signal, + })).agent + await expect(startInProcessRun({ ...request(resumed), maxDepth: 1 }, {})) + .rejects.toMatchObject({ name: 'SubagentDepthError', attemptedDepth: 2, maxDepth: 1 }) + }) + + it('lets runtime options deepen but never lower the persisted depth', async () => { + const { ctx } = await setup([textResponse('unused')]) + const parent = (await ctx.agents.create({ + sessionId: SessionId('deep-parent'), + meta: { delegationDepth: 2 }, + agentOptions: { provider: 'mock', model: 'mock', subagentDepth: 1 }, + signal: new AbortController().signal, + })).agent + // Persisted 2 vs runtime 1: the child is depth 3, so maxDepth 2 rejects. + await expect(startInProcessRun({ ...request(parent), maxDepth: 2 }, {})) + .rejects.toMatchObject({ name: 'SubagentDepthError', attemptedDepth: 3, maxDepth: 2 }) + }) + it('rejects invalid and exceeded depth before publication', async () => { const { parent } = await setup([]) await expect(startInProcessRun({ ...request(parent), maxDepth: -1 }, {})) @@ -65,11 +102,11 @@ describe('startInProcessRun', () => { await expect(startInProcessRun({ ...request(parent), maxDepth: 0 }, {})) .rejects.toMatchObject({ name: 'SubagentDepthError' }) for (const value of [Number.NaN, 1.5, -1, -0, Number.MAX_SAFE_INTEGER + 1]) { - const malformed = { options: { subagentDepth: value } } as unknown as Agent + const malformed = { options: { subagentDepth: value }, session: { header: {} } } as unknown as Agent await expect(startInProcessRun(request(malformed), {})) .rejects.toThrow('agent subagentDepth must be a non-negative safe integer') } - const maxParent = { options: { subagentDepth: Number.MAX_SAFE_INTEGER } } as unknown as Agent + const maxParent = { options: { subagentDepth: Number.MAX_SAFE_INTEGER }, session: { header: {} } } as unknown as Agent await expect(startInProcessRun(request(maxParent), {})).rejects.toBeInstanceOf(RangeError) }) diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 5097ad914a..105af77450 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -40,6 +40,10 @@ Start-time features are advertised in `provider.capabilities` because the servic - `toolFilter` — apply the requested child tool restriction. - `persona` — apply a per-child persona. +## Delegation depth + +The seam owns the depth vocabulary shared by implementations and consumers: the `AgentOptions.subagentDepth` declaration, `assertSubagentMaxDepth`, and `delegationDepthOf(agent)`. The persisted `SessionHeader.delegationDepth` is authoritative and monotone — runtime options may deepen the count but never lower it, so a resumed child cannot be re-counted as top-level. + Runtime features are optional methods on `SubagentRun`: `sendMessage?` steers a live child, while `resume?` asynchronously creates a continuation run. Method presence is the capability check. `inheritsParentContext` is descriptive rather than enforceable. It says only whether the child sees completed parent conversation history (`fork` does; `spawn` and ACP do not), not whether it inherits tools, services, or authority. diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index 19779f9137..00655f1f51 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -57,6 +57,33 @@ export type { SubagentStopReasonMap, } from './types.ts' +declare module '@deepseek-ai/dsh-agent' { + interface AgentOptions { + /** Delegation depth: zero for a top-level agent and parent depth + 1 for a child. */ + subagentDepth?: number + } +} + +/** + * Read an agent's delegation depth, treating absence as top-level depth zero. + * The persisted session header is authoritative and monotone: runtime + * `AgentOptions.subagentDepth` may DEEPEN the count but can never lower it — + * a resumed child arrives with fresh options, and counting it from zero would + * let it delegate as if it were top-level. + * @param agent - the agent whose header and options carry the depth. + * @returns its non-negative safe-integer depth. + * @throws if the runtime `AgentOptions.subagentDepth` is not a non-negative safe integer. + */ +export function delegationDepthOf(agent: Agent): number { + const runtime = agent.options.subagentDepth + if (runtime !== undefined && (!Number.isSafeInteger(runtime) || runtime < 0 || Object.is(runtime, -0))) { + throw new TypeError('agent subagentDepth must be a non-negative safe integer') + } + // The header value was validated at the session boundary (creation and + // persistence load both construct through the store). + return Math.max(agent.session.header.delegationDepth ?? 0, runtime ?? 0) +} + /** * Reject a recursion cap that cannot represent an exact delegation depth. * @param maxDepth - the optional runtime value to validate. diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 6355208a4d..e2af5ac552 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -22,7 +22,7 @@ With `run_in_background: true`, the tool registers the parent-owned task before | `agentOptions` | Default child options, currently including `model`. | | `persona` | Per-child persona; requires provider `persona` capability. | | `toolFilter` | Per-child global-tool restriction; requires `toolFilter` capability. | -| `maxDepth` | Absolute delegation-depth cap; requires `depthLimit` capability. | +| `maxDepth` | Absolute delegation-depth cap, default `3` (`0` forbids delegation); a numeric cap requires the `depthLimit` capability and fails the mount without it. `'provider-managed'` sends no cap for an out-of-process provider whose budget belongs to the child harness. The tool stays visible at the cap; each attempted start checks the calling agent's current depth and returns an errored tool result when rejected. | ## Concurrency diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index f46f3dda2c..bcd28c6ab1 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -45,8 +45,7 @@ export interface Config { /** * Tool filter applied to every child. Filtered tools disappear from its * prompt and reject execution. Requires the provider's `toolFilter` - * capability; unknown names fail startup. Children otherwise see this tool, - * so deny it or set `maxDepth` to bound recursion. + * capability; unknown names fail startup. */ toolFilter?: { /** Global tool names the child keeps; everything else is removed. */ @@ -55,10 +54,15 @@ export interface Config { deny?: string[] } /** - * Maximum child depth. Requires the provider's `depthLimit` capability and a - * non-negative safe integer. Omission is unbounded. + * Maximum child depth: a non-negative safe integer (default `3`; `0` forbids + * delegation entirely), or `'provider-managed'` to send no cap. A numeric cap + * requires the provider's `depthLimit` capability (mount fails loud + * otherwise). The provider checks the calling agent's current depth at every + * start; the tool remains model-visible so runtime policy owns rejection. + * `'provider-managed'` is for an out-of-process provider (ACP) whose + * recursion budget belongs to the child harness's own deployment. */ - maxDepth?: number + maxDepth?: number | 'provider-managed' } export const Config: z = z.object({ @@ -76,7 +80,7 @@ export const Config: z = z.object({ allow: z.array(z.string()).default(undefined as unknown as string[]), deny: z.array(z.string()).default(undefined as unknown as string[]), }).default(undefined as unknown as { allow: string[]; deny: string[] }), - maxDepth: z.natural().max(Number.MAX_SAFE_INTEGER), + maxDepth: z.union([z.natural().max(Number.MAX_SAFE_INTEGER), z.const('provider-managed' as const)]).default(3), }) /** @@ -195,6 +199,7 @@ function providerWording(inheritsConversation: boolean): { description: string; } function startRequest(config: Config, prompt: string, parent: Agent, signal: AbortSignal): SubagentStartRequest { + const maxDepth = typeof config.maxDepth === 'number' ? config.maxDepth : undefined return { prompt: [{ type: 'text', text: prompt }], parent, @@ -202,7 +207,7 @@ function startRequest(config: Config, prompt: string, parent: Agent, signal: Abo ...config.agentOptions !== undefined ? { agentOptions: config.agentOptions } : {}, ...config.persona !== undefined ? { persona: config.persona } : {}, ...config.toolFilter !== undefined ? { toolFilter: config.toolFilter } : {}, - ...config.maxDepth !== undefined ? { maxDepth: config.maxDepth } : {}, + ...maxDepth !== undefined ? { maxDepth } : {}, } } @@ -218,8 +223,9 @@ async function settleStart(start: Promise, signal: AbortSignal): Pr } export function apply(ctx: Context, config: Config): void { - // Direct apply() bypasses Schemastery's numeric constraints. - assertSubagentMaxDepth(config.maxDepth) + // Direct apply() bypasses Schemastery's numeric constraints. A direct-apply + // omission stays capless (the schema default only runs through the loader). + if (config.maxDepth !== 'provider-managed') assertSubagentMaxDepth(config.maxDepth) // Reject an empty explicit filter at load instead of failing every delegation. if (config.toolFilter !== undefined && config.toolFilter.allow === undefined && config.toolFilter.deny === undefined) { throw new Error('tool-subagent: `toolFilter` is configured but names neither `allow` nor `deny` — remove the key or fill the filter') @@ -228,6 +234,15 @@ export function apply(ctx: Context, config: Config): void { // can change provider availability while this fiber remains active. let disposeTool: (() => void) | undefined const mount = (provider: SubagentProvider): void => { + // A numeric cap the provider cannot enforce is a misconfiguration — fail at + // mount (the earliest point the provider's capabilities are known), not on + // the first delegation. + if (typeof config.maxDepth === 'number' && !provider.capabilities.depthLimit) { + throw new Error( + `tool-subagent: provider "${provider.name}" cannot enforce maxDepth (no depthLimit capability) — ` + + 'set maxDepth: \'provider-managed\' to leave the recursion budget to the provider', + ) + } const wording = providerWording(provider.inheritsParentContext) const backgroundEnabled = config.enableRunInBackground !== false disposeTool = ctx.tools.register(defineTool({ diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index d88367ced5..9e4d68c08c 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -7,6 +7,7 @@ import ToolRegistry from '@deepseek-ai/dsh-tools' import { type Agent } from '@deepseek-ai/dsh-agent' import AgentRegistry from '@deepseek-ai/dsh-agent' import SubagentService from '@deepseek-ai/dsh-subagent' +import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent' import TaskService from '@deepseek-ai/dsh-tasks' import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks' import * as mock from './scripted-provider.ts' @@ -22,7 +23,7 @@ import { SessionId } from '@deepseek-ai/dsh-session' * shipping code path. */ -/** A minimal parent Agent — the tool reads `agent.id` for `parent`. */ +/** A minimal parent Agent passed through to the provider request. */ function fakeAgent(id = 'parent-1'): Agent { return { id: SessionId(id) } as unknown as Agent } @@ -85,7 +86,7 @@ describe('dsh-tool-subagent', () => { // Schema omission is advertising, not enforcement: the arg validator // allows undeclared keys, so the opt-out must also hold in execute(). const ctx = await setup({ provider: 'mock', enableRunInBackground: false }) - const parent = { id: SessionId('sess-off'), inject: () => {}, session: { header: { version: 0, id: 'sess-off', createdAt: 0 } } } as unknown as Agent + const parent = { id: SessionId('sess-off'), inject: () => {}, options: {}, session: { header: { version: 0, id: 'sess-off', createdAt: 0 } } } as unknown as Agent const forced = await callSubagent(ctx, { description: 'd', prompt: 'p', run_in_background: true }, { agent: parent }) expect(forced.isError).toBe(true) @@ -162,7 +163,7 @@ describe('dsh-tool-subagent', () => { dispose: async () => {}, }), }) - await ctx.plugin(tool, { provider: 'weird' }) + await ctx.plugin(tool, { provider: 'weird', maxDepth: 'provider-managed' }) const result = await callSubagent(ctx, { description: 'd', prompt: 'p' }) expect(result.isError).toBe(true) @@ -191,7 +192,7 @@ describe('dsh-tool-subagent', () => { } }, }) - await ctx.plugin(tool, { provider: 'capture', agentOptions: { model: 'child-model' } }) + await ctx.plugin(tool, { provider: 'capture', agentOptions: { model: 'child-model' }, maxDepth: 'provider-managed' }) await callSubagent(ctx, { description: 'd', prompt: 'p' }) expect(seen?.agentOptions).toEqual({ model: 'child-model' }) @@ -348,7 +349,7 @@ describe('dsh-tool-subagent', () => { dispose: async () => void disposed(), }), }) - await ctx.plugin(tool, { provider: 'spy' }) + await ctx.plugin(tool, { provider: 'spy', maxDepth: 'provider-managed' }) await callSubagent(ctx, { description: 'd', prompt: 'p' }) expect(disposed).toHaveBeenCalledTimes(1) @@ -371,7 +372,7 @@ describe('dsh-tool-subagent', () => { dispose: async () => void disposed(), }), }) - await ctx.plugin(tool, { provider: 'spy' }) + await ctx.plugin(tool, { provider: 'spy', maxDepth: 'provider-managed' }) const result = await callSubagent(ctx, { description: 'd', prompt: 'p' }) expect(result.isError).toBe(true) @@ -404,7 +405,7 @@ describe('dsh-tool-subagent', () => { } }, }) - await ctx.plugin(tool, { provider: 'spy' }) + await ctx.plugin(tool, { provider: 'spy', maxDepth: 'provider-managed' }) const controller = new AbortController() const pending = callSubagent(ctx, { description: 'd', prompt: 'p' }, { signal: controller.signal }) @@ -432,7 +433,7 @@ describe('dsh-tool-subagent', () => { throw new Error('start aborted') }, }) - await ctx.plugin(tool, { provider: 'spy' }) + await ctx.plugin(tool, { provider: 'spy', maxDepth: 'provider-managed' }) const controller = new AbortController() controller.abort() // already aborted BEFORE the tool runs @@ -511,7 +512,6 @@ describe('dsh-tool-subagent', () => { }) it.each([ - { label: 'null', value: null as unknown as number }, { label: 'a string', value: '1' as unknown as number }, { label: 'NaN', value: Number.NaN }, { label: 'positive infinity', value: Number.POSITIVE_INFINITY }, @@ -555,7 +555,7 @@ describe('dsh-tool-subagent', () => { } }, }) - await ctx.plugin(tool, { provider: 'capture3', toolFilter: { deny: ['subagent'] } }) + await ctx.plugin(tool, { provider: 'capture3', toolFilter: { deny: ['subagent'] }, maxDepth: 'provider-managed' }) await callSubagent(ctx, { description: 'd', prompt: 'p' }) expect(seen?.toolFilter).toEqual({ deny: ['subagent'] }) expect(seen?.toolFilter).not.toHaveProperty('allow') @@ -585,7 +585,7 @@ describe('dsh-tool-subagent', () => { } }, }) - await ctx.plugin(tool, { provider: 'capture4' }) + await ctx.plugin(tool, { provider: 'capture4', maxDepth: 'provider-managed' }) await callSubagent(ctx, { description: 'd', prompt: 'p' }) expect(seen).toBeDefined() expect(seen).not.toHaveProperty('agentOptions') @@ -616,6 +616,7 @@ describe('dsh-tool-subagent background mode', () => { id, ctx: scopeFiber.ctx, inject, + options: {}, session: { id, header: { version: 0, id, createdAt: 0 } }, } as unknown as Agent ctx.agents.register(agent) @@ -846,6 +847,7 @@ describe('background preflight failure (no orphaned child, by construction)', () id, ctx: scopeFiber.ctx, inject: () => {}, + options: {}, session: { id, header: { version: 0, id, createdAt: 0 } }, } as unknown as Agent ctx.agents.register(parent) @@ -879,3 +881,85 @@ describe('background preflight failure (no orphaned child, by construction)', () expect(starts).toBe(0) }) }) + +describe('depth budget configuration', () => { + /** Mount the tool over a request-capturing provider with full capabilities. */ + async function captureSetup(config: Omit = {}) { + const requests: SubagentStartRequest[] = [] + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(SubagentService) + ctx.subagents.registerProvider({ + name: 'capture', + capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, + inheritsParentContext: false, + start: async (request) => { + requests.push(request) + return { + id: SessionId(`capture-child-${requests.length}`), + localAgent: undefined, + result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }), + dispose: async () => {}, + } + }, + }) + await ctx.plugin(tool, { provider: 'capture', ...config }) + return { ctx, requests } + } + + it('defaults maxDepth to 3 and forwards it in the start request', async () => { + const { ctx, requests } = await captureSetup() + await callSubagent(ctx, { description: 'd', prompt: 'p' }) + expect(requests[0]?.maxDepth).toBe(3) + expect(requests[0]?.toolFilter).toBeUndefined() + }) + + it('forwards an explicit tool filter unchanged instead of encoding the depth policy into it', async () => { + const { ctx, requests } = await captureSetup({ toolFilter: { deny: ['dangerous'] }, maxDepth: 0 }) + await callSubagent(ctx, { description: 'd', prompt: 'p' }) + expect(requests[0]?.maxDepth).toBe(0) + expect(requests[0]?.toolFilter).toEqual({ deny: ['dangerous'] }) + }) + + it('rejects a numeric maxDepth on a provider without the depthLimit capability at mount', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(SubagentService) + ctx.subagents.registerProvider({ + name: 'no-depth', + capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + inheritsParentContext: false, + start: async () => { throw new Error('unreachable') }, + }) + await expect(ctx.plugin(tool, { provider: 'no-depth' })) + .rejects.toThrow(/provider-managed/) + }) + + it("'provider-managed' omits the cap so a capability-less provider mounts and starts", async () => { + const requests: SubagentStartRequest[] = [] + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(SubagentService) + ctx.subagents.registerProvider({ + name: 'external', + capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + inheritsParentContext: false, + start: async (request) => { + requests.push(request) + return { + id: SessionId('external-child'), + localAgent: undefined, + result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }), + dispose: async () => {}, + } + }, + }) + await ctx.plugin(tool, { provider: 'external', maxDepth: 'provider-managed' }) + await callSubagent(ctx, { description: 'd', prompt: 'p' }) + expect(requests[0]?.maxDepth).toBeUndefined() + expect(requests[0]?.toolFilter).toBeUndefined() + }) +}) diff --git a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/behavior.json b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/behavior.json index d44a3a9698..fd06978be1 100644 --- a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/behavior.json +++ b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/behavior.json @@ -2,11 +2,11 @@ "prompt": "respond", "logs": [ { "file": "b/parent.jsonl", "lines": [ - { "type": "session", "id": "{{SID}}", "createdAt": 700, "cwd": "{{CWD}}" }, + { "type": "session", "id": "{{SID}}", "createdAt": 700, "cwd": "{{CWD}}", "delegationDepth": 0 }, { "type": "request/header", "seq": 0, "time": 3, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "initial" } } ]}, { "file": "b/child.jsonl", "lines": [ - { "type": "session", "id": "abababab-cdcd-4efe-8ada-badabadabada", "createdAt": 800, "cwd": "{{CWD}}", "parentSession": "{{SID}}" }, + { "type": "session", "id": "abababab-cdcd-4efe-8ada-badabadabada", "createdAt": 800, "cwd": "{{CWD}}", "parentSession": "{{SID}}", "delegationDepth": 1 }, { "type": "request/header", "seq": 0, "time": 2, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "initial" } } ]} ] diff --git a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/session.1.jsonl b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/session.1.jsonl index 1caf2610b3..4fa81014ae 100644 --- a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/session.1.jsonl +++ b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/session.1.jsonl @@ -1,2 +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":"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","delegationDepth":1} {"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/fixtures/record-suite/rec-child/session.jsonl b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/session.jsonl index a2beac360d..e972a78d8e 100644 --- a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/session.jsonl +++ b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-child/session.jsonl @@ -1,2 +1,2 @@ -{"type":"session","id":"f6fa7fcf-dd9c-4b39-8815-b25ddcebfd88","createdAt":700,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-KBQJbW"} +{"type":"session","id":"f6fa7fcf-dd9c-4b39-8815-b25ddcebfd88","createdAt":700,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-KBQJbW","delegationDepth":0} {"type":"request/header","seq":0,"time":3,"data":{"header":{"config":{"model":"fake"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} diff --git a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/behavior.json b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/behavior.json index a24e30d80a..b0ed5f1a3f 100644 --- a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/behavior.json +++ b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/behavior.json @@ -3,7 +3,7 @@ "logs": [{ "file": "b/main.jsonl", "lines": [ - { "type": "session", "id": "{{SID}}", "createdAt": 600, "cwd": "{{CWD}}" }, + { "type": "session", "id": "{{SID}}", "createdAt": 600, "cwd": "{{CWD}}", "delegationDepth": 0 }, { "type": "request/header", "seq": 0, "time": 4, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "initial" } } ] }] diff --git a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/session.jsonl b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/session.jsonl index e9dd3fb16c..40bb4d37dc 100644 --- a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/session.jsonl +++ b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-pin/session.jsonl @@ -1,2 +1,2 @@ -{"type":"session","id":"ccdc749f-56f3-4267-9750-598b5c60b7b2","createdAt":600,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-nOQ4Gy"} +{"type":"session","id":"ccdc749f-56f3-4267-9750-598b5c60b7b2","createdAt":600,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-nOQ4Gy","delegationDepth":0} {"type":"request/header","seq":0,"time":4,"data":{"header":{"config":{"model":"fake"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} diff --git a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-skip/session.jsonl b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-skip/session.jsonl index 104f2a0df2..035d1353b7 100644 --- a/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-skip/session.jsonl +++ b/packages/support/acp-snapshot/tests/fixtures/record-suite/rec-skip/session.jsonl @@ -1 +1 @@ -{"type":"session","id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} diff --git a/packages/support/acp-snapshot/tests/fixtures/suite/authored-error/behavior.json b/packages/support/acp-snapshot/tests/fixtures/suite/authored-error/behavior.json index 808d9672b9..991de99fd6 100644 --- a/packages/support/acp-snapshot/tests/fixtures/suite/authored-error/behavior.json +++ b/packages/support/acp-snapshot/tests/fixtures/suite/authored-error/behavior.json @@ -3,7 +3,7 @@ "logs": [{ "file": "b/main.jsonl", "lines": [ - { "type": "session", "id": "{{SID}}", "createdAt": 500, "cwd": "{{CWD}}" }, + { "type": "session", "id": "{{SID}}", "createdAt": 500, "cwd": "{{CWD}}", "delegationDepth": 0 }, { "type": "turn/end", "seq": 1, "time": 9, "data": { "error": "model exploded" } } ] }] diff --git a/packages/support/acp-snapshot/tests/fixtures/suite/authored-error/session.jsonl b/packages/support/acp-snapshot/tests/fixtures/suite/authored-error/session.jsonl index 36991a214e..a202c7141d 100644 --- a/packages/support/acp-snapshot/tests/fixtures/suite/authored-error/session.jsonl +++ b/packages/support/acp-snapshot/tests/fixtures/suite/authored-error/session.jsonl @@ -1,2 +1,2 @@ -{"type":"session","id":"44444444-3333-4222-8111-000000000000","createdAt":17,"cwd":"/rec/authored-cwd"} +{"type":"session","id":"44444444-3333-4222-8111-000000000000","createdAt":17,"cwd":"/rec/authored-cwd","delegationDepth":0} {"type":"turn/end","seq":1,"time":17,"data":{"error":"model exploded"}} diff --git a/packages/support/acp-snapshot/tests/fixtures/suite/blocked-log/behavior.json b/packages/support/acp-snapshot/tests/fixtures/suite/blocked-log/behavior.json index e0a438297d..209159da7d 100644 --- a/packages/support/acp-snapshot/tests/fixtures/suite/blocked-log/behavior.json +++ b/packages/support/acp-snapshot/tests/fixtures/suite/blocked-log/behavior.json @@ -3,7 +3,7 @@ "logs": [{ "file": "b/main.jsonl", "lines": [ - { "type": "session", "id": "{{SID}}", "createdAt": 400, "cwd": "{{CWD}}" }, + { "type": "session", "id": "{{SID}}", "createdAt": 400, "cwd": "{{CWD}}", "delegationDepth": 0 }, { "type": "hook/result", "seq": 1, "time": 8, "data": { "decision": "block", "durationMs": 37 } } ] }] diff --git a/packages/support/acp-snapshot/tests/fixtures/suite/blocked-log/session.jsonl b/packages/support/acp-snapshot/tests/fixtures/suite/blocked-log/session.jsonl index 6d8474812d..75eb18fa71 100644 --- a/packages/support/acp-snapshot/tests/fixtures/suite/blocked-log/session.jsonl +++ b/packages/support/acp-snapshot/tests/fixtures/suite/blocked-log/session.jsonl @@ -1,2 +1,2 @@ -{"type":"session","id":"99999999-8888-4777-8666-555555555555","createdAt":13,"cwd":"/rec/blocked-cwd"} +{"type":"session","id":"99999999-8888-4777-8666-555555555555","createdAt":13,"cwd":"/rec/blocked-cwd","delegationDepth":0} {"type":"hook/result","seq":1,"time":13,"data":{"decision":"block","durationMs":99}} diff --git a/packages/support/acp-snapshot/tests/fixtures/suite/no-model/session.jsonl b/packages/support/acp-snapshot/tests/fixtures/suite/no-model/session.jsonl index 104f2a0df2..035d1353b7 100644 --- a/packages/support/acp-snapshot/tests/fixtures/suite/no-model/session.jsonl +++ b/packages/support/acp-snapshot/tests/fixtures/suite/no-model/session.jsonl @@ -1 +1 @@ -{"type":"session","id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}"} +{"type":"session","id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0} diff --git a/packages/support/acp-snapshot/tests/fixtures/suite/pin-turn/behavior.json b/packages/support/acp-snapshot/tests/fixtures/suite/pin-turn/behavior.json index 4b63893a62..ad4c368e49 100644 --- a/packages/support/acp-snapshot/tests/fixtures/suite/pin-turn/behavior.json +++ b/packages/support/acp-snapshot/tests/fixtures/suite/pin-turn/behavior.json @@ -3,7 +3,7 @@ "logs": [{ "file": "b/main.jsonl", "lines": [ - { "type": "session", "id": "{{SID}}", "createdAt": 100, "cwd": "{{CWD}}" }, + { "type": "session", "id": "{{SID}}", "createdAt": 100, "cwd": "{{CWD}}", "delegationDepth": 0 }, { "type": "request/header", "seq": 0, "time": 100, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "initial" } }, { "type": "request/header", "seq": 1, "time": 100, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT\n\nNEW PROMPT LINE", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "change" } }, { "type": "turn/start", "seq": 2, "time": 100, "data": { "turn": 1 } } diff --git a/packages/support/acp-snapshot/tests/fixtures/suite/pin-turn/session.jsonl b/packages/support/acp-snapshot/tests/fixtures/suite/pin-turn/session.jsonl index 54b64b59a3..693e728d2c 100644 --- a/packages/support/acp-snapshot/tests/fixtures/suite/pin-turn/session.jsonl +++ b/packages/support/acp-snapshot/tests/fixtures/suite/pin-turn/session.jsonl @@ -1,4 +1,4 @@ -{"type":"session","id":"12121212-3434-4545-8686-787878787878","createdAt":7,"cwd":"/rec/pin-cwd"} +{"type":"session","id":"12121212-3434-4545-8686-787878787878","createdAt":7,"cwd":"/rec/pin-cwd","delegationDepth":0} {"type":"request/header","seq":0,"time":7,"data":{"header":{"config":{"model":"fake"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/header","seq":1,"time":7,"data":{"header":{"config":{"model":"fake"},"system":"{{system}}","tools":"{{tools}}"},"reason":"change"}} {"type":"turn/start","seq":2,"time":7,"data":{"turn":1}} diff --git a/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/behavior.json b/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/behavior.json index d5cbbf9d28..8903d0360e 100644 --- a/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/behavior.json +++ b/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/behavior.json @@ -3,12 +3,12 @@ "echoWorkspace": true, "logs": [ { "file": "b/parent.jsonl", "lines": [ - { "type": "session", "id": "{{SID}}", "createdAt": 200, "cwd": "{{CWD}}" }, + { "type": "session", "id": "{{SID}}", "createdAt": 200, "cwd": "{{CWD}}", "delegationDepth": 0 }, { "type": "request/header", "seq": 0, "time": 5, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "initial" } }, { "type": "assistant/chunk", "seq": 1, "time": 5, "data": { "turn": 1, "step": 1, "chunk": { "type": "text-delta", "index": 0, "text": "hi" } } } ]}, { "file": "b/child.jsonl", "lines": [ - { "type": "session", "id": "eeeeeeee-1111-4222-8333-444444444444", "createdAt": 300, "cwd": "{{CWD}}", "parentSession": "{{SID}}" }, + { "type": "session", "id": "eeeeeeee-1111-4222-8333-444444444444", "createdAt": 300, "cwd": "{{CWD}}", "parentSession": "{{SID}}", "delegationDepth": 1 }, { "type": "request/header", "seq": 0, "time": 6, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "initial" } } ]} ] diff --git a/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/session.1.jsonl b/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/session.1.jsonl index a844f891fc..5fb01dbe8a 100644 --- a/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/session.1.jsonl +++ b/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/session.1.jsonl @@ -1,2 +1,2 @@ -{"type":"session","id":"eeeeeeee-1111-4222-8333-444444444444","createdAt":12,"cwd":"/rec/plain-cwd","parentSession":"56565656-7878-4989-8a9a-9b9b9b9b9b9b"} +{"type":"session","id":"eeeeeeee-1111-4222-8333-444444444444","createdAt":12,"cwd":"/rec/plain-cwd","parentSession":"56565656-7878-4989-8a9a-9b9b9b9b9b9b","delegationDepth":1} {"type":"request/header","seq":0,"time":12,"data":{"header":{"config":{"model":"fake"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} diff --git a/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/session.jsonl b/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/session.jsonl index 744998f959..6bdf784ce2 100644 --- a/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/session.jsonl +++ b/packages/support/acp-snapshot/tests/fixtures/suite/plain-turn/session.jsonl @@ -1,3 +1,3 @@ -{"type":"session","id":"56565656-7878-4989-8a9a-9b9b9b9b9b9b","createdAt":11,"cwd":"/rec/plain-cwd"} +{"type":"session","id":"56565656-7878-4989-8a9a-9b9b9b9b9b9b","createdAt":11,"cwd":"/rec/plain-cwd","delegationDepth":0} {"type":"request/header","seq":0,"time":11,"data":{"header":{"config":{"model":"fake"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"assistant/chunk","seq":1,"time":11,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"hi"}}} diff --git a/packages/support/acp-snapshot/tests/suite.spec.ts b/packages/support/acp-snapshot/tests/suite.spec.ts index ac6f944a4a..6fbb06bb9d 100644 --- a/packages/support/acp-snapshot/tests/suite.spec.ts +++ b/packages/support/acp-snapshot/tests/suite.spec.ts @@ -90,12 +90,12 @@ function staleRefreshFixtures(dir: string): void { writeFileSync(plainBehaviorFile, `${JSON.stringify(plainBehavior, null, 2)}\n`) writeFileSync(join(dir, 'blocked-log', 'session.jsonl'), [ - '{"type":"session","id":"99999999-8888-4777-8666-555555555555","createdAt":13,"cwd":"/rec/blocked-cwd"}', + '{"type":"session","id":"99999999-8888-4777-8666-555555555555","createdAt":13,"cwd":"/rec/blocked-cwd","delegationDepth":0}', '{"type":"hook/result","seq":1,"time":13,"data":{"decision":"stale","durationMs":99}}', '', ].join('\n')) writeFileSync(join(dir, 'authored-error', 'session.jsonl'), [ - '{"type":"session","id":"77777777-8888-4777-8666-555555555555","createdAt":13,"cwd":"/rec/error-cwd"}', + '{"type":"session","id":"77777777-8888-4777-8666-555555555555","createdAt":13,"cwd":"/rec/error-cwd","delegationDepth":0}', '{"type":"turn/end","seq":1,"time":9,"data":{"error":"stale"}}', '', ].join('\n')) diff --git a/packages/support/invariants/README.md b/packages/support/invariants/README.md index 139caf8584..aef85dbb02 100644 --- a/packages/support/invariants/README.md +++ b/packages/support/invariants/README.md @@ -4,7 +4,7 @@ Runtime event-contract assertions intended for development diagnostics. This pur The plugin has no environment guard: it is active wherever it is registered. The default [`dsh-agent-spine-demo`](../../examples/agent-spine-demo/README.md) bundle mounts it unconditionally; a custom composition can omit it when the runtime cost is undesirable. It doubles as executable documentation of the event taxonomy — the assertions *are* the contract. -Session itself owns immutable, surface-valid log storage in every composition: it takes one lossless JSON snapshot of each candidate, validates the complete surface transition, deep-freezes the accepted record, and exposes the log through immutable array snapshots. The invariants plugin checks the remaining cross-record and cross-seam rules that Session does not own. +Session itself owns immutable, surface-valid log storage in every composition: it takes one lossless JSON snapshot of each candidate, validates complete provenance and positional replacement, restricts `tool/result` replacement to one current result's `content`, deep-freezes the accepted record, and exposes the log through immutable array snapshots. The invariants plugin checks the remaining cross-record and cross-seam rules that Session does not own. Session-log assertions run during Cordis `internal/dispatch`, while `Session.append()` is resolving the `session/event` callback snapshot but before it pushes the candidate into the log. A valid transition is staged by exact event identity and applied to the live trace only when that same committed event reaches the plugin's contained post-commit listener. A later internal dispatch check can therefore veto without advancing either the log or the invariant trace, while ordinary `session/event` observer failures remain observe-only. @@ -31,8 +31,7 @@ Session log (per session): - **turns pair and nest** — `turn/start` opens a turn, `turn/end` closes the matching one; no overlapping turns. - **steps nest in turns** — `step/start` opens a step in the open turn; `step/end` closes the matching step. - **chunks belong to an open step** — `step/start` precedes its `assistant/chunk`s. -- **a `tool/result` needs a prior `tool/call`** — but NOT the converse: a `tool/call` may have no result (a thrown tool-execution pipeline step ends the turn with no `tool/result`, which is legal). -- **provenance sources are valid and unambiguous** — `sourceEventSeqs` contains unique earlier known seqs; only `assistant/message` may carry an explicit empty list, which denotes a known empty provider stream rather than absent legacy provenance. +- **an appended `tool/result` needs a prior `tool/call`** — fresh `surfaceOp: 'append'` results name the open step and consume its pending call. A Session-validated replacement is a turn-enclosed rewrite, not another execution. A `tool/call` may still have no result when the execution pipeline throws. Agent status (per agent): diff --git a/packages/support/invariants/src/index.ts b/packages/support/invariants/src/index.ts index deabeeea17..91b4da4566 100644 --- a/packages/support/invariants/src/index.ts +++ b/packages/support/invariants/src/index.ts @@ -151,6 +151,16 @@ function validateEvent(trace: SessionTrace, event: SessionEvent): SessionTraceTr break } case 'tool/result': { + // Session has already validated a provenance-backed content rewrite. + // It is durable turn work, not a second execution of the original call. + if (event.surfaceOp !== 'append') { + if (trace.openTurn === null) { + throw new InvariantError( + 'tool/result surface replacement appended outside any open turn', + ) + } + break + } requireOpenStep(trace, 'tool/result', event.data.turn, event.data.step) // A result needs a prior matching call in the same step. (The converse // does NOT hold: a call may have no result — a throwing tool-execution diff --git a/packages/support/invariants/tests/invariants.spec.ts b/packages/support/invariants/tests/invariants.spec.ts index 94f3dcc360..4f066eff8c 100644 --- a/packages/support/invariants/tests/invariants.spec.ts +++ b/packages/support/invariants/tests/invariants.spec.ts @@ -189,6 +189,19 @@ describe('session-log invariants', () => { .toThrow(/no prior tool\/call/) }) + it('keeps fresh tool-result appends open-step and pending-call checked', async () => { + const { ctx } = await setup() + const session = ctx.sessions.create() + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + expect(() => session.append('tool/result', { + turn: 1, + step: 1, + callId: CallId('closed'), + content: [], + isError: false, + }, { surfaceOp: 'append' })).toThrow(/open is turn 1\/step null/) + }) + it('allows a synthetic interrupted tool/result from crash repair without a prior tool/call event', async () => { const { ctx } = await setup() const session = ctx.sessions.create() @@ -465,6 +478,41 @@ describe('HMR safety', () => { }) describe('surface contract under the invariants composition', () => { + async function toolResultRewriteFixture(openRewriteTurn = true) { + const { ctx } = await setup() + const session = ctx.sessions.create() + session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) + const unrelated = session.append('user/message', { + content: [{ type: 'text', text: 'request' }], + source: { kind: 'user' }, + }, { surfaceOp: 'append' }) + session.append('step/start', { turn: 1, step: 1 }) + session.append('tool/call', { + turn: 1, + step: 1, + callId: CallId('rewrite'), + name: 'echo', + arguments: '{}', + }) + const originalData = { + turn: 1, + step: 1, + callId: CallId('rewrite'), + content: [{ type: 'text' as const, text: 'original' }], + isError: true, + error: { name: 'ExitError', code: 'EXIT_1' }, + meta: { presentation: { kind: 'terminal', output: 'full output' } }, + futureField: { nested: ['preserve', 1] }, + } + const original = session.append('tool/result', originalData, { surfaceOp: 'append' }) + session.append('step/end', { turn: 1, step: 1 }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + if (openRewriteTurn) { + session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } }) + } + return { session, unrelated, original } + } + it('accepts well-formed surface metadata', async () => { const { ctx } = await setup() const session = ctx.sessions.create() @@ -487,6 +535,71 @@ describe('surface contract under the invariants composition', () => { // no throw — well-formed replace op }) + it('treats a provenance-backed tool-result replacement as a turn-enclosed rewrite', async () => { + const { session, original } = await toolResultRewriteFixture() + + expect(() => session.append('tool/result', { + ...original.data, + content: [{ type: 'text', text: 'pruned' }], + }, { + surfaceOp: { op: 'replace', start: original.seq, end: original.seq }, + sourceEventSeqs: [original.seq], + })).not.toThrow() + }) + + it('rejects a tool-result replacement outside a turn', async () => { + const { session, original } = await toolResultRewriteFixture(false) + + expect(() => session.append('tool/result', { + ...original.data, + content: [{ type: 'text', text: 'pruned' }], + }, { + surfaceOp: { op: 'replace', start: original.seq, end: original.seq }, + sourceEventSeqs: [original.seq], + })).toThrow(/outside any open turn/) + }) + + it('rejects a tool-result replacement targeting an unrelated current node', async () => { + const { session, unrelated, original } = await toolResultRewriteFixture() + expect(() => session.append('tool/result', { + ...original.data, + content: [{ type: 'text', text: 'forged' }], + }, { + surfaceOp: { op: 'replace', start: unrelated.seq, end: unrelated.seq }, + sourceEventSeqs: [unrelated.seq], + })).toThrow(/must target a current tool\/result/) + }) + + it('rejects a multi-node tool-result replacement even with complete provenance', async () => { + const { session, unrelated, original } = await toolResultRewriteFixture() + expect(() => session.append('tool/result', { + ...original.data, + content: [{ type: 'text', text: 'forged' }], + }, { + surfaceOp: { op: 'replace', start: unrelated.seq, end: original.seq }, + sourceEventSeqs: [unrelated.seq, original.seq], + })).toThrow(/must rewrite exactly one current node/) + }) + + it.each([ + ['callId', { callId: CallId('forged') }], + ['turn', { turn: 2 }], + ['step', { step: 2 }], + ['error', { error: { name: 'ExitError', code: 'DIFFERENT' } }], + ['meta', { meta: { presentation: { kind: 'generic' } } }], + ['future data', { futureField: { nested: ['changed'] } }], + ])('rejects a content rewrite with altered %s', async (_label, altered) => { + const { session, original } = await toolResultRewriteFixture() + expect(() => session.append('tool/result', { + ...original.data, + ...altered, + content: [{ type: 'text', text: 'pruned' }], + }, { + surfaceOp: { op: 'replace', start: original.seq, end: original.seq }, + sourceEventSeqs: [original.seq], + })).toThrow(/may change only content/) + }) + it('accepts known-empty assistant provenance and rejects empty provenance elsewhere', async () => { const { ctx } = await setup() const session = ctx.sessions.create() diff --git a/packages/ui/acp/README.md b/packages/ui/acp/README.md index 44748add2a..9841d1ce2e 100644 --- a/packages/ui/acp/README.md +++ b/packages/ui/acp/README.md @@ -114,7 +114,7 @@ When optional consumers are loaded, ACP form answers become the exact JSON shape #### Token effect -Answer, error, and denial text enters context only through the owning tool result; presentation metadata adds zero model tokens. +Answer, error, and denial text enters context only through the owning tool result; presentation metadata adds zero model tokens. A replacement `tool/result` still changes the model-facing session surface, but live and replayed ACP feeds ignore it as an execution update so the original terminal or diff completion is not overwritten. #### KV Cache effect diff --git a/packages/ui/acp/acp-feature-support.md b/packages/ui/acp/acp-feature-support.md index 0b108cae67..497973731e 100644 --- a/packages/ui/acp/acp-feature-support.md +++ b/packages/ui/acp/acp-feature-support.md @@ -82,7 +82,7 @@ These are capabilities the bridge would *drive* on the editor. The harness runs | `agent_thought_chunk` | S | ✅ | ✅ | ✅ | From `assistant/chunk` reasoning-delta. | | `user_message_chunk` | S | ✅ | ✅ | ✅ | Emitted during `session/load` replay to reconstruct the user side. | | `tool_call` | S | ✅ | ✅ | ✅ | Tool-owned presentation (`presentCall`); see [§5](#5-tool-call-rendering). | -| `tool_call_update` | S | ✅ | ✅ | ✅ | From `tool/result` via `presentResult`. | +| `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 | ❌ | ✅ | ✅ | No slash commands advertised. | | `current_mode_update` | S | ❌ | ✅ | ✅ | No session modes. | diff --git a/packages/ui/acp/src/index.ts b/packages/ui/acp/src/index.ts index fe6b2630c7..b190aeb47d 100644 --- a/packages/ui/acp/src/index.ts +++ b/packages/ui/acp/src/index.ts @@ -827,10 +827,11 @@ export function apply(ctx: Context, config: AcpConfig): void { // session/cancel maps to the queue-aware agent.cancel(reason): it aborts // a RUNNING step, clears the queued + steering FIFOs, and drops a // turn that is about to start (the pre-step window) — so a queued-but- - // not-yet-started prompt never runs, and a prompt accepted right after - // cannot be batched into the cancelled turn. Scoped to THIS session's + // not-yet-started prompt never runs, while a prompt accepted afterward + // remains a separate queued turn. Scoped to THIS session's // agent — a cancel in one session never touches another's stream or - // pending prompt (RFC 011 isolation). We ALSO settle the in-flight prompt + // pending prompt (multi-session isolation). + // We ALSO settle the in-flight prompt // as cancelled directly here: do NOT rely on the resulting turn/end to // settle it, because cancel() may drop the turn before any turn/end is // emitted, and removing this direct settle would move the RPC's @@ -1039,7 +1040,8 @@ function validateMcpServers(params: { mcpServers?: unknown[] }): void { * loaded transcript reconstructs the USER side of each turn without echoing * a live `session/prompt` back to the client * - `tool/call` → `tool_call` (pending) - * - `tool/result` → `tool_call_update` (completed/failed) + * - appended `tool/result` → `tool_call_update` (completed/failed) + * - replacement `tool/result` → no update (context rewrite, not execution) * * Tool-call presentation (title/kind/rawInput, and the completed-state content) * is owned by each TOOL via `presentCall`/`presentResult` — the bridge never @@ -1100,6 +1102,10 @@ export function streamSessionEventUpdate( return } case 'tool/result': { + // Replacements (for example model-free pruning) are transcript rewrites, + // not repeated tool executions. Re-presenting one would consume no + // pending call and could clobber the original terminal/diff completion. + if (event.surfaceOp !== undefined && event.surfaceOp !== 'append') return const view = presenter.result(event.data.callId, event.data.content, event.data.isError, event.data.meta) notify({ sessionId, update: toolResultUpdate(event.data.callId, view, event.data.isError, terminal) }) return diff --git a/packages/ui/acp/tests/config-options.spec.ts b/packages/ui/acp/tests/config-options.spec.ts index 3611c55e1f..29fb06b28c 100644 --- a/packages/ui/acp/tests/config-options.spec.ts +++ b/packages/ui/acp/tests/config-options.spec.ts @@ -199,12 +199,12 @@ describe('acp bridge — session config options', () => { expect(after.configOptions).toEqual(optionsWithPermission('danger-full-access')) const session = h.ctx.agents.list()[0]?.session - expect(session?.events.some(e => e.type === 'permission/preset' || e.type === 'bash/sandbox-mode' || e.type === 'approval/policy')).toBe(false) + expect(session?.events.some(e => e.type === 'permission/preset' || e.type === 'sandbox/mode' || e.type === 'approval/policy')).toBe(false) await h.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'anchor' }] }) const events = session?.events ?? [] expect(events.filter(e => e.type === 'permission/preset').map(e => e.data)).toEqual([{ preset: 'danger-full-access' }]) - expect(events.filter(e => e.type === 'bash/sandbox-mode').map(e => e.data)).toEqual([{ mode: 'danger-full-access' }]) + expect(events.filter(e => e.type === 'sandbox/mode').map(e => e.data)).toEqual([{ mode: 'danger-full-access' }]) expect(events.filter(e => e.type === 'approval/policy').map(e => e.data)).toEqual([{ policy: 'never' }]) const turnStart = events.findIndex(e => e.type === 'turn/start') const anchored = events.findIndex(e => e.type === 'permission/preset') @@ -234,7 +234,7 @@ describe('acp bridge — session config options', () => { expect(back.configOptions).toEqual(optionsWithPermission('workspace-write')) await h.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'anchor' }] }) const events = h.ctx.agents.list()[0]?.session.events ?? [] - expect(events.some(e => e.type === 'permission/preset' || e.type === 'bash/sandbox-mode' || e.type === 'approval/policy')).toBe(false) + expect(events.some(e => e.type === 'permission/preset' || e.type === 'sandbox/mode' || e.type === 'approval/policy')).toBe(false) }) it('a no-op switch (the value already shown) records nothing and keeps a live pending', async () => { @@ -262,7 +262,7 @@ describe('acp bridge — session config options', () => { const anchored = events.findIndex(e => e.type === 'permission/preset') expect(turnStart).toBeGreaterThanOrEqual(0) expect(anchored).toBeGreaterThan(turnStart) - expect(events.some(e => e.type === 'bash/sandbox-mode')).toBe(true) + expect(events.some(e => e.type === 'sandbox/mode')).toBe(true) expect(events.some(e => e.type === 'approval/policy')).toBe(true) await h.client.cancel({ sessionId }) await hung @@ -332,7 +332,7 @@ describe('acp bridge — session config options', () => { const agent = h.ctx.agents.list()[0] if (agent === undefined) throw new Error('expected an agent') agent.session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) - agent.session.append('bash/sandbox-mode', { mode: 'read-only' }) + agent.session.append('sandbox/mode', { mode: 'read-only' }) agent.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) const echo = await h.client.setSessionConfigOption({ sessionId, configId: 'permission', value: 'custom' }) const option = echo.configOptions?.find(entry => entry.id === 'permission') diff --git a/packages/ui/acp/tests/dispose.spec.ts b/packages/ui/acp/tests/dispose.spec.ts index 5cbdb6859b..8baf076d58 100644 --- a/packages/ui/acp/tests/dispose.spec.ts +++ b/packages/ui/acp/tests/dispose.spec.ts @@ -223,7 +223,8 @@ describe('acp bridge — disposal & HMR safety', () => { it('per-session AgentHandle dispose leaves sibling agents untouched', async () => { // The factory returns a per-agent AgentHandle whose dispose() tears down - // EXACTLY that agent + its session — RFC 011 isolation. Create two agents + // EXACTLY that agent + its session — the registry's per-handle isolation + // contract. Create two agents // directly through the registry factory (the same path the ACP bridge uses), // dispose one handle, and assert the other survives, registered and // queryable, with its session still in the store. diff --git a/packages/ui/acp/tests/load.spec.ts b/packages/ui/acp/tests/load.spec.ts index b8de5b8557..1d84727de4 100644 --- a/packages/ui/acp/tests/load.spec.ts +++ b/packages/ui/acp/tests/load.spec.ts @@ -161,6 +161,53 @@ describe('acp bridge — session/load replay', () => { expect(meta.terminal_exit?.exit_code).toBe(0) }) + it('keeps one terminal completion live and on replay when a pruning replacement is logged', async () => { + live = await makeBridgeHarness({ + storageDir, + withBash: true, + script: [toolCallResponse('c1', 'bash', { command: 'echo full', description: 'Print full output' }), textResponse('done')], + }) + await live.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: { _meta: { terminal_output: true } } }) + const { sessionId } = await live.client.newSession({ cwd: process.cwd(), mcpServers: [] }) + await live.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'run it' }] }) + + const session = live.ctx.agents.get(SessionId(sessionId))!.session + const original = session.events.find(event => event.type === 'tool/result') + if (original?.type !== 'tool/result') throw new Error('expected original tool/result') + const liveCompletions = () => live!.updates.filter(update => + update.sessionUpdate === 'tool_call_update' && update.toolCallId === 'c1') + expect(liveCompletions()).toHaveLength(1) + expect((liveCompletions()[0] as { _meta?: { terminal_output?: { data: string } } })._meta?.terminal_output?.data) + .toBe('full\n') + + session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } }) + session.append('tool/result', { + ...original.data, + content: [{ type: 'text', text: '[... tool result middle pruned ...]' }], + }, { + surfaceOp: { op: 'replace', start: original.seq, end: original.seq }, + sourceEventSeqs: [original.seq], + }) + session.append('turn/end', { turn: 2, reason: { kind: 'completed' } }) + + // The replacement is durable but is not another live completion. + expect(session.events.filter(event => event.type === 'tool/result')).toHaveLength(2) + expect(JSON.stringify(session.deriveMessages())).toContain('tool result middle pruned') + expect(liveCompletions()).toHaveLength(1) + await live.dispose() + live = undefined + + loader = await makeBridgeHarness({ storageDir, withBash: true, script: [] }) + await loader.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: { _meta: { terminal_output: true } } }) + await loader.client.loadSession({ sessionId, cwd: process.cwd(), mcpServers: [] }) + + const replayed = loader.updates.filter(update => + update.sessionUpdate === 'tool_call_update' && update.toolCallId === 'c1') + expect(replayed).toHaveLength(1) + expect((replayed[0] as { _meta?: { terminal_output?: { data: string } } })._meta?.terminal_output?.data) + .toBe('full\n') + }) + it('a load whose resume finishes after a client disconnect leaks no live session', async () => { // Stall persistence so transport closes while resume is pending. Whether the SDK rejects first // or the bridge's post-await guard fires, no agent may survive for the dead connection. diff --git a/packages/ui/acp/tests/multi-session.spec.ts b/packages/ui/acp/tests/multi-session.spec.ts index 0881fe4199..efeb00f9ad 100644 --- a/packages/ui/acp/tests/multi-session.spec.ts +++ b/packages/ui/acp/tests/multi-session.spec.ts @@ -14,7 +14,7 @@ function messageTextFor(updates: { sessionId?: string; update: CapturedUpdate }[ .join('') } -describe('acp bridge — RFC 011 multi-session isolation', () => { +describe('acp bridge — multi-session isolation', () => { let storageDir: string let harness: BridgeHarness | undefined diff --git a/packages/ui/acp/tests/stream-update.spec.ts b/packages/ui/acp/tests/stream-update.spec.ts index 415e9afb33..51a3e3115b 100644 --- a/packages/ui/acp/tests/stream-update.spec.ts +++ b/packages/ui/acp/tests/stream-update.spec.ts @@ -105,6 +105,22 @@ describe('streamSessionEventUpdate', () => { expect((failed[0] as { status: string }).status).toBe('failed') }) + it('emits no execution update for a tool-result surface replacement', () => { + const replacement = { + ...evt('tool/result', { + turn: 1, + step: 1, + callId: CallId('c1'), + content: [{ type: 'text', text: '[... tool result middle pruned ...]' }], + isError: false, + }), + seq: 2, + surfaceOp: { op: 'replace', start: 1, end: 1 }, + sourceEventSeqs: [1], + } as SessionEvent + expect(updatesFor(replacement)).toEqual([]) + }) + it('drops non-text tool-result content (text-only)', () => { const update = updatesFor(evt('tool/result', { turn: 1, step: 1, callId: CallId('c1'), @@ -450,6 +466,16 @@ describe('terminal-card mapping (capability-gated)', () => { const callEvent = evt('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'bash', arguments: JSON.stringify({ command: 'echo hi', description: 'Greet' }) }) const resultEvent = evt('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'hi\n' }], isError: false }) + const prunedResultEvent = { + ...resultEvent, + seq: 2, + data: { + ...resultEvent.data, + content: [{ type: 'text', text: '[... tool result middle pruned ...]' }], + }, + surfaceOp: { op: 'replace', start: 1, end: 1 }, + sourceEventSeqs: [1], + } as SessionEvent function termUpdates(tool: ToolDefinition, enabled: boolean, cwd: string | undefined, ...events: SessionEvent[]): SessionNotification['update'][] { const presenter = new ToolPresenter(registryOf(tool)) @@ -477,6 +503,27 @@ describe('terminal-card mapping (capability-gated)', () => { }) }) + it('live/replay translation preserves the original terminal completion across a pruning rewrite', () => { + const updates = termUpdates( + termTool({ card: 'terminal' }, { output: 'hi\n', exitCode: 0 }), + true, + '/work/proj', + callEvent, + resultEvent, + prunedResultEvent, + ) + expect(updates).toHaveLength(2) + expect(updates[1]).toEqual({ + sessionUpdate: 'tool_call_update', + toolCallId: 'c1', + status: 'completed', + _meta: { + terminal_output: { terminal_id: 'c1', data: 'hi\n' }, + terminal_exit: { terminal_id: 'c1', exit_code: 0 }, + }, + }) + }) + it('capability ON: an ABSOLUTE tool cwd wins; a RELATIVE one resolves against the session cwd', () => { const [absCall] = termUpdates(termTool({ card: 'terminal', cwd: '/explicit/abs' }, { output: 'x' }), true, '/work/proj', callEvent) expect((absCall as unknown as { _meta: { terminal_info: { cwd: string } } })._meta.terminal_info.cwd).toBe('/explicit/abs') @@ -633,17 +680,38 @@ describe('result-time diff card (REAL fs edit tool → tool_call_update diff blo // call-time snippet, then the tool/result carries the tool's computed applied-hunk `meta`, // which presentResult narrows into a `diff` result card the bridge forwards as `{ type: // 'diff' }` content blocks. The real tool is required because its result metadata is the contract. - it('forwards the applied-hunk meta onto the wire as tool_call_update diff content', async () => { + it('live/replay translation keeps the applied diff when a pruning rewrite follows', async () => { const ctx = await fsCtx() const presenter = new ToolPresenter(ctx.tools) const args = JSON.stringify({ file_path: 'src/b.ts', old_string: 'OLD', new_string: 'NEW' }) // The applied hunk the tool would compute and persist on the result meta. const meta = { diffs: [{ path: 'src/b.ts', oldText: 'a\nOLD\nb', newText: 'a\nNEW\nb' }] } - const [, resultUpdate] = updatesWith( + const originalResult = evt('tool/result', { + turn: 1, + step: 1, + callId: CallId('e1'), + content: [{ type: 'text', text: 'ok' }], + isError: false, + meta, + }) + const replacement = { + ...originalResult, + seq: 3, + data: { + ...originalResult.data, + content: [{ type: 'text', text: '[... tool result middle pruned ...]' }], + }, + surfaceOp: { op: 'replace', start: 2, end: 2 }, + sourceEventSeqs: [2], + } as SessionEvent + const updates = updatesWith( presenter, evt('tool/call', { turn: 1, step: 1, callId: CallId('e1'), name: 'edit', arguments: args }), - evt('tool/result', { turn: 1, step: 1, callId: CallId('e1'), content: [{ type: 'text', text: 'ok' }], isError: false, meta }), + originalResult, + replacement, ) + expect(updates).toHaveLength(2) + const resultUpdate = updates[1] expect(resultUpdate).toEqual({ sessionUpdate: 'tool_call_update', toolCallId: 'e1', diff --git a/packages/ui/permission/README.md b/packages/ui/permission/README.md index f7c135f5e3..12196d89a0 100644 --- a/packages/ui/permission/README.md +++ b/packages/ui/permission/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-permission -User-facing permission presets through `ctx.permission` ([`PermissionService`](src/index.ts)). Each configured name bundles `bash/sandbox-mode` with `approval/policy`; the defaults are `workspace-write` (`workspace-write` + `ask`) and `danger-full-access` (`danger-full-access` + `never`). The ACP bridge exposes them as one `Permissions` select, while sandbox execution and approval continue to consume their own knobs. +User-facing permission presets through `ctx.permission` ([`PermissionService`](src/index.ts)). Each configured name bundles `sandbox/mode` with `approval/policy`; the defaults are `workspace-write` (`workspace-write` + `ask`) and `danger-full-access` (`danger-full-access` + `never`). The ACP bridge exposes them as one `Permissions` select, while sandbox execution and approval continue to consume their own knobs. `set(session, name)` records a changed selection in a log-only `permission/preset` event, then calls each knob's setter only when its effective value changes. The selection event precedes the knob events and preserves user intent when presets share a bundle; a net-zero selection appends nothing. `current(events)` prefers a still-matching recorded selection, then the first matching table entry, and otherwise returns `custom`. Clients may display `custom` as the current value, but cannot select it. diff --git a/packages/ui/permission/package.json b/packages/ui/permission/package.json index e38e5a7bf1..2d78f52a78 100644 --- a/packages/ui/permission/package.json +++ b/packages/ui/permission/package.json @@ -24,6 +24,7 @@ "peerDependencies": { "@deepseek-ai/dsh-bash": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", + "@deepseek-ai/dsh-sandbox-policy": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-user-approval": "^0.0.1", "cordis": "^4.0.0-rc.7" @@ -34,6 +35,7 @@ "devDependencies": { "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-user-approval": "workspace:^", "cordis": "^4.0.0-rc.7" diff --git a/packages/ui/permission/src/index.ts b/packages/ui/permission/src/index.ts index 3d61df79bf..a92f3bc221 100644 --- a/packages/ui/permission/src/index.ts +++ b/packages/ui/permission/src/index.ts @@ -12,7 +12,10 @@ import { Context, Service } from 'cordis' import z from 'schemastery' import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' -import { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-bash' +import { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy' +// Side-effect type import: declaration-merges `ctx.bash` (the capability fact +// `sandboxMode` this service reads), without a value dependency on the seam. +import type {} from '@deepseek-ai/dsh-bash' import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval' import { APPROVAL_POLICIES, effectiveApprovalPolicy, setApprovalPolicy } from '@deepseek-ai/dsh-user-approval' @@ -36,7 +39,7 @@ declare module '@deepseek-ai/dsh-session' { /** One preset's sandbox/approval bundle and optional client presentation. */ export interface PresetSpec { - /** The `bash/sandbox-mode` value the preset writes through. */ + /** The `sandbox/mode` value the preset writes through. */ sandbox: SandboxMode /** The `approval/policy` value the preset writes through. */ approval: ApprovalPolicy diff --git a/packages/ui/permission/tests/permission.spec.ts b/packages/ui/permission/tests/permission.spec.ts index 50b630bfd1..a203bd5082 100644 --- a/packages/ui/permission/tests/permission.spec.ts +++ b/packages/ui/permission/tests/permission.spec.ts @@ -51,7 +51,7 @@ describe('PermissionService', () => { it('a knob state matching no table entry derives custom — a state, not an error', async () => { const ctx = await mounted() const session = freshSession('sess-custom') - session.append('bash/sandbox-mode', { mode: 'read-only' }) + session.append('sandbox/mode', { mode: 'read-only' }) expect(ctx.permission.current(session.events)).toBe(CUSTOM_PRESET) ctx.permission.set(session, 'danger-full-access') expect(ctx.permission.current(session.events)).toBe('danger-full-access') @@ -74,7 +74,7 @@ describe('PermissionService', () => { ctx.permission.set(session, 'agentish') expect(ctx.permission.current(session.events)).toBe('agentish') session.append('approval/policy', { policy: 'never' }) - session.append('bash/sandbox-mode', { mode: 'danger-full-access' }) + session.append('sandbox/mode', { mode: 'danger-full-access' }) expect(ctx.permission.current(session.events)).toBe('danger-full-access') }) @@ -84,7 +84,7 @@ describe('PermissionService', () => { ctx.permission.set(session, 'danger-full-access') expect(session.events.map(e => [e.type, e.data])).toEqual([ ['permission/preset', { preset: 'danger-full-access' }], - ['bash/sandbox-mode', { mode: 'danger-full-access' }], + ['sandbox/mode', { mode: 'danger-full-access' }], ['approval/policy', { policy: 'never' }], ]) }) @@ -102,12 +102,12 @@ describe('PermissionService', () => { ctx.permission.set(session, 'danger-full-access') // Re-selecting from a drifted state records the choice and repairs only // the changed knob. - session.append('bash/sandbox-mode', { mode: 'read-only' }) + session.append('sandbox/mode', { mode: 'read-only' }) ctx.permission.set(session, 'danger-full-access') const tail = session.events.slice(4) expect(tail.map(e => [e.type, e.data])).toEqual([ ['permission/preset', { preset: 'danger-full-access' }], - ['bash/sandbox-mode', { mode: 'danger-full-access' }], + ['sandbox/mode', { mode: 'danger-full-access' }], ]) }) diff --git a/packages/ui/permission/tsconfig.json b/packages/ui/permission/tsconfig.json index 8b9cff62b4..fa31f71f69 100644 --- a/packages/ui/permission/tsconfig.json +++ b/packages/ui/permission/tsconfig.json @@ -23,6 +23,9 @@ { "path": "../../sandbox/sandbox" }, + { + "path": "../../sandbox/sandbox-policy" + }, { "path": "../../bash/bash" }, diff --git a/packages/ui/stdio/README.md b/packages/ui/stdio/README.md index eda7d00b8b..07cc6a5b21 100644 --- a/packages/ui/stdio/README.md +++ b/packages/ui/stdio/README.md @@ -31,7 +31,7 @@ Each non-empty terminal line outside an active question becomes one text block, #### Token effect -Submitted text is retained under the agent loop's normal session-history and compaction rules. The welcome banner, `> ` prompt, rendered transcript, and `[tool call]` / `[tool result]` terminal lines add no tokens. +Submitted text is retained under the agent loop's normal session-history and compaction rules. The welcome banner, `> ` prompt, rendered transcript, and `[tool call]` / `[tool result]` terminal lines add no tokens. A replacement `tool/result` remains model-visible through the session surface but is not rendered as a second execution; stdio keeps the original full-fidelity result line. #### KV Cache effect diff --git a/packages/ui/stdio/src/index.ts b/packages/ui/stdio/src/index.ts index 6f73d948bf..ac22cf6730 100644 --- a/packages/ui/stdio/src/index.ts +++ b/packages/ui/stdio/src/index.ts @@ -145,6 +145,10 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt inReasoning = false output.write(`\n [tool call] ${toolName}(${args})`) } else if (event.type === 'tool/result') { + // A surface replacement changes future model context; it is not another + // execution. Keep the original full-fidelity terminal presentation and + // suppress duplicate output during live delivery or log replay. + if (event.surfaceOp !== undefined && event.surfaceOp !== 'append') return const { content } = event.data const text = content.filter(block => block.type === 'text').map(block => block.text).join('') output.write(`\n [tool result] ${text}\n `) @@ -164,9 +168,9 @@ export function createStdioChat(ctx: Context, config: Config, runtime: StdioRunt // immediately — no turn will ever start, so there is nothing to wait // for. (Gating on an observed 'running' here would hang forever.) // - If work WAS submitted, exit the next time the agent settles to idle - // AFTER having run. Two subtleties this handles: the loop batches - // several queued messages into ONE turn (one idle), so we don't count - // sends; and agent.send() does NOT synchronously flip status to + // AFTER having run. Later lines may steer the active turn, and consecutive + // queued turns can share one running interval, so we don't count inputs; + // agent.send() also does NOT synchronously flip status to // 'running', so requiring an observed 'running' first (`sawRunning`) // avoids exiting in the gap before the turn starts and dropping work. let stdinClosed = false diff --git a/packages/ui/stdio/tests/stdio.spec.ts b/packages/ui/stdio/tests/stdio.spec.ts index a3069462ff..478914849c 100644 --- a/packages/ui/stdio/tests/stdio.spec.ts +++ b/packages/ui/stdio/tests/stdio.spec.ts @@ -367,6 +367,43 @@ describe('createStdioChat rendering', () => { expect(out.text()).toContain('[tool result] file.txt') }) + it('renders one full-fidelity result whether the event feed is live or replayed', async () => { + const { ctx, out } = await setup() + const session = makeSession('main') + const original = { + type: 'tool/result', + seq: 2, + time: 0, + data: { + turn: 1, + step: 1, + callId: 'c1', + content: [{ type: 'text', text: 'full terminal output' }], + isError: false, + meta: { terminal: { output: 'full terminal output' } }, + }, + surfaceOp: 'append', + } as SessionEvent + const replacement = { + ...original, + seq: 3, + data: { + ...original.data, + content: [{ type: 'text', text: '[... tool result middle pruned ...]' }], + }, + surfaceOp: { op: 'replace', start: 2, end: 2 }, + sourceEventSeqs: [2], + } as SessionEvent + + // Stdio consumes the same session/event shape whether a host forwards a + // live append or replays a stored log through the rendering feed. + for (const event of [original, replacement]) ctx.emit('session/event', session, event) + + expect(out.text().match(/\[tool result\]/g)).toHaveLength(1) + expect(out.text()).toContain('full terminal output') + expect(out.text()).not.toContain('tool result middle pruned') + }) + it('renders a todo/write session event as a glyphed checklist', async () => { const { ctx, out } = await setup() const session = {} as Session diff --git a/packages/web/AGENTS.md b/packages/web/AGENTS.md new file mode 100644 index 0000000000..1b9721d9ca --- /dev/null +++ b/packages/web/AGENTS.md @@ -0,0 +1,5 @@ +# AGENTS.md — Web Packages + +These rules supplement the package conventions in [packages/AGENTS.md](../AGENTS.md). + +- **Reject redirects on credential-bearing provider requests.** Configure the HTTP client to fail before following any redirect response. Regression coverage must prove that the redirect target is not contacted and that every credentialed provider opts into the policy. The configured endpoint necessarily receives the initial request; this prevents automatic forwarding of credentials or request data to another origin, not compromise of the configured endpoint. diff --git a/packages/web/web-search-deepseek/README.md b/packages/web/web-search-deepseek/README.md index e3045d7f7c..e71a769725 100644 --- a/packages/web/web-search-deepseek/README.md +++ b/packages/web/web-search-deepseek/README.md @@ -37,7 +37,7 @@ DeepSeek returns no provider-generated answer surface this provider trusts as `c Results are deduplicated by URL because one request may surface the same page across searches. DeepSeek exposes `maxUses`, not a result-count knob, so the seam enforces `maxResults` by truncating `sources[]` and setting `truncated`. -Provider failures become `WEB_PROVIDER_ERROR`; caller cancellation becomes `WEB_ABORTED`. +Provider failures become `WEB_PROVIDER_ERROR`; caller cancellation becomes `WEB_ABORTED`. HTTP redirects are rejected before the `Location` target is contacted and surface as `WEB_PROVIDER_ERROR`. ## Model Experience diff --git a/packages/web/web-search-deepseek/src/provider.ts b/packages/web/web-search-deepseek/src/provider.ts index b272565441..cd259a999d 100644 --- a/packages/web/web-search-deepseek/src/provider.ts +++ b/packages/web/web-search-deepseek/src/provider.ts @@ -127,7 +127,7 @@ export function mapAnthropicResponse(response: AnthropicResponse): WebSearchResu return { sources, truncated: false } } -/** The DeepSeek-backed search provider. */ +/** The DeepSeek-backed search provider; HTTP redirects fail as `WEB_PROVIDER_ERROR`. */ export class DeepSeekSearchProvider implements WebSearchProvider { readonly id = DEEPSEEK_PROVIDER_ID @@ -145,6 +145,7 @@ export class DeepSeekSearchProvider implements WebSearchProvider { try { response = await fetch(`${this.options.baseURL}/messages`, { method: 'POST', + redirect: 'error', headers: { // Official DeepSeek expects `x-api-key`; an Anthropic-compatible proxy // may expect `Authorization: Bearer` — send both so either resolves. diff --git a/packages/web/web-search-deepseek/tests/deepseek.spec.ts b/packages/web/web-search-deepseek/tests/deepseek.spec.ts index afca4ecce2..5a9972cfd6 100644 --- a/packages/web/web-search-deepseek/tests/deepseek.spec.ts +++ b/packages/web/web-search-deepseek/tests/deepseek.spec.ts @@ -162,6 +162,7 @@ describe('DeepSeekSearchProvider request mapping', () => { await new DeepSeekSearchProvider(options).search({ query: 'hello' }) const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit] expect(url).toBe('https://api.deepseek.test/anthropic/v1/messages') + expect(init).toMatchObject({ method: 'POST', redirect: 'error' }) const headers = init.headers as Record expect(headers['x-api-key']).toBe('ds-key') expect(headers['authorization']).toBe('Bearer ds-key') diff --git a/packages/web/web-search-deepseek/tests/redirect.spec.ts b/packages/web/web-search-deepseek/tests/redirect.spec.ts new file mode 100644 index 0000000000..100d60f390 --- /dev/null +++ b/packages/web/web-search-deepseek/tests/redirect.spec.ts @@ -0,0 +1,123 @@ +/** + * Real HTTP coverage proves whether native `fetch` contacts a cross-origin `Location`; mocked + * request-init assertions alone cannot observe that boundary. + */ + +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { createServer, type IncomingMessage, type Server } from 'node:http' +import type { AddressInfo } from 'node:net' +import { DeepSeekSearchProvider } from '@deepseek-ai/dsh-web-search-deepseek' + +const TEST_API_KEY = 'redirect-test-key' +const TEST_QUERY = 'private redirect query' +const targetRequests: ReceivedRequest[] = [] + +interface ReceivedRequest { + readonly body: string + readonly headers: IncomingMessage['headers'] + readonly method?: string +} + +let redirectOrigin: string +let targetOrigin: string + +const targetServer = createServer((request, response) => { + void captureRequest(request).then((received) => { + targetRequests.push(received) + response.writeHead(204).end() + }, (error: unknown) => response.destroy(asError(error))) +}) + +const redirectServer = createServer((request, response) => { + request.resume() + const status = Number(new URL(request.url ?? '/', 'http://fixture.test').pathname.split('/')[1]) + response.writeHead(status, { location: `${targetOrigin}/collect` }).end() +}) + +beforeAll(async () => { + targetOrigin = await listen(targetServer) + redirectOrigin = await listen(redirectServer) +}) + +afterAll(async () => { + await Promise.all([close(redirectServer), close(targetServer)]) +}) + +describe('DeepSeekSearchProvider redirect policy', () => { + it.each([301, 302, 303, 307, 308])('rejects HTTP %i before contacting Location', async (status) => { + targetRequests.length = 0 + const provider = new DeepSeekSearchProvider({ + apiKey: TEST_API_KEY, + baseURL: `${redirectOrigin}/${status}`, + model: 'deepseek-chat', + apiVersion: '2023-06-01', + maxTokens: 32, + maxUses: 1, + }) + + await expect(provider.search({ query: TEST_QUERY })) + .rejects.toMatchObject({ code: 'WEB_PROVIDER_ERROR' }) + expect(targetRequests).toHaveLength(0) + }) + + it('shows default 307 following forwards the custom credential and POST body', async () => { + targetRequests.length = 0 + const body = JSON.stringify({ query: TEST_QUERY }) + await fetch(`${redirectOrigin}/307`, { + method: 'POST', + headers: { + 'x-api-key': TEST_API_KEY, + 'authorization': `Bearer ${TEST_API_KEY}`, + 'content-type': 'application/json', + }, + body, + }) + + expect(targetRequests).toHaveLength(1) + expect(targetRequests[0]).toMatchObject({ method: 'POST', body }) + expect(targetRequests[0]?.headers['x-api-key']).toBe(TEST_API_KEY) + }) +}) + +/** Read a complete request received by the redirect target. */ +function captureRequest(request: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Uint8Array[] = [] + request.on('data', (chunk: unknown) => { + if (typeof chunk === 'string' || chunk instanceof Uint8Array) chunks.push(Buffer.from(chunk)) + else reject(new TypeError('unexpected HTTP request chunk')) + }) + request.once('error', reject) + request.once('end', () => { + resolve({ + ...request.method !== undefined ? { method: request.method } : {}, + headers: request.headers, + body: Buffer.concat(chunks).toString('utf8'), + }) + }) + }) +} + +/** Listen on an ephemeral loopback port and return the server origin. */ +async function listen(server: Server): Promise { + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', resolve) + }) + const address = server.address() as AddressInfo + return `http://127.0.0.1:${address.port}` +} + +/** Close a listening fixture server after every request has settled. */ +async function close(server: Server): Promise { + if (!server.listening) return + await new Promise((resolve, reject) => server.close((error) => { + if (error === undefined) resolve() + else reject(error) + })) +} + +/** Normalize an unknown fixture failure for `ServerResponse.destroy`. */ +function asError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)) +} diff --git a/packages/web/web-search-exa/README.md b/packages/web/web-search-exa/README.md index a4f1fa648e..d8ab206e7a 100644 --- a/packages/web/web-search-exa/README.md +++ b/packages/web/web-search-exa/README.md @@ -23,7 +23,7 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i ## Mapping -Exa returns a flat `results[]` and no generated answer, so `content` is omitted. Each result maps to a `WebSearchSource`: `url` ← `url`, `title` ← `title`, `snippet` ← the first non-empty `highlights[]` entry (a result with no highlight has no portable snippet and is dropped), `publishedAt` ← `publishedDate`. A request's `maxResults` wins over the configured `numResults` default and is sent as Exa's `numResults` for a cost/latency optimization; the final bound is enforced by the seam. Provider failures (HTTP errors, network failure, unparseable or wrong-shape bodies) surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. +Exa returns a flat `results[]` and no generated answer, so `content` is omitted. Each result maps to a `WebSearchSource`: `url` ← `url`, `title` ← `title`, `snippet` ← the first non-empty `highlights[]` entry (a result with no highlight has no portable snippet and is dropped), `publishedAt` ← `publishedDate`. A request's `maxResults` wins over the configured `numResults` default and is sent as Exa's `numResults` for a cost/latency optimization; the final bound is enforced by the seam. Provider failures (HTTP errors, network failure, unparseable or wrong-shape bodies) surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. HTTP redirects are rejected before the `Location` target is contacted and surface as `WEB_PROVIDER_ERROR`. ## Model Experience diff --git a/packages/web/web-search-exa/src/provider.ts b/packages/web/web-search-exa/src/provider.ts index ffc82683b1..3c62dabfa5 100644 --- a/packages/web/web-search-exa/src/provider.ts +++ b/packages/web/web-search-exa/src/provider.ts @@ -80,7 +80,7 @@ export function mapExaResponse(response: ExaSearchResponse): WebSearchResult { return { sources, truncated: false } } -/** The Exa-backed search provider. */ +/** The Exa-backed search provider; HTTP redirects fail as `WEB_PROVIDER_ERROR`. */ export class ExaSearchProvider implements WebSearchProvider { readonly id = EXA_PROVIDER_ID @@ -100,6 +100,7 @@ export class ExaSearchProvider implements WebSearchProvider { try { response = await fetch(`${this.options.baseURL}/search`, { method: 'POST', + redirect: 'error', headers: { 'authorization': `Bearer ${this.options.apiKey}`, 'content-type': 'application/json', diff --git a/packages/web/web-search-exa/tests/exa.spec.ts b/packages/web/web-search-exa/tests/exa.spec.ts index 40c7afda6f..6e29b10aa8 100644 --- a/packages/web/web-search-exa/tests/exa.spec.ts +++ b/packages/web/web-search-exa/tests/exa.spec.ts @@ -96,6 +96,7 @@ describe('ExaSearchProvider request mapping', () => { expect(fetchMock).toHaveBeenCalledOnce() const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit] expect(url).toBe('https://api.exa.test/search') + expect(init).toMatchObject({ method: 'POST', redirect: 'error' }) expect((init.headers as Record)['authorization']).toBe('Bearer exa-key') expect(JSON.parse(init.body as string)).toEqual({ query: 'hello', diff --git a/packages/web/web-search-perplexity/README.md b/packages/web/web-search-perplexity/README.md index d8f9191621..a3728e0197 100644 --- a/packages/web/web-search-perplexity/README.md +++ b/packages/web/web-search-perplexity/README.md @@ -23,7 +23,7 @@ This is an **implementation** package: it registers a provider into `ctx.web`, i ## Mapping -`content` ← `choices[0].message.content` (the generated answer). `sources[]` prefers the structured `search_results[]` (`url`, `title`, `snippet`, `publishedAt` ← `date`), falling back to the URL-only `citations[]` array only when `search_results` is absent — those sources carry just a `url`, which is why `title`/`snippet`/`publishedAt` are optional on the seam. Provider failures surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. Perplexity has no result-count control, so `maxResults` is enforced by the seam (truncating `sources[]` and setting `truncated`). +`content` ← `choices[0].message.content` (the generated answer). `sources[]` prefers the structured `search_results[]` (`url`, `title`, `snippet`, `publishedAt` ← `date`), falling back to the URL-only `citations[]` array only when `search_results` is absent — those sources carry just a `url`, which is why `title`/`snippet`/`publishedAt` are optional on the seam. Provider failures surface as `WebError` `WEB_PROVIDER_ERROR`; an aborted request surfaces as `WEB_ABORTED`. HTTP redirects are rejected before the `Location` target is contacted and surface as `WEB_PROVIDER_ERROR`. Perplexity has no result-count control, so `maxResults` is enforced by the seam (truncating `sources[]` and setting `truncated`). ## Model Experience diff --git a/packages/web/web-search-perplexity/src/provider.ts b/packages/web/web-search-perplexity/src/provider.ts index 8ec5231627..fc6cb9df19 100644 --- a/packages/web/web-search-perplexity/src/provider.ts +++ b/packages/web/web-search-perplexity/src/provider.ts @@ -82,7 +82,7 @@ export function mapPerplexityResponse(response: PerplexityResponse): WebSearchRe } } -/** The Perplexity-backed search provider. */ +/** The Perplexity-backed search provider; HTTP redirects fail as `WEB_PROVIDER_ERROR`. */ export class PerplexitySearchProvider implements WebSearchProvider { readonly id = PERPLEXITY_PROVIDER_ID @@ -103,6 +103,7 @@ export class PerplexitySearchProvider implements WebSearchProvider { try { response = await fetch(`${this.options.baseURL}/chat/completions`, { method: 'POST', + redirect: 'error', headers: { 'authorization': `Bearer ${this.options.apiKey}`, 'content-type': 'application/json', diff --git a/packages/web/web-search-perplexity/tests/perplexity.spec.ts b/packages/web/web-search-perplexity/tests/perplexity.spec.ts index 9662226bc6..b622342384 100644 --- a/packages/web/web-search-perplexity/tests/perplexity.spec.ts +++ b/packages/web/web-search-perplexity/tests/perplexity.spec.ts @@ -90,6 +90,7 @@ describe('PerplexitySearchProvider request mapping', () => { await new PerplexitySearchProvider(options).search({ query: 'hello' }) const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit] expect(url).toBe('https://api.perplexity.test/chat/completions') + expect(init).toMatchObject({ method: 'POST', redirect: 'error' }) expect((init.headers as Record)['authorization']).toBe('Bearer pplx-key') expect(JSON.parse(init.body as string)).toEqual({ model: 'sonar', max_tokens: 1024, messages: [{ role: 'user', content: 'hello' }] }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8fb891bf1..eec26673b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,12 +119,18 @@ importers: '@deepseek-ai/dsh-compact-basic': specifier: workspace:* version: link:../packages/compact/compact-basic + '@deepseek-ai/dsh-compact-tool-result-prune': + specifier: workspace:* + version: link:../packages/compact/compact-tool-result-prune '@deepseek-ai/dsh-fs-local': specifier: workspace:* version: link:../packages/fs/fs-local '@deepseek-ai/dsh-fs-policy': specifier: workspace:* version: link:../packages/fs/fs-policy + '@deepseek-ai/dsh-fs-sandbox': + specifier: workspace:^ + version: link:../packages/fs/fs-sandbox '@deepseek-ai/dsh-goal': specifier: workspace:* version: link:../packages/goal/goal @@ -155,6 +161,9 @@ importers: '@deepseek-ai/dsh-sandbox-local': specifier: workspace:* version: link:../packages/sandbox/sandbox-local + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../packages/sandbox/sandbox-policy '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:* version: link:../packages/session-persistence/session-persistence-jsonl @@ -224,9 +233,6 @@ importers: '@deepseek-ai/dsh-sandbox': specifier: workspace:^ version: link:../../sandbox/sandbox - '@deepseek-ai/dsh-session': - specifier: workspace:^ - version: link:../../core/session 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) @@ -248,10 +254,6 @@ importers: version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) packages/bash/bash-sandbox: - dependencies: - schemastery: - specifier: ^3.18.0 - version: 3.18.0 devDependencies: '@deepseek-ai/dsh-bash': specifier: workspace:^ @@ -265,6 +267,9 @@ importers: '@deepseek-ai/dsh-sandbox-local': specifier: workspace:^ version: link:../../sandbox/sandbox-local + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy 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) @@ -302,6 +307,9 @@ importers: '@deepseek-ai/dsh-sandbox': specifier: workspace:^ version: link:../../sandbox/sandbox + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -385,6 +393,9 @@ importers: '@deepseek-ai/dsh-compact': specifier: workspace:^ version: link:../compact + '@deepseek-ai/dsh-compact-tool-result-prune': + specifier: workspace:^ + version: link:../compact-tool-result-prune '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants @@ -404,6 +415,31 @@ importers: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) + packages/compact/compact-tool-result-prune: + dependencies: + schemastery: + specifier: ^3.18.0 + version: 3.18.0 + devDependencies: + '@cordisjs/plugin-include': + specifier: workspace:^ + version: link:../../../vendor/include + '@cordisjs/plugin-loader': + specifier: workspace:^ + version: link:../../../vendor/loader + '@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 + cordis: + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) + packages/context/time-context: dependencies: schemastery: @@ -873,6 +909,9 @@ importers: '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm + '@deepseek-ai/dsh-sandbox': + specifier: workspace:^ + version: link:../../sandbox/sandbox 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) @@ -905,6 +944,24 @@ importers: 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/fs/fs-sandbox: + devDependencies: + '@deepseek-ai/dsh-fs': + specifier: workspace:^ + version: link:../fs + '@deepseek-ai/dsh-fs-local': + specifier: workspace:^ + version: link:../fs-local + '@deepseek-ai/dsh-sandbox': + specifier: workspace:^ + version: link:../../sandbox/sandbox + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy + 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/fs/tool-fs: dependencies: diff: @@ -938,6 +995,12 @@ importers: '@deepseek-ai/dsh-llm-deepseek': specifier: workspace:^ version: link:../../llm/llm-deepseek + '@deepseek-ai/dsh-sandbox': + specifier: workspace:^ + version: link:../../sandbox/sandbox + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -947,6 +1010,9 @@ importers: '@deepseek-ai/dsh-tools': specifier: workspace:^ version: link:../../core/tools + '@deepseek-ai/dsh-user-approval': + specifier: workspace:^ + version: link:../../ui/user-approval 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) @@ -1258,6 +1324,22 @@ importers: 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-policy: + dependencies: + schemastery: + specifier: ^3.18.0 + version: 3.18.0 + devDependencies: + '@deepseek-ai/dsh-sandbox': + specifier: workspace:^ + version: link:../sandbox + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + 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/sdk/create-sdk: dependencies: '@deepseek-ai/dsh-helper': @@ -2086,6 +2168,9 @@ importers: '@deepseek-ai/dsh-sandbox': specifier: workspace:^ version: link:../../sandbox/sandbox + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session @@ -2523,6 +2608,9 @@ importers: '@deepseek-ai/dsh-compact-basic': specifier: workspace:^ version: link:../../packages/compact/compact-basic + '@deepseek-ai/dsh-compact-tool-result-prune': + specifier: workspace:^ + version: link:../../packages/compact/compact-tool-result-prune '@deepseek-ai/dsh-fs': specifier: workspace:^ version: link:../../packages/fs/fs @@ -2574,6 +2662,9 @@ importers: '@deepseek-ai/dsh-sandbox': specifier: workspace:^ version: link:../../packages/sandbox/sandbox + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../packages/sandbox/sandbox-policy '@deepseek-ai/dsh-scope': specifier: workspace:^ version: link:../../packages/core/scope @@ -2817,15 +2908,33 @@ importers: website: devDependencies: - markdown-it-mathjax3: - specifier: ^4.3.2 - version: 4.3.2 + '@braintree/sanitize-url': + specifier: 7.1.2 + version: 7.1.2 + cytoscape: + specifier: 3.34.0 + version: 3.34.0 + cytoscape-cose-bilkent: + specifier: 4.1.0 + version: 4.1.0(cytoscape@3.34.0) + dayjs: + specifier: 1.11.21 + version: 1.11.21 + debug: + specifier: 4.4.3 + version: 4.4.3 + mermaid: + specifier: 11.16.0 + version: 11.16.0 + vite: + specifier: ^5.4.14 + version: 5.4.21(@types/node@25.9.3)(lightningcss@1.32.0) vitepress: - specifier: ^1.6.3 - version: 1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(markdown-it-mathjax3@4.3.2)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3) - vue: - specifier: ^3.5.13 - version: 3.5.39(typescript@6.0.3) + specifier: ^1.6.4 + version: 1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3) + vitepress-plugin-mermaid: + specifier: ^2.0.17 + version: 2.0.17(mermaid@11.16.0)(vitepress@1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3)) packages: @@ -3088,6 +3197,9 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@braintree/sanitize-url@6.0.4': + resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} @@ -3606,6 +3718,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mermaid-js/mermaid-mindmap@9.3.0': + resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==} + '@mermaid-js/parser@1.2.0': resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==} @@ -4686,10 +4801,6 @@ packages: '@vueuse/shared@12.8.2': resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} - '@xmldom/xmldom@0.9.10': - resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} - engines: {node: '>=14.6'} - '@xterm/headless@5.5.0': resolution: {integrity: sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g==} @@ -4729,10 +4840,6 @@ packages: resolution: {integrity: sha512-OyacJsaeuLUvGWOynNqYc6sx88XvyoG39wMT8SYqL3l9wwaorDW/LPRbUPfhzw0bWsUWzNCZTnFYOrWFBKsUaw==} engines: {node: '>= 14.0.0'} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -4796,9 +4903,6 @@ packages: resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -4848,13 +4952,6 @@ packages: character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - cheerio-select@1.6.0: - resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==} - - cheerio@1.0.0-rc.10: - resolution: {integrity: sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==} - engines: {node: '>= 6'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -4869,18 +4966,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - commander@13.1.0: - resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} - engines: {node: '>=18'} - commander@15.0.0: resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} engines: {node: '>=22.12.0'} - commander@6.2.1: - resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} - engines: {node: '>= 6'} - commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -4948,17 +5037,10 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - css-select@4.3.0: - resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} - css-tree@3.2.1: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -5176,26 +5258,9 @@ packages: resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} - dom-serializer@1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@3.3.0: - resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==} - engines: {node: '>= 4'} - - domhandler@4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} - dompurify@3.4.11: resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} - domutils@2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} - dts-resolver@3.0.0: resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} engines: {node: ^22.18.0 || >=24.0.0} @@ -5235,9 +5300,6 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -5274,10 +5336,6 @@ packages: engines: {node: '>=18'} hasBin: true - escape-goat@3.0.0: - resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} - engines: {node: '>=10'} - escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -5320,10 +5378,6 @@ packages: jiti: optional: true - esm@3.2.25: - resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} - engines: {node: '>=6'} - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5586,12 +5640,6 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - htmlparser2@5.0.1: - resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} - - htmlparser2@6.1.0: - resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} - http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -5796,11 +5844,6 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - juice@8.1.0: - resolution: {integrity: sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==} - engines: {node: '>=10.0.0'} - hasBin: true - jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -5999,9 +6042,6 @@ packages: mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} - markdown-it-mathjax3@4.3.2: - resolution: {integrity: sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==} - markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -6019,10 +6059,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mathjax-full@3.2.2: - resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} - deprecated: Version 4 replaces this package with the scoped package @mathjax/src - mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -6066,9 +6102,6 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} - mensch@0.3.4: - resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==} - merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -6076,9 +6109,6 @@ packages: mermaid@11.16.0: resolution: {integrity: sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==} - mhchemparser@4.2.1: - resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} - micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -6171,11 +6201,6 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -6197,9 +6222,6 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - mj-context-menu@0.6.1: - resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -6295,21 +6317,12 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -6377,12 +6390,6 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - parse5-htmlparser2-tree-adapter@6.0.1: - resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} - - parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -6663,9 +6670,6 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - slick@1.12.2: - resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==} - smol-toml@1.6.1: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} @@ -6685,10 +6689,6 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} - speech-rule-engine@4.1.4: - resolution: {integrity: sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==} - hasBin: true - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -6779,9 +6779,6 @@ packages: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@6.0.0: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} @@ -6933,10 +6930,6 @@ packages: resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} hasBin: true - valid-data-url@3.0.1: - resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} - engines: {node: '>=10'} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -7026,6 +7019,12 @@ packages: yaml: optional: true + vitepress-plugin-mermaid@2.0.17: + resolution: {integrity: sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==} + peerDependencies: + mermaid: 10 || 11 + vitepress: ^1.0.0 || ^1.0.0-alpha + vitepress@1.6.4: resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==} hasBin: true @@ -7095,17 +7094,10 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} - web-resource-inliner@6.0.1: - resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==} - engines: {node: '>=10.0.0'} - web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -7118,9 +7110,6 @@ packages: resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -7131,9 +7120,6 @@ packages: engines: {node: '>=8'} hasBin: true - wicked-good-xpath@1.3.0: - resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} - word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -7612,6 +7598,9 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@braintree/sanitize-url@6.0.4': + optional: true + '@braintree/sanitize-url@7.1.2': {} '@bramus/specificity@2.4.2': @@ -8012,6 +8001,17 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mermaid-js/mermaid-mindmap@9.3.0': + dependencies: + '@braintree/sanitize-url': 6.0.4 + cytoscape: 3.34.0 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.0) + cytoscape-fcose: 2.2.0(cytoscape@3.34.0) + d3: 7.9.0 + khroma: 2.1.0 + non-layered-tidy-tree-layout: 2.0.2 + optional: true + '@mermaid-js/parser@1.2.0': dependencies: '@chevrotain/types': 11.1.2 @@ -8983,8 +8983,6 @@ snapshots: transitivePeerDependencies: - typescript - '@xmldom/xmldom@0.9.10': {} - '@xterm/headless@5.5.0': {} accepts@2.0.0: @@ -9035,8 +9033,6 @@ snapshots: '@algolia/requester-fetch': 5.55.2 '@algolia/requester-node-http': 5.55.2 - ansi-colors@4.1.3: {} - ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -9097,8 +9093,6 @@ snapshots: transitivePeerDependencies: - supports-color - boolbase@1.0.0: {} - bowser@2.14.1: {} brace-expansion@2.1.2: @@ -9137,24 +9131,6 @@ snapshots: character-entities@2.0.2: {} - cheerio-select@1.6.0: - dependencies: - css-select: 4.3.0 - css-what: 6.2.2 - domelementtype: 2.3.0 - domhandler: 4.3.1 - domutils: 2.8.0 - - cheerio@1.0.0-rc.10: - dependencies: - cheerio-select: 1.6.0 - dom-serializer: 1.4.1 - domhandler: 4.3.1 - htmlparser2: 6.1.0 - parse5: 6.0.1 - parse5-htmlparser2-tree-adapter: 6.0.1 - tslib: 2.8.1 - chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -9167,12 +9143,8 @@ snapshots: comma-separated-tokens@2.0.3: {} - commander@13.1.0: {} - commander@15.0.0: {} - commander@6.2.1: {} - commander@7.2.0: {} commander@8.3.0: {} @@ -9240,21 +9212,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-select@4.3.0: - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 4.3.1 - domutils: 2.8.0 - nth-check: 2.1.1 - css-tree@3.2.1: dependencies: mdn-data: 2.27.1 source-map-js: 1.2.1 - css-what@6.2.2: {} - csstype@3.2.3: {} cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.0): @@ -9484,32 +9446,10 @@ snapshots: diff@9.0.0: {} - dom-serializer@1.4.1: - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - entities: 2.2.0 - - domelementtype@2.3.0: {} - - domhandler@3.3.0: - dependencies: - domelementtype: 2.3.0 - - domhandler@4.3.1: - dependencies: - domelementtype: 2.3.0 - dompurify@3.4.11: optionalDependencies: '@types/trusted-types': 2.0.7 - domutils@2.8.0: - dependencies: - dom-serializer: 1.4.1 - domelementtype: 2.3.0 - domhandler: 4.3.1 - dts-resolver@3.0.0(oxc-resolver@11.20.0): optionalDependencies: oxc-resolver: 11.20.0 @@ -9538,8 +9478,6 @@ snapshots: encodeurl@2.0.0: {} - entities@2.2.0: {} - entities@7.0.1: {} entities@8.0.0: {} @@ -9611,8 +9549,6 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 - escape-goat@3.0.0: {} - escape-html@1.0.3: {} escape-string-regexp@4.0.0: {} @@ -9686,8 +9622,6 @@ snapshots: transitivePeerDependencies: - supports-color - esm@3.2.25: {} - espree@10.4.0: dependencies: acorn: 8.17.0 @@ -9999,20 +9933,6 @@ snapshots: html-void-elements@3.0.0: {} - htmlparser2@5.0.1: - dependencies: - domelementtype: 2.3.0 - domhandler: 3.3.0 - domutils: 2.8.0 - entities: 2.2.0 - - htmlparser2@6.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 4.3.1 - domutils: 2.8.0 - entities: 2.2.0 - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -10199,16 +10119,6 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 - juice@8.1.0: - dependencies: - cheerio: 1.0.0-rc.10 - commander: 6.2.1 - mensch: 0.3.4 - slick: 1.12.2 - web-resource-inliner: 6.0.1 - transitivePeerDependencies: - - encoding - jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -10383,13 +10293,6 @@ snapshots: mark.js@8.11.1: {} - markdown-it-mathjax3@4.3.2: - dependencies: - juice: 8.1.0 - mathjax-full: 3.2.2 - transitivePeerDependencies: - - encoding - markdown-table@3.0.4: {} marked@16.4.2: {} @@ -10398,13 +10301,6 @@ snapshots: math-intrinsics@1.1.0: {} - mathjax-full@3.2.2: - dependencies: - esm: 3.2.25 - mhchemparser: 4.2.1 - mj-context-menu: 0.6.1 - speech-rule-engine: 4.1.4 - mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -10523,8 +10419,6 @@ snapshots: media-typer@1.1.0: {} - mensch@0.3.4: {} - merge-descriptors@2.0.0: {} mermaid@11.16.0: @@ -10551,8 +10445,6 @@ snapshots: ts-dedent: 2.3.0 uuid: 14.0.1 - mhchemparser@4.2.1: {} - micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -10750,8 +10642,6 @@ snapshots: dependencies: mime-db: 1.54.0 - mime@2.6.0: {} - minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 @@ -10768,8 +10658,6 @@ snapshots: mitt@3.0.1: {} - mj-context-menu@0.6.1: {} - mri@1.2.0: {} ms@2.1.3: {} @@ -10844,19 +10732,14 @@ snapshots: node-domexception@1.0.0: {} - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 + non-layered-tidy-tree-layout@2.0.2: + optional: true object-assign@4.1.1: {} @@ -10958,12 +10841,6 @@ snapshots: pako@1.0.11: {} - parse5-htmlparser2-tree-adapter@6.0.1: - dependencies: - parse5: 6.0.1 - - parse5@6.0.1: {} - parse5@8.0.1: dependencies: entities: 8.0.0 @@ -11322,8 +11199,6 @@ snapshots: sisteransi@1.0.5: {} - slick@1.12.2: {} - smol-toml@1.6.1: {} source-map-js@1.2.1: {} @@ -11334,12 +11209,6 @@ snapshots: speakingurl@14.0.1: {} - speech-rule-engine@4.1.4: - dependencies: - '@xmldom/xmldom': 0.9.10 - commander: 13.1.0 - wicked-good-xpath: 1.3.0 - stackback@0.0.2: {} statuses@2.0.2: {} @@ -11420,8 +11289,6 @@ snapshots: dependencies: tldts: 7.4.5 - tr46@0.0.3: {} - tr46@6.0.0: dependencies: punycode: 2.3.1 @@ -11551,8 +11418,6 @@ snapshots: uuid@14.0.1: {} - valid-data-url@3.0.1: {} - vary@1.1.2: {} vfile-message@4.0.3: @@ -11615,7 +11480,14 @@ snapshots: tsx: 4.22.4 yaml: 2.9.0 - vitepress@1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(markdown-it-mathjax3@4.3.2)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3): + vitepress-plugin-mermaid@2.0.17(mermaid@11.16.0)(vitepress@1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3)): + dependencies: + mermaid: 11.16.0 + vitepress: 1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3) + optionalDependencies: + '@mermaid-js/mermaid-mindmap': 9.3.0 + + vitepress@1.6.4(@algolia/client-search@5.55.2)(@types/node@25.9.3)(lightningcss@1.32.0)(postcss@8.5.15)(search-insights@2.17.3)(typescript@6.0.3): dependencies: '@docsearch/css': 3.8.2 '@docsearch/js': 3.8.2(@algolia/client-search@5.55.2)(search-insights@2.17.3) @@ -11636,7 +11508,6 @@ snapshots: vite: 5.4.21(@types/node@25.9.3)(lightningcss@1.32.0) vue: 3.5.39(typescript@6.0.3) optionalDependencies: - markdown-it-mathjax3: 4.3.2 postcss: 8.5.15 transitivePeerDependencies: - '@algolia/client-search' @@ -11740,21 +11611,8 @@ snapshots: walk-up-path@4.0.0: {} - web-resource-inliner@6.0.1: - dependencies: - ansi-colors: 4.1.3 - escape-goat: 3.0.0 - htmlparser2: 5.0.1 - mime: 2.6.0 - node-fetch: 2.7.0 - valid-data-url: 3.0.1 - transitivePeerDependencies: - - encoding - web-streams-polyfill@3.3.3: {} - webidl-conversions@3.0.1: {} - webidl-conversions@8.0.1: {} whatwg-mimetype@5.0.0: {} @@ -11767,11 +11625,6 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -11781,8 +11634,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wicked-good-xpath@1.3.0: {} - word-wrap@1.2.5: {} wordwrap@1.0.0: {} diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index 2d3560ec5c..6f34f2b15b 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -32,12 +32,14 @@ "@deepseek-ai/dsh-jsonrpc-demo": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", + "@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^", "@deepseek-ai/dsh-llm-deepseek": "workspace:^", "@deepseek-ai/dsh-llm-pi-ai": "workspace:^", "@deepseek-ai/dsh-permission": "workspace:^", "@deepseek-ai/dsh-paths": "workspace:^", "@deepseek-ai/dsh-repeat-tool-guard": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", diff --git a/scripts/cordis-core-api.spec.ts b/scripts/cordis-core-api.spec.ts new file mode 100644 index 0000000000..d35899553c --- /dev/null +++ b/scripts/cordis-core-api.spec.ts @@ -0,0 +1,49 @@ +/** Tests for the generated Cordis core API reference. */ + +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { + CORDIS_CORE_API_PAGES, + renderCordisCoreApiPage, + renderCordisCoreApiPages, + type CordisCoreApiPage, +} from './cordis-core-api.ts' + +const roots: string[] = [] + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +describe('Cordis core API generation', () => { + it('renders the five detailed pages from pinned vendor declarations', () => { + const pages = renderCordisCoreApiPages() + expect([...pages.keys()]).toEqual(CORDIS_CORE_API_PAGES.map(page => page.out)) + expect(pages.get('docs/cordis-catalog/core/context.md')).toContain('### ctx.extend(meta?)') + expect(pages.get('docs/cordis-catalog/core/events.md')).toContain('## DispatchMode') + expect(pages.get('docs/cordis-catalog/core/fiber.md')).toContain('## EffectMeta') + expect(pages.get('docs/cordis-catalog/core/registry.md')).toContain('## Plugin') + expect(pages.get('docs/cordis-catalog/core/service.md')).toContain('### Service.resolveConfig') + + const fiber = pages.get('docs/cordis-catalog/core/fiber.md') ?? '' + expect(fiber).toContain('```\n\nRegister a cleanup-aware effect on this fiber.') + expect(fiber).toContain('- `execute` — the effect body; see `Effect` for accepted shapes.') + expect(fiber).toContain('**Returns** a disposer that tears the effect down and settles once done.') + }) + + it('rejects a public core class without source JSDoc', () => { + const root = mkdtempSync(join(tmpdir(), 'dsh-cordis-core-api-')) + roots.push(root) + mkdirSync(join(root, 'vendor/cordis/src'), { recursive: true }) + writeFileSync(join(root, 'vendor/cordis/src/service.ts'), 'export class Service {\n run(): string { return "ok" }\n}\n') + const page: CordisCoreApiPage = { + out: 'docs/cordis-catalog/core/service.md', + title: 'Service', + intro: 'Service API.', + sections: [{ kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' }], + } + expect(() => renderCordisCoreApiPage(page, root)).toThrow('class Service') + }) +}) diff --git a/scripts/cordis-core-api.ts b/scripts/cordis-core-api.ts new file mode 100644 index 0000000000..a2400fdb54 --- /dev/null +++ b/scripts/cordis-core-api.ts @@ -0,0 +1,433 @@ +/** Generate detailed Cordis core API pages from pinned vendor declarations. */ + +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import ts from 'typescript' +import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations } from './jsdoc.ts' +import { cordisModuleBody } from './cordis-walk.ts' + +const root = resolve(import.meta.dirname, '..') +const FENCE = 'ts cordis-catalog' + +/** One declaration group rendered on a Cordis core API page. */ +type CordisCoreApiSection = + | { kind: 'class'; file: string; symbol: string; prefix?: string; heading?: string } + | { kind: 'context-merge'; file: string; heading?: string } + | { kind: 'decl'; file: string; symbol: string } + +/** One generated Cordis core API page. */ +export interface CordisCoreApiPage { + out: string + title: string + intro: string + sections: CordisCoreApiSection[] +} + +/** Explicit editorial grouping for the pinned Cordis core surface. */ +export const CORDIS_CORE_API_PAGES: CordisCoreApiPage[] = [ + { + out: 'docs/cordis-catalog/core/context.md', + title: 'Context', + intro: 'The context is the core Cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods are documented on [Events](events.md), effects and the current fiber on [Fiber](fiber.md), and plugin loading on [Registry](registry.md).', + sections: [ + { kind: 'class', file: 'vendor/cordis/src/context.ts', symbol: 'Context', prefix: 'ctx.' }, + { kind: 'context-merge', file: 'vendor/cordis/src/reflect.ts', heading: 'Service store and mixins' }, + ], + }, + { + out: 'docs/cordis-catalog/core/events.md', + title: 'Events', + intro: 'The event-dispatch API mixed into every context. Harness event declarations and their dispatch modes are generated separately in the [Cordis events catalog](../events.md).', + sections: [ + { kind: 'context-merge', file: 'vendor/cordis/src/events.ts' }, + { kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'EventOptions' }, + { kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'DispatchMode' }, + ], + }, + { + out: 'docs/cordis-catalog/core/fiber.md', + title: 'Fiber', + intro: 'A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber, and `ctx.effect()` delegates to it.', + sections: [ + { kind: 'context-merge', file: 'vendor/cordis/src/fiber.ts' }, + { kind: 'class', file: 'vendor/cordis/src/fiber.ts', symbol: 'Fiber', heading: 'The Fiber class' }, + { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'Effect' }, + { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'Disposable' }, + { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'EffectMeta' }, + { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'CordisError' }, + { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'ValidationError' }, + ], + }, + { + out: 'docs/cordis-catalog/core/registry.md', + title: 'Registry', + intro: 'Plugin loading and dependency injection.', + sections: [ + { kind: 'context-merge', file: 'vendor/cordis/src/registry.ts' }, + { kind: 'decl', file: 'vendor/cordis/src/registry.ts', symbol: 'Plugin' }, + { kind: 'decl', file: 'vendor/cordis/src/registry.ts', symbol: 'Inject' }, + ], + }, + { + out: 'docs/cordis-catalog/core/service.md', + title: 'Service', + intro: 'The base class for context services. A subclass loaded as a plugin registers itself as `ctx.`.', + sections: [ + { kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' }, + ], + }, +] + +interface MemberDoc { + name: string + heading: string + signatures: string[] + jsDoc: string + doc: string + params: { name: string; text: string }[] + returns: string | null + source: string +} + +interface RenderContext { + scanRoot: string + cache: Map + violations: string[] +} + +function load(ctx: RenderContext, rel: string): { sf: ts.SourceFile; text: string } { + const cached = ctx.cache.get(rel) + if (cached !== undefined) return cached + const text = readFileSync(resolve(ctx.scanRoot, rel), 'utf8') + const entry = { sf: ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true), text } + ctx.cache.set(rel, entry) + return entry +} + +function sourceJsDoc(text: string, sf: ts.SourceFile, node: ts.Node): string { + const raw = rawJsDoc(text, node) + if (raw === '') return '' + const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf)) + const lineStart = sf.getPositionOfLineAndCharacter(line, 0) + const indent = text.slice(lineStart, node.getStart(sf)) + return raw.split('\n') + .map((sourceLine, index) => index > 0 && sourceLine.startsWith(indent) + ? sourceLine.slice(indent.length) + : sourceLine) + .join('\n') +} + +function signatureOf(member: ts.Node, sf: ts.SourceFile): string { + const full = member.getText(sf) + const tail = (member as { body?: ts.Node; initializer?: ts.Node }).body + ?? (member as { initializer?: ts.Node }).initializer + const signature = tail + ? full.slice(0, full.length - tail.getText(sf).length).replace(/[=\s]+$/, '') + : full + return signature.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim() +} + +function headingParams(parameters: readonly ts.ParameterDeclaration[], sf: ts.SourceFile): string { + const names = parameters + .filter(parameter => !(ts.isIdentifier(parameter.name) && parameter.name.text === 'this')) + .map((parameter) => { + const rest = parameter.dotDotDotToken ? '...' : '' + const optional = parameter.questionToken || parameter.initializer ? '?' : '' + return `${rest}${parameter.name.getText(sf)}${optional}` + }) + return `(${names.join(', ')})` +} + +function isPublicInstance(member: ts.ClassElement): boolean { + const modifiers = ts.getCombinedModifierFlags(member) + if (modifiers & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected | ts.ModifierFlags.Static)) return false + if (!member.name || ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false + return !member.name.getText().startsWith('_') +} + +function isPublicStatic(member: ts.ClassElement): boolean { + const modifiers = ts.getCombinedModifierFlags(member) + if (modifiers & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) return false + if (!(modifiers & ts.ModifierFlags.Static)) return false + if (!member.name || ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false + return !member.name.getText().startsWith('_') +} + +type Member = ts.MethodDeclaration + | ts.MethodSignature + | ts.PropertyDeclaration + | ts.PropertySignature + | ts.GetAccessorDeclaration + +function memberDoc(ctx: RenderContext, where: string, name: string, group: Member[], rel: string): MemberDoc { + const { sf, text } = load(ctx, rel) + const first = group[0] + if (first === undefined) throw new Error(`cordis-core-api: empty member group for ${name}.`) + const rawDocs = group.map(member => sourceJsDoc(text, sf, member)) + const docIndex = rawDocs.findIndex(raw => parseJsDoc(raw).doc !== '') + const raw = docIndex === -1 ? '' : (rawDocs[docIndex] ?? '') + const doc = parseJsDoc(raw).doc + if (doc === '') ctx.violations.push(`${where} has no JSDoc prose.`) + const { params: tags, returns } = parseTags(raw) + const functionMembers = group.filter((member): member is ts.MethodDeclaration | ts.MethodSignature => + ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) + const docCarrier = functionMembers[docIndex === -1 ? 0 : docIndex] + const params: { name: string; text: string }[] = [] + if (docCarrier !== undefined) { + checkParams(where, 'cordis-core-api', docCarrier.parameters, tags, sf, + parameter => ts.isIdentifier(parameter.name) && parameter.name.text === 'this', ctx.violations) + if (docCarrier.type !== undefined) { + checkReturns(where, docCarrier.type, returns, sf, ctx.violations) + } else if (returns === null && ts.isMethodDeclaration(docCarrier)) { + ctx.violations.push(`${where} has no return type annotation; document the result with @returns.`) + } + for (const parameter of docCarrier.parameters) { + if (!ts.isIdentifier(parameter.name) || parameter.name.text === 'this') continue + const text = tags.get(parameter.name.text) + if (text !== undefined) params.push({ name: parameter.name.text, text }) + } + } + const headingSource = docCarrier ?? functionMembers[0] + const signatures = ts.isMethodDeclaration(first) && functionMembers.length > 1 + ? functionMembers.filter(member => ts.isMethodDeclaration(member) && member.body === undefined) + : group + return { + name, + heading: headingSource === undefined ? '' : headingParams(headingSource.parameters, sf), + signatures: signatures.map(member => signatureOf(member, sf)), + jsDoc: raw, + doc, + params, + returns, + source: pointer(rel, sf, first), + } +} + +function heritageMembers( + statement: ts.InterfaceDeclaration, + sf: ts.SourceFile, + groups: Map, +): void { + for (const clause of statement.heritageClauses ?? []) { + for (const type of clause.types) { + if (!ts.isIdentifier(type.expression) || type.expression.text !== 'Pick') continue + const [target, keys] = type.typeArguments ?? [] + if (target === undefined || keys === undefined || !ts.isTypeReferenceNode(target)) continue + const targetName = target.typeName.getText(sf) + const cls = sf.statements.find( + (entry): entry is ts.ClassDeclaration => ts.isClassDeclaration(entry) && entry.name?.text === targetName, + ) + if (cls === undefined) continue + const picked = new Set() + const collect = (node: ts.TypeNode): void => { + if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) picked.add(node.literal.text) + if (ts.isUnionTypeNode(node)) node.types.forEach(collect) + } + collect(keys) + for (const member of cls.members) { + if (!ts.isMethodDeclaration(member)) continue + const name = member.name.getText(sf) + if (!picked.has(name)) continue + const group = groups.get(name) ?? [] + group.push(member) + groups.set(name, group) + } + } + } +} + +function contextMergeMembers(ctx: RenderContext, rel: string): MemberDoc[] { + const { sf } = load(ctx, rel) + const body = cordisModuleBody(sf) + if (body === null) throw new Error(`cordis-core-api: ${rel} has no Context module merge.`) + const groups = new Map() + for (const statement of body.statements) { + if (!ts.isInterfaceDeclaration(statement) || statement.name.text !== 'Context') continue + heritageMembers(statement, sf, groups) + for (const member of statement.members) { + if (!ts.isMethodSignature(member) && !ts.isPropertySignature(member)) continue + if (ts.isComputedPropertyName(member.name)) continue + const name = member.name.getText(sf) + const group = groups.get(name) ?? [] + group.push(member) + groups.set(name, group) + } + } + return [...groups.entries()].map(([name, group]) => + memberDoc(ctx, `ctx.${name} (${rel})`, name, group, rel)) +} + +function classMembers(ctx: RenderContext, rel: string, className: string): { + doc: string + instance: MemberDoc[] + statics: MemberDoc[] + source: string +} { + const { sf, text } = load(ctx, rel) + const cls = sf.statements.find( + (statement): statement is ts.ClassDeclaration => + ts.isClassDeclaration(statement) && statement.name?.text === className, + ) + if (cls === undefined) throw new Error(`cordis-core-api: class ${className} not found in ${rel}.`) + const doc = parseJsDoc(rawJsDoc(text, cls)).doc + if (doc === '') ctx.violations.push(`class ${className} (${pointer(rel, sf, cls)}) has no JSDoc.`) + const instance = new Map() + const statics = new Map() + for (const member of cls.members) { + if (!ts.isMethodDeclaration(member) && !ts.isPropertyDeclaration(member) && !ts.isGetAccessorDeclaration(member)) continue + const name = member.name.getText(sf) + if (isPublicInstance(member)) { + const group = instance.get(name) ?? [] + group.push(member) + instance.set(name, group) + } else if (isPublicStatic(member) && !ts.isGetAccessorDeclaration(member)) { + const group = statics.get(name) ?? [] + group.push(member) + statics.set(name, group) + } + } + const declaration = sf.statements.find( + (statement): statement is ts.InterfaceDeclaration => + ts.isInterfaceDeclaration(statement) && statement.name.text === className, + ) + for (const member of declaration?.members ?? []) { + if (!ts.isPropertySignature(member) || ts.isComputedPropertyName(member.name)) continue + const name = member.name.getText(sf) + const group = instance.get(name) ?? [] + group.push(member) + instance.set(name, group) + } + const render = (groups: Map, prefix: string): MemberDoc[] => + [...groups.entries()].map(([name, group]) => memberDoc(ctx, `${prefix}${name} (${rel})`, name, group, rel)) + return { + doc, + instance: render(instance, `${className}#`), + statics: render(statics, `${className}.`), + source: pointer(rel, sf, cls), + } +} + +function stripBodies(node: ts.Node, sf: ts.SourceFile): string { + const cuts: { start: number; end: number }[] = [] + const visit = (entry: ts.Node): void => { + const functionLike = ts.isMethodDeclaration(entry) + || ts.isConstructorDeclaration(entry) + || ts.isFunctionDeclaration(entry) + || ts.isGetAccessorDeclaration(entry) + || ts.isSetAccessorDeclaration(entry) + if (functionLike && entry.body !== undefined) { + const signatureEnd = (entry.type ?? entry.parameters.at(-1) ?? entry).getEnd() + cuts.push({ start: signatureEnd, end: entry.body.getEnd() }) + return + } + entry.forEachChild(visit) + } + visit(node) + const base = node.getStart(sf) + let output = node.getText(sf) + for (const cut of cuts.sort((left, right) => right.start - left.start)) { + const head = output.slice(0, cut.start - base) + const between = output.slice(cut.start - base, cut.end - base) + const bodyBrace = between.indexOf('{') + output = head + between.slice(0, bodyBrace).trimEnd() + output.slice(cut.end - base) + } + return output +} + +function declarationPaste(ctx: RenderContext, rel: string, symbol: string): { doc: string; code: string; source: string } { + const { sf, text } = load(ctx, rel) + const matches = sf.statements.filter((statement) => { + const named = ts.isInterfaceDeclaration(statement) + || ts.isTypeAliasDeclaration(statement) + || ts.isClassDeclaration(statement) + || ts.isEnumDeclaration(statement) + || ts.isModuleDeclaration(statement) + return named && statement.name?.getText(sf) === symbol + }) + const first = matches[0] + if (first === undefined) throw new Error(`cordis-core-api: declaration ${symbol} not found in ${rel}.`) + const doc = parseJsDoc(sourceJsDoc(text, sf, first)).doc + const code = matches.map((statement) => { + const jsDoc = sourceJsDoc(text, sf, statement) + const declaration = stripBodies(statement, sf).replace(/^export\s+(default\s+)?/, '') + return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}` + }).join('\n\n') + return { doc, code, source: pointer(rel, sf, first) } +} + +function sourceLink(source: string): string { + const [file, line] = source.split(':') + return `[Source](../../../${file}${line === undefined ? '' : `#L${line}`})` +} + +function unlink(text: string): string { + return text.replace(/\{@link\s+([^}|\s]+)\s*(?:[|\s]\s*([^}]*))?\}/g, (_match, target: string, label?: string) => { + const name = label?.trim() + return name && name !== '' ? name : `\`${target}\`` + }) +} + +function prose(doc: string): string[] { + const paragraphs = unlink(doc) + .split(/\n\s*\n/) + .map(paragraph => paragraph.replace(/\s*\n\s*/g, ' ').trim()) + .filter(paragraph => paragraph !== '') + return paragraphs.flatMap((paragraph, index) => index === 0 ? [paragraph] : ['', paragraph]) +} + +function renderMember(prefix: string, member: MemberDoc): string[] { + const lines = [`### ${prefix}${member.name}${member.heading}`, '', `\`\`\`${FENCE}`] + if (member.jsDoc !== '') lines.push(member.jsDoc) + lines.push(...member.signatures, '```', '') + if (member.doc !== '') lines.push(...prose(member.doc), '') + for (const parameter of member.params) lines.push(`- \`${parameter.name}\` — ${unlink(parameter.text)}`) + if (member.params.length > 0) lines.push('') + if (member.returns !== null && member.returns !== '') lines.push(`**Returns** ${unlink(member.returns)}`, '') + lines.push(sourceLink(member.source), '') + return lines +} + +/** Render one detailed Cordis core API page and reject undocumented members. */ +export function renderCordisCoreApiPage( + page: CordisCoreApiPage, + scanRoot: string = root, +): string { + const ctx: RenderContext = { scanRoot, cache: new Map(), violations: [] } + const lines = [ + '', + '', + `# ${page.title}`, + '', + page.intro, + '', + ] + for (const section of page.sections) { + if (section.kind !== 'decl' && section.heading !== undefined) lines.push(`## ${section.heading}`, '') + if (section.kind === 'context-merge') { + for (const member of contextMergeMembers(ctx, section.file)) lines.push(...renderMember('ctx.', member)) + } else if (section.kind === 'class') { + const cls = classMembers(ctx, section.file, section.symbol) + if (cls.doc !== '') lines.push(...prose(cls.doc), '') + lines.push(sourceLink(cls.source), '') + const prefix = section.prefix ?? `${section.symbol.toLowerCase()}.` + for (const member of cls.instance) lines.push(...renderMember(prefix, member)) + if (cls.statics.length > 0) { + lines.push('## Static members', '') + for (const member of cls.statics) lines.push(...renderMember(`${section.symbol}.`, member)) + } + } else { + const declaration = declarationPaste(ctx, section.file, section.symbol) + lines.push(`## ${section.symbol}`, '') + if (declaration.doc !== '') lines.push(...prose(declaration.doc), '') + lines.push(`\`\`\`${FENCE}`, declaration.code, '```', '', sourceLink(declaration.source), '') + } + } + reportViolations('gen-cordis-catalog', ctx.violations) + return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n` +} + +/** Render every detailed Cordis core API page. */ +export function renderCordisCoreApiPages(scanRoot: string = root): Map { + return new Map(CORDIS_CORE_API_PAGES.map(page => [page.out, renderCordisCoreApiPage(page, scanRoot)])) +} diff --git a/scripts/cordis-walk.ts b/scripts/cordis-walk.ts index 44e87b2a21..f4f045b06d 100644 --- a/scripts/cordis-walk.ts +++ b/scripts/cordis-walk.ts @@ -1,10 +1,7 @@ /** - * Shared AST walkers for the cordis documentation generators - * (`gen-cordis-catalog.ts`, `gen-website-api.ts`): locating the cordis module - * merge in a source file, enumerating its `interface Events` members, and - * resolving the `interface Context` service keys to their service classes. - * One walk, two renderers — the catalog and the website page carry different - * prose but must agree on WHAT exists. + * AST walkers for the Cordis catalog generator: locate the Cordis module merge + * in a source file, enumerate its `interface Events` members, and resolve the + * `interface Context` service keys to their service classes. */ import ts from 'typescript' diff --git a/scripts/doc-budgets.manifest.json b/scripts/doc-budgets.manifest.json index d849248189..010f767c3e 100644 --- a/scripts/doc-budgets.manifest.json +++ b/scripts/doc-budgets.manifest.json @@ -1,7 +1,7 @@ { "AGENTS.md": 1600, "docs/AGENTS.md": 1150, - "docs/architecture.md": 1790, + "docs/architecture.md": 1800, "docs/cordis-primer.md": 600, "docs/defensive-patterns.md": 550, "docs/testing.md": 960, diff --git a/scripts/doc-typecheck.ts b/scripts/doc-typecheck.ts index 9149653e6d..6ddfdd8f8f 100644 --- a/scripts/doc-typecheck.ts +++ b/scripts/doc-typecheck.ts @@ -192,7 +192,7 @@ function remapBlockPaths(output: string, blocks: Block[]): string { }) } -const markdownGlobs = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md'] +const markdownGlobs = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md'] const files: string[] = [] for (const pattern of markdownGlobs) { diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index ac59617e98..d4a94b917d 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -5,9 +5,10 @@ * curated table below. `--check` verifies both committed artifacts. */ -import { globSync, readFileSync, writeFileSync } from 'node:fs' -import { resolve, sep } from 'node:path' +import { globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { dirname, resolve, sep } from 'node:path' import ts from 'typescript' +import { renderCordisCoreApiPages } from './cordis-core-api.ts' import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations, type Mode } from './jsdoc.ts' import { cordisModuleBody, eventMembers, serviceClasses } from './cordis-walk.ts' @@ -57,6 +58,7 @@ export const LINK_MAP: Record = { CodeRunResult: 'code-runtime.md', CompactionResult: 'compaction.md', CompactionTrigger: 'compaction.md', + PruneResult: 'compaction.md', FileReadOutcome: 'filesystem.md', FsDirEntry: 'filesystem.md', FsEditOutcome: 'filesystem.md', @@ -271,8 +273,7 @@ interface InheritedEntry { source: string } -// cordisModuleBody / eventMembers / serviceClasses live in cordis-walk.ts, -// shared with gen-website-api.ts — one walk, two renderers. +// cordisModuleBody / eventMembers / serviceClasses live in cordis-walk.ts. /** The signature text of a method-signature member (everything but a body). */ function memberSignature(member: ts.TypeElement | ts.ClassElement, sf: ts.SourceFile): string { @@ -510,7 +511,7 @@ export function renderEvents(events: EventEntry[]): string { '', GATE_NOTICE, '', - 'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely.', + 'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md).', '', 'Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).', '', @@ -545,7 +546,7 @@ export function renderServices(services: ServiceEntry[]): string { '', GATE_NOTICE, '', - 'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely.', + 'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. Detailed Context, Fiber, Registry, and Service APIs are generated in the [Cordis core API](core/context.md).', '', ] for (const s of services) lines.push(...renderService(s)) @@ -569,6 +570,7 @@ function main(): void { const outputs: [string, string][] = [ [OUT_EVENTS, renderEvents(collectEvents())], [OUT_SERVICES, renderServices(collectServices())], + ...renderCordisCoreApiPages(), ] if (process.argv.includes('--check')) { const stale: string[] = [] @@ -585,15 +587,19 @@ function main(): void { if (committed !== content) stale.push(out) } if (stale.length === 0) { - console.log(`gen-cordis-catalog: ${OUT_EVENTS} and ${OUT_SERVICES} are up to date.`) + console.log(`gen-cordis-catalog: ${outputs.length} generated file(s) are up to date.`) process.exit(0) } console.error(`gen-cordis-catalog: ${stale.join(' and ')} ${stale.length === 1 ? 'is' : 'are'} stale. Run \`pnpm run gen-cordis-catalog\` and commit the result.`) process.exit(1) } - for (const [out, content] of outputs) writeFileSync(resolve(root, out), content) - console.log(`gen-cordis-catalog: wrote ${OUT_EVENTS} and ${OUT_SERVICES}.`) + for (const [out, content] of outputs) { + const destination = resolve(root, out) + mkdirSync(dirname(destination), { recursive: true }) + writeFileSync(destination, content) + } + console.log(`gen-cordis-catalog: wrote ${outputs.length} generated file(s).`) } // Run only when invoked as a script, not when imported by a test. diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 95db287394..a900b6c704 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -96,6 +96,14 @@ const SERVICE_ROLES: ServiceRole[] = [ consumers: ['compact-basic'], note: 'Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements.', }, + { + key: 'toolResultPrune', + pkg: 'compact-tool-result-prune', + title: 'Model-free tool-result pruning', + mode: 'core', + consumers: ['compact-basic'], + note: 'Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction.', + }, { key: 'sessions', pkg: 'session', @@ -202,6 +210,15 @@ const SERVICE_ROLES: ServiceRole[] = [ consumers: ['bash-sandbox'], note: 'Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement.', }, + { + key: 'sandboxPolicy', + pkg: 'sandbox-policy', + title: 'Sandbox policy home', + mode: 'core', + implementations: [], + consumers: ['bash-sandbox', 'fs-sandbox'], + note: 'The one home for the deployment default mode + workspace root; only the sandboxed executor and provider read the service (the tool layers use the pure `sandbox/mode` fold it also exports). Both enforcing families read it so bash and fs cannot confine to different roots.', + }, { key: 'approval', pkg: 'approval', @@ -234,10 +251,10 @@ const SERVICE_ROLES: ServiceRole[] = [ pkg: 'fs', title: 'Filesystem provider seam', mode: 'seam', - implementations: ['fs-local'], + implementations: ['fs-local', 'fs-sandbox'], consumers: ['tool-fs'], companions: ['fs-policy'], - note: 'tool-fs executes read/write/edit through ctx.fs; fs-policy contributes observed-state checks through the fs/* event gate.', + note: 'tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-policy contributes observed-state checks through the fs/* event gate.', }, { key: 'compact', @@ -440,7 +457,7 @@ const APP_EXAMPLES = [ title: 'REPL Agent App Composition', label: 'examples/repl-agent', config: 'examples/repl-agent/cordis.yml', - summary: 'The REPL agent demo adds the real DeepSeek adapter, filesystem tools, todo_write, compaction, and both subagent transports on top of the stdio app package.', + summary: 'The REPL agent demo adds the real DeepSeek adapter, filesystem tools, todo_write, tool-result pruning, compaction, and both subagent transports on top of the stdio app package.', }, { id: 'tui', @@ -908,7 +925,7 @@ function renderLifecycle(): string { '', 'The `assistant/message` edge records every successful provider call, including content-less and `max-tokens` finishes. Empty content stays out of derived history while the durable anchor retains usage and exact chunk provenance, including an explicit empty source set.', '', - '`dsh-compact-basic` uses `agent/post-step` for pressure after those durable facts and `agent/request-error` only for canonical context overflow. Recovery compacts between the closed failed step and a fresh retry step, and returns retry only when the surface replacement generation advances; otherwise the original request error remains authoritative.', + '`dsh-compact-basic` uses `agent/post-step` for pressure after those durable facts and `agent/request-error` only for canonical context overflow. Once either trigger qualifies, optional tool-result pruning runs before summary selection. Recovery works between the closed failed step and a fresh retry step, and returns retry only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.', '', 'SDK users that need replayable transcript data should consume `session/event`; `agent/*` is the live coordination surface for queue/status, prompt interception, request shaping, steering, continuation, and errors.', '', diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 287133454a..2bc9b78e23 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -79,11 +79,16 @@ class CatalogSearchBashExecutor extends BashExecutor { } } -/** Register the descriptor needed to mount schema-producing consumers. */ +/** + * Register the descriptor needed to mount schema-producing consumers. Declares + * the full capability set of the shipped in-process providers so consumers + * mount under their shipped defaults (tool-subagent's default numeric maxDepth + * requires `depthLimit`). + */ function registerCatalogSubagentProvider(ctx: Context, name: string): void { const provider: SubagentProvider = { name, - capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }, inheritsParentContext: false, start: () => Promise.reject(new Error('tool-catalog provider cannot start a child')), } diff --git a/scripts/gen-website-api.ts b/scripts/gen-website-api.ts deleted file mode 100644 index 480c19779f..0000000000 --- a/scripts/gen-website-api.ts +++ /dev/null @@ -1,757 +0,0 @@ -/** - * Generate (and verify) the website API reference under `website/zh-CN/api/`. - * - * The website's API section is FULLY GENERATED from source — never hand-edit - * it. The hand-written hub `api/index.md` sits OUTSIDE the generated subdirs - * (`api/cordis/`, `api/harness/`), so the orphan sweep never touches it. Two tiers: - * - * - `api/cordis/*` — the vendored cordis framework surface (Context, Events, - * Fiber, Registry, Service), driven by the CORDIS_PAGES manifest below. - * Members come from the real class declarations and the `declare module - * './context.ts'` interface merges (the typed `ctx.*` surface a plugin - * author actually sees). - * - `api/harness/*` — one page per `ctx.` harness service (walked from - * every `declare module 'cordis'` Context merge under `packages///src`), - * plus `events.md` listing every harness event grouped by scope. - * - * Prose comes from the JSDoc; the generator HARD-ERRORS (aggregated) when a - * rendered member lacks a summary, a parameter lacks `@param`, or a non-void - * annotated return lacks `@returns` — so a vendor sync or a new service method - * cannot land undocumented without CI going red. Pages are English (the - * planned zh translation flow arrives separately; see docs/i18n/README.md). - * - * Signature fences use the ` ```ts website-api ` info string and retain the - * declaration's original source JSDoc. doc-typecheck only processes its known - * info strings, so these bare (non-compilable) fragments are skipped there, - * while VitePress still highlights the `ts` token. The sidebar fragment - * `website/.vitepress/config/api-sidebar.json` is generated alongside so - * navigation can never drift from the page set. - * - * `tsx scripts/gen-website-api.ts` → write pages + sidebar - * `tsx scripts/gen-website-api.ts --check` → exit 1 if committed copies are - * stale (doc-sync / CI gate) - */ - -import { globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { dirname, resolve } from 'node:path' -import ts from 'typescript' -import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations, type Mode } from './jsdoc.ts' -import { cordisModuleBody, eventMembers, serviceClasses } from './cordis-walk.ts' - -const root = resolve(import.meta.dirname, '..') - -/** Output roots: generated pages and the generated sidebar fragment. */ -const PAGES_DIR = 'website/zh-CN/api' -const SIDEBAR_OUT = 'website/.vitepress/config/api-sidebar.json' - -/** GitHub blob base for source links on the public site (repo-relative paths - * do not resolve on the built site, unlike the in-repo catalogs). */ -const GITHUB = 'https://github.com/deepseek-harness/deepseek-harness/blob/master' - -/** Signature-fence info string (skipped by doc-typecheck, highlighted as ts). */ -const FENCE = 'ts website-api' - -/** Return sorted repository-relative glob matches with stable URL separators. */ -function repoGlob(pattern: string): string[] { - return globSync(pattern, { cwd: root }).map(rel => rel.replaceAll('\\', '/')).sort() -} - -/** One rendered member: a method/property plus its parsed JSDoc. */ -interface MemberDoc { - /** Display name, e.g. `on` or `agent/pre-step`. */ - name: string - /** Heading suffix with parameter names, e.g. `(name, listener, options?)`; - * empty for properties. */ - heading: string - /** All overload signature lines (bodies stripped). */ - signatures: string[] - /** Original source JSDoc, dedented only from its containing declaration. */ - jsDoc: string - /** Description prose, one paragraph per line. */ - doc: string - /** Parameter name → `@param` text, in declaration order. */ - params: { name: string; text: string }[] - /** `@returns` text, or null for void/undocumented. */ - returns: string | null - /** Repo-relative `file:line` of the (first) declaration. */ - source: string -} - -/** A cordis-page section: which declarations it renders. */ -type Section = - | { kind: 'class'; file: string; symbol: string; prefix?: string; heading?: string } - | { kind: 'context-merge'; file: string; heading?: string } - | { kind: 'decl'; file: string; symbol: string } - -/** One generated cordis page. */ -interface CordisPage { - out: string - title: string - intro: string - sections: Section[] -} - -/** - * The cordis tier manifest. Deliberately explicit (not a blind walk): the - * vendor `Context` mixes true plugin-author surface with internals, and page - * grouping is an editorial choice — but every member listed here is still - * EXTRACTED, never transcribed, so signatures and docs cannot drift. - */ -const CORDIS_PAGES: CordisPage[] = [ - { - out: 'cordis/context.md', - title: 'Context', - intro: 'The context is the core cordis object: every service, event, and lifecycle API is reached through `ctx`. Event methods (`ctx.on`, `ctx.emit`, …) are documented on [Events](./events.md); `ctx.effect` and `ctx.fiber` on [Fiber](./fiber.md); `ctx.plugin` and `ctx.inject` on [Registry](./registry.md).', - sections: [ - { kind: 'class', file: 'vendor/cordis/src/context.ts', symbol: 'Context', prefix: 'ctx.' }, - { kind: 'context-merge', file: 'vendor/cordis/src/reflect.ts', heading: 'Service store and mixins' }, - ], - }, - { - out: 'cordis/events.md', - title: 'Events', - intro: 'The event system mixed into every context. Harness-defined events are cataloged on [Harness events](../harness/events.md).', - sections: [ - { kind: 'context-merge', file: 'vendor/cordis/src/events.ts' }, - { kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'EventOptions' }, - { kind: 'decl', file: 'vendor/cordis/src/events.ts', symbol: 'DispatchMode' }, - ], - }, - { - out: 'cordis/fiber.md', - title: 'Fiber', - intro: 'A fiber is one loaded plugin instance: its lifecycle state, validated config, and registered effects. `ctx.fiber` is the current fiber; `ctx.effect()` delegates to it.', - sections: [ - { kind: 'context-merge', file: 'vendor/cordis/src/fiber.ts' }, - { kind: 'class', file: 'vendor/cordis/src/fiber.ts', symbol: 'Fiber', heading: 'The Fiber class' }, - { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'Effect' }, - { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'Disposable' }, - { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'EffectMeta' }, - { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'CordisError' }, - { kind: 'decl', file: 'vendor/cordis/src/fiber.ts', symbol: 'ValidationError' }, - ], - }, - { - out: 'cordis/registry.md', - title: 'Registry', - intro: 'Plugin loading and dependency injection.', - sections: [ - { kind: 'context-merge', file: 'vendor/cordis/src/registry.ts' }, - { kind: 'decl', file: 'vendor/cordis/src/registry.ts', symbol: 'Plugin' }, - { kind: 'decl', file: 'vendor/cordis/src/registry.ts', symbol: 'Inject' }, - ], - }, - { - out: 'cordis/service.md', - title: 'Service', - intro: 'Base class for context services: subclass it and load the subclass as a plugin to register `ctx.`.', - sections: [ - { kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' }, - ], - }, -] -// --------------------------------------------------------------------------- -// Extraction -// --------------------------------------------------------------------------- - -const sfCache = new Map() - -/** Parse (and cache) one repo-relative source file. */ -function load(rel: string): { sf: ts.SourceFile; text: string } { - const cached = sfCache.get(rel) - if (cached) return cached - const text = readFileSync(resolve(root, rel), 'utf8') - const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true) - const entry = { sf, text } - sfCache.set(rel, entry) - return entry -} -// The module-merge walk (cordisModuleBody / eventMembers / serviceClasses) is -// shared with gen-cordis-catalog.ts via cordis-walk.ts. - -/** Original JSDoc with only the source container's indentation removed. */ -function sourceJSDoc(text: string, sf: ts.SourceFile, node: ts.Node): string { - const raw = rawJsDoc(text, node) - if (raw === '') return '' - const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf)) - const lineStart = sf.getPositionOfLineAndCharacter(line, 0) - const indent = text.slice(lineStart, node.getStart(sf)) - return raw.split('\n') - .map((sourceLine, index) => index > 0 && sourceLine.startsWith(indent) - ? sourceLine.slice(indent.length) - : sourceLine) - .join('\n') -} - -/** Signature text of a member: full text minus body/initializer, whitespace - * collapsed, trailing semicolon stripped. */ -function signatureOf(member: ts.Node, sf: ts.SourceFile): string { - const full = member.getText(sf) - const tail = (member as { body?: ts.Node; initializer?: ts.Node }).body - ?? (member as { initializer?: ts.Node }).initializer - const sig = tail ? full.slice(0, full.length - tail.getText(sf).length).replace(/[=\s]+$/, '') : full - return sig.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim() -} - -/** `(a, b?, ...rest)` heading suffix from a parameter list, `this` dropped. */ -function headingParams(parameters: readonly ts.ParameterDeclaration[], sf: ts.SourceFile): string { - const names = parameters - .filter(p => !(ts.isIdentifier(p.name) && p.name.text === 'this')) - .map((p) => { - const dots = p.dotDotDotToken ? '...' : '' - const opt = p.questionToken || p.initializer ? '?' : '' - return `${dots}${p.name.getText(sf)}${opt}` - }) - return `(${names.join(', ')})` -} - -/** Whether a class member is renderable public API (non-static half). */ -function isPublicInstance(member: ts.ClassElement): boolean { - const mods = ts.getCombinedModifierFlags(member) - if (mods & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected | ts.ModifierFlags.Static)) return false - if (!member.name) return false - if (ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false - return !member.name.getText().startsWith('_') -} - -/** Whether a class member is renderable public STATIC API. */ -function isPublicStatic(member: ts.ClassElement): boolean { - const mods = ts.getCombinedModifierFlags(member) - if (mods & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) return false - if (!(mods & ts.ModifierFlags.Static)) return false - if (!member.name || ts.isComputedPropertyName(member.name) || ts.isPrivateIdentifier(member.name)) return false - return !member.name.getText().startsWith('_') -} - -/** Build a MemberDoc from a declaration group (overloads share one entry), - * collecting completeness violations for everything rendered. */ -function memberDoc( - where: string, - name: string, - group: (ts.MethodDeclaration | ts.MethodSignature | ts.PropertyDeclaration | ts.PropertySignature | ts.GetAccessorDeclaration)[], - rel: string, - violations: string[], -): MemberDoc { - const { sf, text } = load(rel) - const first = group[0] - if (!first) throw new Error(`gen-website-api: empty member group for ${name}`) - // Doc from the first overload that carries JSDoc prose. - const rawDocs = group.map(m => sourceJSDoc(text, sf, m)) - const docIndex = rawDocs.findIndex(r => parseJsDoc(r).doc !== '') - const raw = docIndex === -1 ? '' : (rawDocs[docIndex] ?? '') - const doc = parseJsDoc(raw).doc - if (!doc) violations.push(`${where} has no JSDoc prose.`) - const { params: tags, returns } = parseTags(raw) - const params: { name: string; text: string }[] = [] - let returnsText: string | null = null - const funcLike = group.filter((m): m is ts.MethodDeclaration | ts.MethodSignature => ts.isMethodDeclaration(m) || ts.isMethodSignature(m)) - const docCarrier = funcLike[docIndex === -1 ? 0 : docIndex] - if (docCarrier) { - checkParams(where, 'website-api', docCarrier.parameters, tags, sf, - p => ts.isIdentifier(p.name) && p.name.text === 'this', violations) - if (docCarrier.type) { - checkReturns(where, docCarrier.type, returns, sf, violations) - } else if (!returns && ts.isMethodDeclaration(docCarrier)) { - // Comment-only vendor policy: we cannot add a return type annotation to - // pinned upstream source, so an unannotated rendered method must carry - // an explicit @returns describing the result instead. - violations.push(`${where} has no return type annotation; document the result with @returns.`) - } - for (const p of docCarrier.parameters) { - if (ts.isIdentifier(p.name) && p.name.text === 'this') continue - const pname = p.name.getText(sf) - const tag = tags.get(pname) - if (tag) params.push({ name: pname, text: tag }) - } - returnsText = returns - } - const headingSource = docCarrier ?? funcLike[0] - return { - name, - heading: headingSource ? headingParams(headingSource.parameters, sf) : '', - signatures: (ts.isMethodDeclaration(first) && funcLike.length > 1 - ? funcLike.filter(m => ts.isMethodDeclaration(m) && !m.body) - : group).map(m => signatureOf(m, sf)), - jsDoc: raw, - doc, - params, - returns: returnsText, - source: pointer(rel, sf, first), - } -} - -/** Resolve an `extends Pick` heritage clause on the Context - * merge to the named members of `Class` declared in the same file — the fiber - * merge (`interface Context extends Pick`) is the motivating - * case: without this, `ctx.effect` had no documented signature anywhere. */ -function heritageMembers( - stmt: ts.InterfaceDeclaration, - sf: ts.SourceFile, - groups: Map, -): void { - for (const clause of stmt.heritageClauses ?? []) { - for (const type of clause.types) { - if (!ts.isIdentifier(type.expression) || type.expression.text !== 'Pick') continue - const [target, keys] = type.typeArguments ?? [] - if (!target || !keys || !ts.isTypeReferenceNode(target)) continue - const targetName = target.typeName.getText(sf) - const cls = sf.statements.find( - (s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === targetName, - ) - if (!cls) continue - const picked = new Set() - const collect = (node: ts.TypeNode): void => { - if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) picked.add(node.literal.text) - if (ts.isUnionTypeNode(node)) node.types.forEach(collect) - } - collect(keys) - for (const member of cls.members) { - if (!ts.isMethodDeclaration(member)) continue - const name = member.name.getText(sf) - if (!picked.has(name)) continue - const group = groups.get(name) ?? [] - group.push(member) - groups.set(name, group) - } - } - } -} - -/** Members of the `interface Context` merge in `rel`, overloads grouped; - * `Pick<…>` heritage resolved to the picked class members. */ -function contextMergeMembers(rel: string, violations: string[]): MemberDoc[] { - const { sf } = load(rel) - const body = cordisModuleBody(sf) - if (!body) throw new Error(`gen-website-api: ${rel} has no context module merge`) - const groups = new Map() - for (const stmt of body.statements) { - if (!ts.isInterfaceDeclaration(stmt) || stmt.name.text !== 'Context') continue - heritageMembers(stmt, sf, groups) - for (const member of stmt.members) { - if (!ts.isMethodSignature(member) && !ts.isPropertySignature(member)) continue - if (ts.isComputedPropertyName(member.name)) continue - const name = member.name.getText(sf) - const group = groups.get(name) ?? [] - group.push(member) - groups.set(name, group) - } - } - return [...groups.entries()].map(([name, group]) => - memberDoc(`ctx.${name} (${rel})`, name, group, rel, violations)) -} - -/** Instance + static members of one class, as two rendered lists. The class's - * same-named top-level interface half (declaration merging — vendor Context - * declares `root`/`events`/`logger`/… on the interface) is folded into the - * instance list, so neither half of a merged symbol goes undocumented. */ -function classMembers(rel: string, className: string, violations: string[]): { - doc: string - instance: MemberDoc[] - statics: MemberDoc[] - source: string -} { - const { sf, text } = load(rel) - const cls = sf.statements.find( - (s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === className, - ) - if (!cls) throw new Error(`gen-website-api: class ${className} not found in ${rel}`) - const clsDoc = parseJsDoc(rawJsDoc(text, cls)).doc - if (!clsDoc) violations.push(`class ${className} (${pointer(rel, sf, cls)}) has no JSDoc.`) - type Renderable = ts.MethodDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration | ts.PropertySignature - const instance = new Map() - const statics = new Map() - for (const member of cls.members) { - const renderable = ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member) || ts.isGetAccessorDeclaration(member) - if (!renderable) continue - const name = member.name.getText(sf) - if (isPublicInstance(member)) { - const group = instance.get(name) ?? [] - group.push(member) - instance.set(name, group) - } else if (isPublicStatic(member) && !ts.isGetAccessorDeclaration(member)) { - const group = statics.get(name) ?? [] - group.push(member) - statics.set(name, group) - } - } - const iface = sf.statements.find( - (s): s is ts.InterfaceDeclaration => ts.isInterfaceDeclaration(s) && s.name.text === className, - ) - for (const member of iface?.members ?? []) { - if (!ts.isPropertySignature(member)) continue - if (ts.isComputedPropertyName(member.name)) continue - const name = member.name.getText(sf) - const group = instance.get(name) ?? [] - group.push(member) - instance.set(name, group) - } - const toDocs = (groups: Map, prefix: string): MemberDoc[] => - [...groups.entries()].map(([name, group]) => - memberDoc(`${prefix}${name} (${rel})`, name, group, rel, violations)) - return { - doc: clsDoc, - instance: toDocs(instance, `${className}#`), - statics: toDocs(statics, `${className}.`), - source: pointer(rel, sf, cls), - } -} - -/** Splice every function-like BODY out of a declaration's text, leaving the - * signature (`) {` → `)`). A reference paste shows shapes, not implementation; - * property initializers (e.g. an `as const` code table) are data and stay. */ -function stripBodies(node: ts.Node, sf: ts.SourceFile): string { - const cuts: { start: number; end: number }[] = [] - const visit = (n: ts.Node): void => { - const funcLike = ts.isMethodDeclaration(n) || ts.isConstructorDeclaration(n) - || ts.isFunctionDeclaration(n) || ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n) - if (funcLike && n.body) { - // Cut from just after the parameter close (or return-type end) through - // the body, so `foo(a: string) { … }` renders as `foo(a: string)`. - const sigEnd = (n.type ?? n.parameters[n.parameters.length - 1] ?? n).getEnd() - // Find the `)` (and optional `: Type`) boundary: body start is exact. - cuts.push({ start: sigEnd, end: n.body.getEnd() }) - return // nothing renderable inside the body - } - n.forEachChild(visit) - } - visit(node) - const base = node.getStart(sf) - let out = node.getText(sf) - for (const cut of cuts.sort((a, b) => b.start - a.start)) { - const head = out.slice(0, cut.start - base) - // Keep everything of the signature up to the closing paren / return type, - // drop ` { … }`. The head may end mid-signature (last param), so retain - // the source between sigEnd and the body's `{` MINUS trailing space. - const between = out.slice(cut.start - base, cut.end - base) - const bodyBrace = between.indexOf('{') - out = head + between.slice(0, bodyBrace).trimEnd() + out.slice(cut.end - base) - } - return out -} - -/** Verbatim declaration paste: every top-level statement named `symbol` - * (class + merged namespace both), with leading JSDoc prose extracted and - * function bodies stripped (a reference shows shapes, not implementation). */ -function declPaste(rel: string, symbol: string): { doc: string; code: string; source: string } { - const { sf, text } = load(rel) - const matches = sf.statements.filter((s) => { - const named = ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s) - || ts.isClassDeclaration(s) || ts.isEnumDeclaration(s) || ts.isModuleDeclaration(s) - return named && s.name?.getText(sf) === symbol - }) - if (matches.length === 0) throw new Error(`gen-website-api: declaration ${symbol} not found in ${rel}`) - const first = matches[0] - if (!first) throw new Error(`gen-website-api: declaration ${symbol} not found in ${rel}`) - const firstJSDoc = sourceJSDoc(text, sf, first) - const doc = parseJsDoc(firstJSDoc).doc - const code = matches.map((statement) => { - const jsDoc = sourceJSDoc(text, sf, statement) - const declaration = stripBodies(statement, sf).replace(/^export\s+(default\s+)?/, '') - return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}` - }).join('\n\n') - return { doc, code, source: pointer(rel, sf, first) } -} - -/** One harness service with member-level detail. */ -interface HarnessService { - key: string - type: string - abstract: boolean - doc: string - members: MemberDoc[] - source: string - /** Owning npm package name (from the package.json beside the entry). */ - pkg: string -} - -/** Walk every harness `declare module 'cordis'` Context merge → services. */ -function collectHarnessServices(violations: string[]): HarnessService[] { - const services: HarnessService[] = [] - for (const rel of repoGlob('packages/*/*/src/index.ts')) { - const { sf, text } = load(rel) - if (!text.includes('interface Context')) continue - const body = cordisModuleBody(sf) - if (!body) continue - const pkgJson = resolve(root, dirname(dirname(rel)), 'package.json') - // Manifest shape is repo-owned; `name` is the one field read here. - const manifest = JSON.parse(readFileSync(pkgJson, 'utf8')) as { name: string } - const pkg = manifest.name - for (const { key, type, cls, abstract, doc: clsDoc } of serviceClasses(body, sf, rel, violations)) { - const groups = new Map() - for (const member of cls.members) { - // Public properties are API too: ctx.codeRuntime.language/isolation - // are readonly descriptors consumers key presentation off. - const renderable = ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member) || ts.isGetAccessorDeclaration(member) - if (!renderable) continue - if (!isPublicInstance(member)) continue - const name = member.name.getText(sf) - const group = groups.get(name) ?? [] - group.push(member) - groups.set(name, group) - } - const members = [...groups.entries()].map(([name, group]) => - memberDoc(`ctx.${key}.${name} (${rel})`, name, group, rel, violations)) - services.push({ key, type, abstract, doc: clsDoc, members, source: pointer(rel, sf, cls), pkg }) - } - } - return services.sort((a, b) => a.key.localeCompare(b.key)) -} - -/** One harness event with member-level detail. */ -interface HarnessEvent { - name: string - scope: string - mode: Mode | null - signature: string - /** Original source event JSDoc, dedented from its module/interface. */ - jsDoc: string - doc: string - params: { name: string; text: string }[] - source: string -} - -/** Walk every harness `interface Events` merge → events. */ -function collectHarnessEvents(violations: string[]): HarnessEvent[] { - const events: HarnessEvent[] = [] - for (const rel of repoGlob('packages/*/*/src/*.ts')) { - const { sf, text } = load(rel) - if (!text.includes('interface Events')) continue - const body = cordisModuleBody(sf) - if (!body) continue - for (const { name, member } of eventMembers(body, sf)) { - const raw = sourceJSDoc(text, sf, member) - const { doc, mode } = parseJsDoc(raw) - if (!mode) violations.push(`event '${name}' (${pointer(rel, sf, member)}) is missing @mode.`) - if (!doc) violations.push(`event '${name}' (${pointer(rel, sf, member)}) has no JSDoc prose.`) - const { params: tags } = parseTags(raw) - const last = member.parameters.at(-1) - const hasNext = !!last && last.name.getText(sf) === 'next' - checkParams(`event '${name}' (${pointer(rel, sf, member)})`, 'website-api', member.parameters, tags, sf, - p => (ts.isIdentifier(p.name) && p.name.text === 'this') || (hasNext && p === last), violations) - const params: { name: string; text: string }[] = [] - for (const p of member.parameters) { - const pname = p.name.getText(sf) - const tag = tags.get(pname) - if (tag) params.push({ name: pname, text: tag }) - } - events.push({ name, scope: name.split('/')[0] ?? name, mode, signature: signatureOf(member, sf), jsDoc: raw, doc, params, source: pointer(rel, sf, member) }) - } - } - return events.sort((a, b) => a.name.localeCompare(b.name)) -} - -// --------------------------------------------------------------------------- -// Rendering -// --------------------------------------------------------------------------- - -const BANNER = '' - -/** GitHub source link for a `file:line` pointer. */ -function sourceLink(source: string): string { - const [file, line] = source.split(':') - return `[Source](${GITHUB}/${file}#L${line})` -} - -/** Normalize JSDoc inline `{@link X}` / `{@link X|label}` / `{@link X label}` - * tags to plain Markdown code spans — left verbatim they leak into the built - * page as literal `{@link …}` text. */ -function unlink(text: string): string { - return text.replace(/\{@link\s+([^}|\s]+)\s*(?:[|\s]\s*([^}]*))?\}/g, (_m, target: string, label?: string) => { - const name = label?.trim() - return name && name !== '' ? name : `\`${target}\`` - }) -} - -/** Render prose paragraphs (one per line of `doc`), JSDoc links normalized. */ -function prose(doc: string): string[] { - return unlink(doc).split('\n').filter(l => l.trim() !== '') -} - -/** Render one member section at heading depth 3. */ -function renderMember(prefix: string, m: MemberDoc): string[] { - const lines: string[] = [] - const call = m.heading === '' ? '' : m.heading - lines.push(`### ${prefix}${m.name}${call}`, '') - lines.push('```' + FENCE) - lines.push(m.jsDoc) - for (const sig of m.signatures) lines.push(sig) - lines.push('```', '') - lines.push(...prose(m.doc), '') - if (m.params.length > 0) { - for (const p of m.params) lines.push(`- \`${p.name}\` — ${unlink(p.text)}`) - lines.push('') - } - if (m.returns) lines.push(`**Returns** ${unlink(m.returns)}`, '') - lines.push(sourceLink(m.source), '') - return lines -} - -/** Render one cordis-tier page from its manifest entry. */ -function renderCordisPage(page: CordisPage, violations: string[]): string { - const lines: string[] = [BANNER, '', `# ${page.title}`, '', page.intro, ''] - for (const section of page.sections) { - if (section.kind !== 'decl' && section.heading) lines.push(`## ${section.heading}`, '') - if (section.kind === 'context-merge') { - for (const m of contextMergeMembers(section.file, violations)) { - lines.push(...renderMember('ctx.', m)) - } - } else if (section.kind === 'class') { - const cls = classMembers(section.file, section.symbol, violations) - lines.push(...prose(cls.doc), '', sourceLink(cls.source), '') - const instancePrefix = section.prefix ?? `${section.symbol.toLowerCase()}.` - for (const m of cls.instance) lines.push(...renderMember(instancePrefix, m)) - if (cls.statics.length > 0) { - lines.push('## Static members', '') - for (const m of cls.statics) lines.push(...renderMember(`${section.symbol}.`, m)) - } - } else { - const decl = declPaste(section.file, section.symbol) - lines.push(`## ${section.symbol}`, '') - if (decl.doc) lines.push(...prose(decl.doc), '') - lines.push('```' + FENCE, decl.code, '```', '', sourceLink(decl.source), '') - } - } - return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n` -} - -/** kebab-case a ctx key: `agentLoop` → `agent-loop`. */ -function kebab(key: string): string { - return key.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`) -} - -/** Render one harness service page. */ -function renderServicePage(svc: HarnessService): string { - const seam = svc.abstract ? ' (abstract seam)' : '' - const lines: string[] = [ - BANNER, '', - `# ctx.${svc.key}`, '', - `\`${svc.type}\`${seam} — provided by \`${svc.pkg}\`.`, '', - ...prose(svc.doc), '', - sourceLink(svc.source), '', - ] - for (const m of svc.members) lines.push(...renderMember(`ctx.${svc.key}.`, m)) - return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n` -} - -/** Render the harness events page, grouped by scope. */ -function renderEventsPage(events: HarnessEvent[]): string { - const lines: string[] = [ - BANNER, '', - '# Harness events', '', - `Every event the harness packages declare on the cordis event bus (${events.length} total), grouped by scope. The **mode** is the dispatch semantics (\`emit\` fire-and-forget, \`parallel\` awaited, \`serial\` first-bail, \`waterfall\` veto-chain — a waterfall listener MUST call \`next()\` to delegate).`, '', - ] - const scopes = [...new Set(events.map(e => e.scope))].sort() - for (const scope of scopes) { - lines.push(`## ${scope}/*`, '') - for (const e of events.filter(ev => ev.scope === scope)) { - lines.push(`### ${e.name}`, '') - lines.push(`**Mode:** \`${e.mode ?? 'unknown'}\``, '') - lines.push('```' + FENCE, e.jsDoc, e.signature, '```', '') - lines.push(...prose(e.doc), '') - if (e.params.length > 0) { - for (const p of e.params) lines.push(`- \`${p.name}\` — ${unlink(p.text)}`) - lines.push('') - } - lines.push(sourceLink(e.source), '') - } - } - return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n` -} - -// --------------------------------------------------------------------------- -// Assembly + CLI -// --------------------------------------------------------------------------- - -/** Build every generated file as `relPath → content`. */ -export function generate(): Map { - const violations: string[] = [] - const files = new Map() - - for (const page of CORDIS_PAGES) { - files.set(`${PAGES_DIR}/${page.out}`, renderCordisPage(page, violations)) - } - - const services = collectHarnessServices(violations) - for (const svc of services) { - files.set(`${PAGES_DIR}/harness/${kebab(svc.key)}.md`, renderServicePage(svc)) - } - - const events = collectHarnessEvents(violations) - files.set(`${PAGES_DIR}/harness/events.md`, renderEventsPage(events)) - - for (const [rel, content] of files) { - if (!rel.endsWith('.md')) continue - for (const match of content.matchAll(/^```ts website-api\n([\s\S]*?)\n```$/gm)) { - const body = match[1] ?? '' - if (!body.startsWith('/**')) { - violations.push(`${rel}: a ts website-api fence does not begin with original source JSDoc.`) - } - } - } - - reportViolations('gen-website-api', violations) - - const sidebar = { - cordis: CORDIS_PAGES.map(p => ({ - text: p.title, - link: `/zh-CN/api/${p.out.replace(/\.md$/, '')}`, - })), - harness: [ - ...services.map(s => ({ text: `ctx.${s.key}`, link: `/zh-CN/api/harness/${kebab(s.key)}` })), - { text: 'Events', link: '/zh-CN/api/harness/events' }, - ], - } - files.set(SIDEBAR_OUT, `${JSON.stringify(sidebar, null, 2)}\n`) - return files -} - -/** CLI entry: default writes, `--check` fails on stale/orphan files. Guarded - * behind an entry-point check so tests can import `generate()`. */ -function main(): void { - const check = process.argv.includes('--check') - const files = generate() - - // Orphan detection: a generated-dir page that generate() no longer emits - // (e.g. a service was renamed) must be deleted, not left to rot. - const expected = new Set([...files.keys()]) - // Orphans live in the generated subdirs only; the hand-written api/index.md - // is one level up and never matches this glob. - const onDisk = repoGlob(`${PAGES_DIR}/{cordis,harness}/*.md`) - const orphans = onDisk.filter(rel => !expected.has(rel)) - - if (check) { - const stale: string[] = [] - for (const [rel, content] of files) { - let current: string | null = null - try { - current = readFileSync(resolve(root, rel), 'utf8') - } catch { - // Missing file: reported as stale below; readFileSync is the probe. - } - if (current !== content) stale.push(rel) - } - if (stale.length > 0 || orphans.length > 0) { - console.error('gen-website-api: website API reference is stale. Run `pnpm run gen-website-api` and commit the result.') - for (const rel of stale) console.error(` stale: ${rel}`) - for (const rel of orphans) console.error(` orphan (delete): ${rel}`) - process.exit(1) - } - console.log(`gen-website-api: ${files.size} generated file(s) fresh.`) - return - } - - for (const [rel, content] of files) { - const abs = resolve(root, rel) - mkdirSync(dirname(abs), { recursive: true }) - writeFileSync(abs, content) - } - for (const rel of orphans) { - console.log(`gen-website-api: orphan page ${rel} — delete it (no longer generated).`) - } - console.log(`gen-website-api: wrote ${files.size} file(s).`) -} - -// Run only when invoked as a script, not when imported by a test. -if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) { - main() -} diff --git a/scripts/md-fences.ts b/scripts/md-fences.ts index 8c84b27457..ad97164369 100644 --- a/scripts/md-fences.ts +++ b/scripts/md-fences.ts @@ -1,6 +1,6 @@ /** * Shared fenced-code-block extractor for the Markdown doc gates - * (`doc-typecheck.ts`, `verify-website-yaml.ts`). One scanner, per-gate + * (currently `doc-typecheck.ts`; future Markdown gates can share it). One scanner, per-gate * classification: each gate maps a fence info string (` ```ts `, * ` ```yaml ignore-check `, …) to its own kind tag and receives every * classified block with its 1-based opening-fence line. diff --git a/scripts/project-doc-site.spec.ts b/scripts/project-doc-site.spec.ts new file mode 100644 index 0000000000..bd6cfb14c7 --- /dev/null +++ b/scripts/project-doc-site.spec.ts @@ -0,0 +1,219 @@ +/** Tests for the documentation website projection adapter. */ + +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { docsPages, type DocsPage } from '../website/docs.ts' +import { addProjectionFrontmatter, projectedPageContent, rewriteMarkdown } from './project-doc-site.ts' + +const roots: string[] = [] + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +function fixture(): { root: string; pages: DocsPage[] } { + const root = mkdtempSync(join(tmpdir(), 'dsh-doc-site-')) + roots.push(root) + mkdirSync(join(root, 'docs'), { recursive: true }) + mkdirSync(join(root, 'packages'), { recursive: true }) + writeFileSync(join(root, 'docs/a.md'), '# A\n') + writeFileSync(join(root, 'docs/b.md'), '# B\n') + writeFileSync(join(root, 'docs/x(y).md'), '# Parentheses\n') + writeFileSync(join(root, 'packages/tool.ts'), 'one\ntwo\n') + writeFileSync(join(root, 'packages/logo.svg'), '\n') + return { + root, + pages: [ + { locale: 'root', contentLocale: 'en-US', source: 'docs/a.md', route: 'a.md', label: 'A', sidebar: 'zh-reference', section: 'Test', order: 1 }, + { locale: 'root', contentLocale: 'en-US', source: 'docs/b.md', route: 'reference-root/b.md', label: 'B', sidebar: 'zh-reference', section: 'Test', order: 2 }, + { locale: 'en', contentLocale: 'en-US', source: 'docs/a.md', route: 'en/a.md', label: 'A', sidebar: 'en-reference', section: 'Test', order: 1 }, + { locale: 'en', contentLocale: 'en-US', source: 'docs/b.md', route: 'en/reference/b.md', label: 'B', sidebar: 'en-reference', section: 'Test', order: 2 }, + ], + } +} + +describe('rewriteMarkdown', () => { + it('maps published pages and pins unpublished source links', () => { + const { root, pages } = fixture() + const source = '[B](b.md#part) [source](../packages/tool.ts:2) [web](https://example.com)\n' + expect(rewriteMarkdown(source, { + locale: 'en', + sourcePath: 'docs/a.md', + route: 'en/a.md', + pages, + repoRoot: root, + repositoryRef: 'abc123', + })).toBe( + '[B](./reference/b.md#part) ' + + '[source](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/packages/tool.ts#L2) ' + + '[web](https://example.com)\n', + ) + }) + + it('selects the published target in the current site locale', () => { + const { root, pages } = fixture() + expect(rewriteMarkdown('[B](b.md)\n', { + locale: 'root', + sourcePath: 'docs/a.md', + route: 'a.md', + pages, + repoRoot: root, + repositoryRef: 'abc123', + })).toBe('[B](./reference-root/b.md)\n') + }) + + it('uses raw GitHub content for unpublished images', () => { + const { root, pages } = fixture() + expect(rewriteMarkdown('![logo](../packages/logo.svg)\n', { + locale: 'en', + sourcePath: 'docs/a.md', + route: 'en/a.md', + pages, + repoRoot: root, + repositoryRef: 'abc123', + })).toBe('![logo](https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/abc123/packages/logo.svg)\n') + }) + + it('does not rewrite Markdown-looking text inside code fences', () => { + const { root, pages } = fixture() + const source = '```md\n[B](b.md)\n```\n' + expect(rewriteMarkdown(source, { + locale: 'en', + sourcePath: 'docs/a.md', + route: 'en/a.md', + pages, + repoRoot: root, + repositoryRef: 'abc123', + })).toBe(source) + }) + + it('replaces the destination token without changing repeated titles or escapes', () => { + const { root, pages } = fixture() + const source = '[title](b.md "b.md") [escaped](x\\(y\\).md)\n' + expect(rewriteMarkdown(source, { + locale: 'en', + sourcePath: 'docs/a.md', + route: 'en/a.md', + pages, + repoRoot: root, + repositoryRef: 'abc123', + })).toBe( + '[title](./reference/b.md "b.md") ' + + '[escaped](https://github.com/deepseek-harness/deepseek-harness/blob/abc123/docs/x(y).md)\n', + ) + }) + + it('routes a pair switcher across locales while ordinary links stay in locale', () => { + const { root, pages } = fixture() + writeFileSync(join(root, 'docs/a.zh.md'), '# A\n') + const paired = pages.filter(page => page.source !== 'docs/a.md') + paired.push( + { + locale: 'root', contentLocale: 'zh-CN', source: 'docs/a.zh.md', sourceAliases: ['docs/a.md'], + route: 'guide/a.md', label: 'A', sidebar: 'zh-guide', section: 'Test', order: 1, + }, + { + locale: 'en', contentLocale: 'en-US', source: 'docs/a.md', sourceAliases: ['docs/a.zh.md'], + route: 'en/guide/a.md', label: 'A', sidebar: 'en-guide', section: 'Test', order: 1, + }, + ) + expect(rewriteMarkdown('[English](a.md) [B](b.md)\n', { + locale: 'root', + sourcePath: 'docs/a.zh.md', + route: 'guide/a.md', + pages: paired, + repoRoot: root, + repositoryRef: 'abc123', + })).toBe('[English](../en/guide/a.md) [B](../reference-root/b.md)\n') + }) + + it('fails loud when a relative target is missing', () => { + const { root, pages } = fixture() + expect(() => rewriteMarkdown('[missing](missing.md)\n', { + locale: 'en', + sourcePath: 'docs/a.md', + route: 'en/a.md', + pages, + repoRoot: root, + repositoryRef: 'abc123', + })).toThrow('links to missing path "missing.md"') + }) +}) + +describe('docsPages locale routes', () => { + it('publishes every route in both locales and selects paired user sources', () => { + const byRoute = new Map(docsPages.map(page => [page.route, page])) + for (const page of docsPages.filter(page => page.locale === 'root')) { + const counterpart = byRoute.get(`en/${page.route}`) + expect(counterpart, page.route).toBeDefined() + expect(counterpart?.locale).toBe('en') + if (page.source.startsWith('docs/user/')) { + expect(page.source).toMatch(/\.zh\.md$/) + expect(page.contentLocale).toBe('zh-CN') + expect(counterpart?.source).toBe(page.source.replace(/\.zh\.md$/, '.md')) + expect(counterpart?.contentLocale).toBe('en-US') + } else { + expect(counterpart?.source).toBe(page.source) + expect(counterpart?.contentLocale).toBe(page.contentLocale) + } + } + }) + + it('publishes the Cordis core API under matching locale structures', () => { + const files = ['context.md', 'events.md', 'fiber.md', 'registry.md', 'service.md'] + for (const file of files) { + const root = docsPages.find(page => page.route === `reference/cordis-api/${file}`) + const english = docsPages.find(page => page.route === `en/reference/cordis-api/${file}`) + expect(root?.source).toBe(`docs/cordis-catalog/core/${file}`) + expect(root?.section).toBe('Cordis API') + expect(english?.source).toBe(root?.source) + expect(english?.section).toBe('Cordis Core API') + } + }) +}) + +describe('addProjectionFrontmatter', () => { + it('adds frontmatter to an ordinary Markdown page', () => { + expect(addProjectionFrontmatter('# Guide\n', 'docs/guide.md')).toBe( + '---\neditSource: "docs/guide.md"\n---\n\n# Guide\n', + ) + }) + + it('extends existing VitePress frontmatter', () => { + expect(addProjectionFrontmatter('---\nlayout: home\n---\n', 'docs/index.md')).toBe( + '---\neditSource: "docs/index.md"\nlayout: home\n---\n', + ) + }) +}) + +describe('projectedPageContent', () => { + const page = (sidebar: DocsPage['sidebar']): DocsPage => ({ + locale: 'root', + contentLocale: 'zh-CN', + source: 'docs/index.zh.md', + route: 'index.md', + label: 'Home', + sidebar, + section: 'Home', + order: 0, + }) + + it('omits the source-only body from locale home pages', () => { + expect(projectedPageContent( + '---\nlayout: home\nhero:\n name: Harness\n---\n\n# Harness\n\n[English](index.md) | 中文\n', + page(null), + )).toBe('---\nlayout: home\nhero:\n name: Harness\n---\n') + }) + + it('keeps the full body for ordinary pages', () => { + const markdown = '---\ntitle: Guide\n---\n\n# Guide\n' + expect(projectedPageContent(markdown, page('zh-guide'))).toBe(markdown) + }) + + it('rejects a locale home source without frontmatter', () => { + expect(() => projectedPageContent('# Harness\n', page(null))) + .toThrow('locale home source "docs/index.zh.md" must start with YAML frontmatter') + }) +}) diff --git a/scripts/project-doc-site.ts b/scripts/project-doc-site.ts new file mode 100644 index 0000000000..8d68aac7a6 --- /dev/null +++ b/scripts/project-doc-site.ts @@ -0,0 +1,322 @@ +/** + * Build-time projection from canonical repository Markdown into VitePress. + * + * The generated tree is disposable: sources stay in their owning `docs/` + * tier, while this adapter rewrites cross-source links for the public site. + */ + +import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { dirname, extname, posix, relative, resolve, sep } from 'node:path' +import { fromMarkdown } from 'mdast-util-from-markdown' +import { gfmFromMarkdown } from 'mdast-util-gfm' +import { gfm } from 'micromark-extension-gfm' +import type { Nodes } from 'mdast' +import { docsPages, type DocsLocale, type DocsPage } from '../website/docs.ts' + +const REPOSITORY_URL = 'https://github.com/deepseek-harness/deepseek-harness' +const root = resolve(import.meta.dirname, '..') +const generatedRoot = resolve(root, 'website/.generated') + +interface Replacement { + start: number + end: number + value: string +} + +interface DestinationRange { + start: number + end: number +} + +type RewritableNode = Extract + +/** Inputs for rewriting one canonical Markdown page. */ +export interface RewriteMarkdownOptions { + locale: DocsLocale + sourcePath: string + route: string + pages: DocsPage[] + repoRoot: string + repositoryRef: string +} + +function repoPath(absPath: string, repoRoot: string): string { + return relative(repoRoot, absPath).split(sep).join('/') +} + +function isExternalOrSiteAbsolute(url: string): boolean { + return url.startsWith('#') + || url.startsWith('//') + || url.startsWith('/') + || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url) +} + +function skipWhitespace(source: string, start: number): number { + let index = start + while (/\s/.test(source[index] ?? '')) index += 1 + return index +} + +function labelEnd(source: string): number { + const first = source.indexOf('[') + if (first === -1) return -1 + let depth = 0 + for (let index = first; index < source.length; index += 1) { + const char = source[index] + if (char === '\\') { + index += 1 + } else if (char === '[') { + depth += 1 + } else if (char === ']') { + depth -= 1 + if (depth === 0) return index + } + } + return -1 +} + +function destinationRange(rawNode: string, type: 'link' | 'image' | 'definition'): DestinationRange { + const endOfLabel = labelEnd(rawNode) + if (endOfLabel === -1) { + throw new Error(`project-doc-site: cannot locate label end in ${JSON.stringify(rawNode)}.`) + } + + let start: number + if (type === 'definition') { + const colon = rawNode.indexOf(':', endOfLabel + 1) + if (colon === -1) { + throw new Error(`project-doc-site: cannot locate definition separator in ${JSON.stringify(rawNode)}.`) + } + start = skipWhitespace(rawNode, colon + 1) + } else { + if (rawNode[endOfLabel + 1] !== '(') { + throw new Error(`project-doc-site: cannot locate inline destination in ${JSON.stringify(rawNode)}.`) + } + start = skipWhitespace(rawNode, endOfLabel + 2) + } + + if (rawNode[start] === '<') { + for (let index = start + 1; index < rawNode.length; index += 1) { + if (rawNode[index] === '\\') index += 1 + else if (rawNode[index] === '>') return { start: start + 1, end: index } + } + throw new Error(`project-doc-site: cannot locate angle-bracket destination end in ${JSON.stringify(rawNode)}.`) + } + + let depth = 0 + for (let index = start; index < rawNode.length; index += 1) { + const char = rawNode[index] + if (char === '\\') { + index += 1 + } else if (char === '(') { + depth += 1 + } else if (char === ')') { + if (depth === 0) return { start, end: index } + depth -= 1 + } else if (/\s/.test(char ?? '') && depth === 0) { + return { start, end: index } + } + } + return { start, end: rawNode.length } +} + +function splitTarget(url: string): { path: string; suffix: string } { + const boundary = url.search(/[?#]/) + if (boundary === -1) return { path: url, suffix: '' } + return { path: url.slice(0, boundary), suffix: url.slice(boundary) } +} + +function decodePath(path: string): string { + try { + return decodeURIComponent(path) + } catch { + throw new Error(`project-doc-site: malformed percent escape in ${JSON.stringify(path)}.`) + } +} + +function routeTarget(fromRoute: string, toRoute: string, suffix: string): string { + const target = posix.relative(posix.dirname(fromRoute), toRoute) + return `${target.startsWith('.') ? target : `./${target}`}${suffix}` +} + +function sourceMap(pages: DocsPage[]): Map> { + const map = new Map>() + for (const page of pages) { + for (const source of [page.source, ...(page.sourceAliases ?? [])]) { + const localized = map.get(source) ?? new Map() + if (localized.has(page.locale)) { + throw new Error(`project-doc-site: duplicate source or alias ${JSON.stringify(source)} for locale ${JSON.stringify(page.locale)}.`) + } + localized.set(page.locale, page) + map.set(source, localized) + } + } + return map +} + +function counterpartSource(source: string): string { + return source.endsWith('.zh.md') + ? source.replace(/\.zh\.md$/, '.md') + : source.replace(/\.md$/, '.zh.md') +} + +function resolveRepositoryTarget(sourceAbs: string, rawPath: string, repoRoot: string): { absPath: string; line?: number } { + const decoded = decodePath(rawPath) + let absPath = resolve(dirname(sourceAbs), decoded) + if (existsSync(absPath)) return { absPath } + + const lineMatch = decoded.match(/:(\d+)$/) + if (lineMatch !== null) { + const lineText = lineMatch[1] + if (lineText === undefined) throw new Error('project-doc-site: line suffix matched without a line number.') + absPath = resolve(dirname(sourceAbs), decoded.slice(0, -lineMatch[0].length)) + if (existsSync(absPath)) return { absPath, line: Number.parseInt(lineText, 10) } + } + + if (extname(decoded) === '') { + const markdown = resolve(dirname(sourceAbs), `${decoded}.md`) + if (existsSync(markdown)) return { absPath: markdown } + const index = resolve(dirname(sourceAbs), decoded, 'index.md') + if (existsSync(index)) return { absPath: index } + } + + throw new Error(`project-doc-site: ${repoPath(sourceAbs, repoRoot)} links to missing path ${JSON.stringify(rawPath)}.`) +} + +function githubTarget( + absPath: string, + line: number | undefined, + suffix: string, + repositoryRef: string, + repoRoot: string, + image: boolean, +): string { + const path = repoPath(absPath, repoRoot) + if (image) return `https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/${repositoryRef}/${path}${suffix}` + const kind = lstatSync(absPath).isDirectory() ? 'tree' : 'blob' + const lineSuffix = line === undefined ? suffix : `#L${line}` + return `${REPOSITORY_URL}/${kind}/${repositoryRef}/${path}${lineSuffix}` +} + +/** + * Rewrite repository-relative links without reserializing Markdown. + * + * @param source Markdown text from the canonical file. + * @param options Source, route, manifest, and repository context. + * @returns Markdown whose published links resolve inside the site or to GitHub. + */ +export function rewriteMarkdown(source: string, options: RewriteMarkdownOptions): string { + const sourceAbs = resolve(options.repoRoot, options.sourcePath) + const published = sourceMap(options.pages) + const tree = fromMarkdown(source, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] }) + const replacements: Replacement[] = [] + + const rewrite = (node: RewritableNode): void => { + if (isExternalOrSiteAbsolute(node.url)) return + const { path, suffix } = splitTarget(node.url) + if (path === '') return + const { absPath, line } = resolveRepositoryTarget(sourceAbs, path, options.repoRoot) + const targetPath = repoPath(absPath, options.repoRoot) + const isLanguageSwitcher = targetPath === counterpartSource(options.sourcePath) + const targetLocale: DocsLocale = isLanguageSwitcher + ? options.locale === 'root' ? 'en' : 'root' + : options.locale + const page = published.get(targetPath)?.get(targetLocale) + const nextUrl = page === undefined + ? githubTarget(absPath, line, suffix, options.repositoryRef, options.repoRoot, node.type === 'image') + : routeTarget(options.route, page.route, suffix) + + const start = node.position?.start.offset + const end = node.position?.end.offset + if (start === undefined || end === undefined) { + throw new Error(`project-doc-site: link ${JSON.stringify(node.url)} has no source offsets.`) + } + const rawNode = source.slice(start, end) + const rawDestination = destinationRange(rawNode, node.type) + replacements.push({ + start: start + rawDestination.start, + end: start + rawDestination.end, + value: nextUrl, + }) + } + + const visit = (node: Nodes): void => { + if ((node.type === 'link' || node.type === 'image' || node.type === 'definition') && 'url' in node) rewrite(node) + if ('children' in node) { + for (const child of node.children) visit(child) + } + } + visit(tree) + + let projected = source + for (const replacement of replacements.sort((a, b) => b.start - a.start)) { + projected = projected.slice(0, replacement.start) + replacement.value + projected.slice(replacement.end) + } + return projected +} + +/** + * Record the canonical edit target in VitePress frontmatter. + * + * @param markdown Projected Markdown content. + * @param sourcePath Repository-relative canonical source path. + * @returns Markdown with an `editSource` frontmatter field. + */ +export function addProjectionFrontmatter(markdown: string, sourcePath: string): string { + const field = `editSource: ${JSON.stringify(sourcePath)}` + if (markdown.startsWith('---\n')) return markdown.replace('---\n', `---\n${field}\n`) + return `---\n${field}\n---\n\n${markdown}` +} + +/** + * Select the Markdown rendered for one published page. + * + * @param markdown Rewritten canonical Markdown content. + * @param page Publication manifest entry for the content. + * @returns Full Markdown for ordinary pages or frontmatter-only Markdown for a locale home page. + */ +export function projectedPageContent(markdown: string, page: DocsPage): string { + if (page.sidebar !== null) return markdown + if (!markdown.startsWith('---\n')) { + throw new Error(`project-doc-site: locale home source ${JSON.stringify(page.source)} must start with YAML frontmatter.`) + } + const closingDelimiter = '\n---\n' + const closing = markdown.indexOf(closingDelimiter, 4) + if (closing === -1) { + throw new Error(`project-doc-site: locale home source ${JSON.stringify(page.source)} has unclosed YAML frontmatter.`) + } + return markdown.slice(0, closing + closingDelimiter.length) +} + +/** Canonical Markdown files watched by the local VitePress dev server. */ +export function docsSourceFiles(): string[] { + return [...new Set(docsPages.map(page => resolve(root, page.source)))] +} + +/** Rebuild the disposable VitePress source tree from the publication manifest. */ +export function projectDocs(): void { + const routes = new Set() + const repositoryRef = process.env.GITHUB_SHA ?? 'master' + rmSync(generatedRoot, { recursive: true, force: true }) + + for (const page of docsPages) { + if (routes.has(page.route)) throw new Error(`project-doc-site: duplicate route ${JSON.stringify(page.route)}.`) + routes.add(page.route) + const sourceAbs = resolve(root, page.source) + if (!existsSync(sourceAbs) || !lstatSync(sourceAbs).isFile()) { + throw new Error(`project-doc-site: source ${JSON.stringify(page.source)} does not exist or is not a file.`) + } + const output = resolve(generatedRoot, page.route) + mkdirSync(dirname(output), { recursive: true }) + const markdown = readFileSync(sourceAbs, 'utf8') + const projected = rewriteMarkdown(markdown, { + sourcePath: page.source, + locale: page.locale, + route: page.route, + pages: docsPages, + repoRoot: root, + repositoryRef, + }) + writeFileSync(output, addProjectionFrontmatter(projectedPageContent(projected, page), page.source)) + } +} diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 0262c1099c..a91af85f2e 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -214,7 +214,6 @@ function ciPrimaryGates(): Gate[] { ...docSyncLeafGates(), pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }), pnpmScript('knip', 'knip'), - pnpmScript('website-build', 'website:build', { label: 'website build' }), pnpmScript('build', 'build', { needs: ['typecheck'] }), pnpmScript('publint', 'publint', { needs: ['build'] }), pnpmScript('node-next-types', 'verify-node-next-types', { @@ -234,7 +233,6 @@ function ciStaticGates(): Gate[] { ...docSyncLeafGates(), pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }), pnpmScript('knip', 'knip'), - pnpmScript('website-build', 'website:build', { label: 'website build' }), ] } @@ -337,7 +335,6 @@ function docSyncLeafGates(options: { pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }), pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }), pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }), - pnpmScript('website-api', 'verify-website-api', { label: 'website api' }), pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }), pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }), pnpmScript('doc-refs', 'verify-doc-refs', { label: 'doc refs' }), @@ -350,8 +347,9 @@ 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' }), pnpmScript('package-readme-limitations', 'verify-package-readme-limitations', { label: 'package README limitations' }), - pnpmScript('website-yaml', 'verify-website-yaml', { label: 'website yaml' }), ] } diff --git a/scripts/translation-pairing.manifest.json b/scripts/translation-pairing.manifest.json index 7f6202a8f4..748f3fa13c 100644 --- a/scripts/translation-pairing.manifest.json +++ b/scripts/translation-pairing.manifest.json @@ -11,6 +11,18 @@ "docs/development.md", "docs/i18n/README.md", "docs/i18n/translation-rules.md", + "docs/user/develop/basic/config.md", + "docs/user/develop/basic/index.md", + "docs/user/develop/basic/tool.md", + "docs/user/develop/framework/events.md", + "docs/user/develop/framework/index.md", + "docs/user/develop/framework/service.md", + "docs/user/develop/practice/index.md", + "docs/user/develop/practice/llm-adapter.md", + "docs/user/guide/config.md", + "docs/user/guide/index.md", + "docs/user/guide/quickstart.md", + "docs/user/index.md", ".agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md", ".agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md", "python/README.md", diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 36b1e3816f..0e27265563 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -53,7 +53,6 @@ { "doc": "docs/core-data-structures/token-meter.md", "symbol": "TokenMeasurement", "source": "packages/llm/token-meter/src/types.ts" }, { "doc": "docs/core-data-structures/token-meter.md", "symbol": "TokenSurfaceNode", "source": "packages/llm/token-meter/src/types.ts" }, - { "doc": "docs/core-data-structures/session.md", "symbol": "ContextEnvelope", "source": "packages/core/session/src/types.ts" }, { "doc": "docs/core-data-structures/session.md", "symbol": "SessionEventMap", "source": "packages/core/session/src/types.ts" }, { "doc": "docs/core-data-structures/session.md", "symbol": "EpochHeader", "source": "packages/core/session/src/types.ts" }, { "doc": "docs/core-data-structures/session.md", "symbol": "TodoItem", "source": "packages/core/session/src/types.ts" }, @@ -170,6 +169,8 @@ { "doc": "docs/core-data-structures/compaction.md", "symbol": "CompactionResult", "source": "packages/compact/compact/src/types.ts" }, { "doc": "docs/core-data-structures/compaction.md", "symbol": "CompactionTrigger", "source": "packages/compact/compact/src/index.ts" }, + { "doc": "docs/core-data-structures/compaction.md", "symbol": "PrunedEntry", "source": "packages/compact/compact-tool-result-prune/src/types.ts" }, + { "doc": "docs/core-data-structures/compaction.md", "symbol": "PruneResult", "source": "packages/compact/compact-tool-result-prune/src/types.ts" }, { "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentCapabilities", "source": "packages/subagent/subagent/src/types.ts" }, { "doc": "docs/core-data-structures/subagent.md", "symbol": "SubagentStartRequest", "source": "packages/subagent/subagent/src/types.ts" }, diff --git a/scripts/verify-md-wrap.ts b/scripts/verify-md-wrap.ts index 0e8dba9371..f9e2bc803d 100644 --- a/scripts/verify-md-wrap.ts +++ b/scripts/verify-md-wrap.ts @@ -2,7 +2,8 @@ * Reject Markdown prose paragraphs spanning multiple physical lines. The GFM * AST distinguishes paragraphs—including those in lists and blockquotes—from * multiline structural nodes. The checker never rewrites; symlinked instruction - * files are deduped. The owning convention is in `docs/AGENTS.md`. + * files are deduped. VitePress frontmatter and custom-container delimiters are + * masked before parsing. The owning convention is in `docs/AGENTS.md`. */ import { readFileSync } from 'node:fs' @@ -35,11 +36,23 @@ interface Violation { text: string } +function maskVitePressStructure(source: string): string { + const lines = source.split('\n') + if (lines[0] === '---') { + const closing = lines.indexOf('---', 1) + if (closing !== -1) { + for (let index = 0; index <= closing; index++) lines[index] = '' + } + } + return lines.map(line => line.trimStart().startsWith(':::') ? '' : line).join('\n') +} + /** Find every hard-wrapped prose paragraph in one Markdown file via its AST. */ function findViolations(absPath: string): Violation[] { const file = relative(root, absPath) const source = readFileSync(absPath, 'utf8') - const tree = parseMarkdown(source) + const parsedSource = maskVitePressStructure(source) + const tree = parseMarkdown(parsedSource) const out: Violation[] = [] visitMarkdown(tree, (node: Nodes): boolean | void => { diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index b62cd5999e..41a13cfbb8 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -48,10 +48,12 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/examples/agent-spine-demo': { kind: 'indirect', reason: 'The bundle only mounts model-facing child plugins.' }, 'packages/fs/fs': { kind: 'indirect', reason: 'The service interface delegates model rendering to dsh-tool-fs.' }, 'packages/fs/fs-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-fs.' }, + 'packages/fs/fs-sandbox': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-fs.' }, 'packages/hooks/hook-protocol': { kind: 'indirect', reason: 'Only the hook bridge plugins render decoded hook output to a model.' }, 'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' }, 'packages/llm/token-meter': { kind: 'indirect', reason: 'The measurement service leaves model-visible changes to its consumers.' }, 'packages/sandbox/sandbox-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' }, + 'packages/sandbox/sandbox-policy': { kind: 'indirect', reason: 'The policy service holds the mode dsh-tool-bash and dsh-tool-fs render in their denial markers.' }, 'packages/sdk/create-sdk': { kind: 'indirect', reason: 'The initializer only writes project files; selected runtime plugins provide the generated project model surface.' }, 'packages/sdk/helper': { kind: 'none', reason: 'The project domain edits files and registers no live agent or model surface.' }, 'packages/sdk/scripts': { kind: 'indirect', reason: 'The launcher delegates model context to the loaded project plugin tree.' }, diff --git a/scripts/verify-type-equiv.ts b/scripts/verify-type-equiv.ts index f886aac567..7a3d2305d7 100644 --- a/scripts/verify-type-equiv.ts +++ b/scripts/verify-type-equiv.ts @@ -14,7 +14,7 @@ import ts from 'typescript' const root = resolve(import.meta.dirname, '..') /** Scan doc-typecheck's full Markdown scope so unmanifested blocks also fail. */ -const MARKDOWN_GLOBS = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', 'website/zh-CN/**/*.md'] +const MARKDOWN_GLOBS = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md'] /** One manifest entry: a source-equivalence block and its source symbol. */ interface ManifestEntry { diff --git a/scripts/verify-website-yaml.ts b/scripts/verify-website-yaml.ts deleted file mode 100644 index 6c086ab645..0000000000 --- a/scripts/verify-website-yaml.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Doc-sync gate: verify the fenced ```yaml examples in the website against - * the loader and the workspace truth. A `cordis.yml` example that names a - * plugin that does not exist, or passes a config key the plugin never - * declared, is worse than no example — it fails silently for the reader. - * - * Scope: `website/zh-CN/**​/*.md`, EXCLUDING `website/zh-CN/api/**` (the api - * pages are generator-owned — their yaml examples are verified at generation - * time by a later stream, not re-checked here). Blocks opt out with - * ` ```yaml ignore-check ` (same philosophy as doc-typecheck's opt-out: the - * count is reported, an unchecked block is a visible decision, not a silent - * hole — placeholder plugin names in tutorials are the legitimate case). - * - * Each checked block is parsed with the loader's REAL schema — - * `JSON_SCHEMA` extended with the `!!js` scalar type exactly as - * vendor/include/src/index.ts declares it — so `!!js process.env.X` parses - * here iff it parses at runtime. Then: - * - * - Root is an ARRAY → a cordis.yml entry list. Every item must be a mapping - * with a string `name` and only the keys `EntryOptions` declares - * (vendor/loader/src/config/entry.ts plus the isolate.ts merge: - * id, name, config, group, disabled, inject, intercept, isolate). - * - `./` / `../` names are illustrative local plugins — existence is not - * checkable, skip. `group:*` names are loader built-ins; their `config` - * is itself an entry list and is recursed into. - * - Any other name must be a real workspace package (`packages/*​/*` and - * `vendor/*` package.json names). - * - For `@deepseek-ai/dsh-*` names the config-catalog generator is the - * truth: kind `config` → the yaml `config`'s top-level keys must be - * properties of the declared config type (member names of the first - * catalog paste ∪ top-level segments of the runtime schema keys); - * config-free kinds → a non-empty `config` mapping is a violation; - * seam/library kinds → name existence only (loading one directly is - * dubious, but that is a docs-prose concern, not this gate's). - * - Root is a MAPPING or scalar → a fragment (e.g. a bare `config:` excerpt): - * syntax check only. - * - * This is a checker, not a fixer: it reports `file:line message` and exits 1. - * - * Run: `tsx scripts/verify-website-yaml.ts`. - */ - -import { globSync, readFileSync } from 'node:fs' -import { resolve } from 'node:path' -import * as yaml from 'js-yaml' -import ts from 'typescript' -import { collectConfigCatalog, type CatalogEntry } from './gen-config-catalog.ts' -import { extractFences } from './md-fences.ts' - -const root = resolve(import.meta.dirname, '..') - -/** Mirror of the loader's yaml schema (vendor/include/src/index.ts): the - * `!!js` tag parses to an expression wrapper, everything else is JSON. */ -const JsExpr = new yaml.Type('tag:yaml.org,2002:js', { - kind: 'scalar', - resolve: data => typeof data === 'string', - construct: (data: string) => ({ __jsExpr: data }), -}) -const schema = yaml.JSON_SCHEMA.extend(JsExpr) - -/** The exact key set an entry mapping may carry: `EntryOptions` in - * vendor/loader/src/config/entry.ts plus the isolate.ts interface merge. */ -const ENTRY_KEYS = ['id', 'name', 'config', 'group', 'disabled', 'inject', 'intercept', 'isolate'] as const - -/** One `file:line message` finding. */ -interface Violation { - file: string - /** 1-based line of the block's opening fence. */ - line: number - message: string -} - -/** One extracted ```yaml block. */ -interface Block { - file: string - /** 1-based line of the opening fence. */ - line: number - kind: 'check' | 'ignore' - code: string -} - -/** Extract every ```yaml / ```yaml ignore-check block from one Markdown file. */ -function extractBlocks(file: string): Block[] { - return extractFences(resolve(root, file), info => - info === 'yaml' ? 'check' : info === 'yaml ignore-check' ? 'ignore' : null) - .map(f => ({ file, line: f.line, kind: f.kind, code: f.code })) -} - -/** Every workspace package name: `packages//` and `vendor/`. */ -function knownPackages(): Set { - const names = new Set() - for (const pattern of ['packages/*/*/package.json', 'vendor/*/package.json']) { - for (const match of globSync(pattern, { cwd: root })) { - const pkg: unknown = JSON.parse(readFileSync(resolve(root, match), 'utf8')) - if (typeof pkg === 'object' && pkg !== null && 'name' in pkg && typeof pkg.name === 'string') { - names.add(pkg.name) - } - } - } - return names -} - -/** The catalog, built once on first `@deepseek-ai/dsh-*` name, keyed by pkg. */ -let catalogByPkg: Map | null = null -function catalogFor(pkg: string): CatalogEntry | undefined { - catalogByPkg ??= new Map(collectConfigCatalog().map(e => [e.pkg, e])) - return catalogByPkg.get(pkg) -} - -/** Top-level property names of the first catalog paste (the verbatim config - * type declaration), parsed as source text. */ -function pasteKeys(paste: string): Set { - const sf = ts.createSourceFile('paste.ts', paste, ts.ScriptTarget.Latest, true) - const keys = new Set() - const addMembers = (members: ts.NodeArray): void => { - for (const m of members) { - if (ts.isPropertySignature(m) || ts.isMethodSignature(m)) { - const name = m.name - keys.add(ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : name.getText(sf)) - } - } - } - for (const stmt of sf.statements) { - if (ts.isInterfaceDeclaration(stmt)) addMembers(stmt.members) - else if (ts.isTypeAliasDeclaration(stmt) && ts.isTypeLiteralNode(stmt.type)) addMembers(stmt.type.members) - } - return keys -} - -/** The allowed top-level config keys of a kind-`config` catalog entry: the - * first paste's member names ∪ the schema keys' top-level segments - * (`agents[].id` → `agents`). Cached per entry. */ -const allowedKeysCache = new Map>() -function allowedConfigKeys(entry: CatalogEntry): Set { - const cached = allowedKeysCache.get(entry.pkg) - if (cached) return cached - const keys = pasteKeys(entry.pastes?.[0]?.text ?? '') - for (const path of entry.schemaKeys ?? []) { - const top = path.split('.')[0]?.replace(/\[\]$/, '') - if (top) keys.add(top) - } - allowedKeysCache.set(entry.pkg, keys) - return keys -} - -/** A parsed yaml mapping (arrays and `!!js` wrappers excluded). */ -function asMapping(value: unknown): Record | null { - if (typeof value !== 'object' || value === null || Array.isArray(value)) return null - if ('__jsExpr' in value) return null - return value as Record -} - -/** Check one cordis.yml entry list (recursing into `group:` sub-lists). */ -function checkEntryList( - items: unknown[], - known: Set, - block: Block, - violations: Violation[], -): void { - const flag = (message: string): void => { - violations.push({ file: block.file, line: block.line, message }) - } - items.forEach((item, index) => { - const at = `entry ${index + 1}` - const entry = asMapping(item) - if (!entry) { - flag(`${at}: not a mapping`) - return - } - const name = entry['name'] - if (typeof name !== 'string') { - flag(`${at}: missing string \`name\``) - return - } - for (const key of Object.keys(entry)) { - if (!(ENTRY_KEYS as readonly string[]).includes(key)) { - flag(`${at} (${name}): unknown entry key \`${key}\` (EntryOptions allows: ${[...ENTRY_KEYS].join(', ')})`) - } - } - // Illustrative local plugin — nothing on disk to check against. - if (name.startsWith('./') || name.startsWith('../')) return - // A `group:`-style pseudo-name is NOT loadable: tree.import() only - // special-cases the `cordis:` prefix, and nothing in this repo registers - // loader builtins — reject it and point at the real group plugin. - if (name.startsWith('group:')) { - flag(`${at}: \`${name}\` is not loadable (no loader builtin is registered); use \`@cordisjs/plugin-group\` with \`group: true\``) - return - } - // The vendored group plugin: its config is a nested entry list. - if (name === '@cordisjs/plugin-group') { - if (Array.isArray(entry['config'])) checkEntryList(entry['config'], known, block, violations) - return - } - if (!known.has(name)) { - flag(`${at}: unknown plugin \`${name}\` (not a workspace package)`) - return - } - if (!name.startsWith('@deepseek-ai/dsh-')) return - const catalog = catalogFor(name) - if (!catalog) return - const config = asMapping(entry['config']) - if (catalog.kind === 'config') { - if (!config) return - const allowed = allowedConfigKeys(catalog) - for (const key of Object.keys(config)) { - if (!allowed.has(key)) { - flag(`${at}: \`${name}\` has no config key \`${key}\` (known keys: ${[...allowed].sort().join(', ')})`) - } - } - } else if (catalog.kind === 'no-config') { - if (config && Object.keys(config).length > 0) { - flag(`${at}: \`${name}\` declares no config, but the example passes one`) - } - } - // seam / library: loading one directly is dubious, but that is a prose - // concern — this gate only vouches for name existence. - }) -} - -const files = globSync('website/zh-CN/**/*.md', { cwd: root }) - .filter(f => !f.startsWith('website/zh-CN/api/')) - .sort() - -const violations: Violation[] = [] -const known = knownPackages() -let entryLists = 0 -let fragments = 0 -let ignored = 0 -let scanned = 0 - -for (const file of files) { - for (const block of extractBlocks(file)) { - scanned++ - if (block.kind === 'ignore') { - ignored++ - continue - } - let parsed: unknown - try { - parsed = yaml.load(block.code, { schema }) - } catch (error) { - const message = error instanceof Error ? error.message.split('\n')[0] ?? 'parse error' : String(error) - violations.push({ file: block.file, line: block.line, message: `yaml parse error: ${message}` }) - continue - } - if (Array.isArray(parsed)) { - entryLists++ - checkEntryList(parsed, known, block, violations) - } else { - // Mapping or scalar root: a fragment (e.g. a bare `config:` excerpt) — - // syntax is all there is to check. - fragments++ - } - } -} - -if (violations.length === 0) { - console.log( - `verify-website-yaml: ${scanned} yaml block(s) in ${files.length} file(s): ` - + `${entryLists} entry list(s) + ${fragments} fragment(s) checked, ${ignored} ignore-check skipped.`, - ) - process.exit(0) -} - -console.error('verify-website-yaml: invalid yaml examples found:') -for (const v of violations) { - console.error(` ${v.file}:${v.line} ${v.message}`) -} -process.exit(1) diff --git a/tsconfig.build.json b/tsconfig.build.json index deda998669..f88d9d26f6 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -44,16 +44,19 @@ { "path": "./packages/code-runtime/code-runtime-worker" }, { "path": "./packages/compact/compact" }, { "path": "./packages/compact/compact-basic" }, + { "path": "./packages/compact/compact-tool-result-prune" }, { "path": "./packages/llm/llm-deepseek" }, { "path": "./packages/llm/llm-pi-ai" }, { "path": "./packages/bash/bash-local" }, { "path": "./packages/sandbox/sandbox" }, { "path": "./packages/sandbox/sandbox-local" }, + { "path": "./packages/sandbox/sandbox-policy" }, { "path": "./packages/bash/bash-sandbox" }, { "path": "./packages/bash/tool-bash" }, { "path": "./packages/fs/fs" }, { "path": "./packages/fs/fs-local" }, { "path": "./packages/fs/fs-policy" }, + { "path": "./packages/fs/fs-sandbox" }, { "path": "./packages/fs/tool-fs" }, { "path": "./packages/fs/tool-fs-search" }, { "path": "./packages/web/web" }, diff --git a/tsconfig.json b/tsconfig.json index 63c301ddac..02fa057e69 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,9 @@ "examples/*/start.ts", "examples/*/tests/**/*.ts", "packages/*/*/tests/**/*.ts", - "scripts/**/*.ts" + "scripts/**/*.ts", + "website/**/*.ts", + "website/.vitepress/**/*.ts" ], "references": [ { "path": "./vendor/cosmokit" }, @@ -58,15 +60,18 @@ { "path": "./packages/bash/bash-local" }, { "path": "./packages/sandbox/sandbox" }, { "path": "./packages/sandbox/sandbox-local" }, + { "path": "./packages/sandbox/sandbox-policy" }, { "path": "./packages/bash/bash-sandbox" }, { "path": "./packages/bash/tool-bash" }, { "path": "./packages/fs/fs" }, { "path": "./packages/fs/fs-local" }, { "path": "./packages/fs/fs-policy" }, + { "path": "./packages/fs/fs-sandbox" }, { "path": "./packages/fs/tool-fs" }, { "path": "./packages/fs/tool-fs-search" }, { "path": "./packages/compact/compact" }, { "path": "./packages/compact/compact-basic" }, + { "path": "./packages/compact/compact-tool-result-prune" }, { "path": "./packages/web/web" }, { "path": "./packages/web/web-search-exa" }, { "path": "./packages/web/web-search-perplexity" }, diff --git a/website/.gitignore b/website/.gitignore index 2c1fa99cb4..29099c8fe6 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -1,3 +1,4 @@ node_modules/ -.vitepress/dist/ -.vitepress/cache/ +.cache/ +.dist/ +.generated/ diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts new file mode 100644 index 0000000000..b9f38b0f7e --- /dev/null +++ b/website/.vitepress/config.ts @@ -0,0 +1,191 @@ +/** VitePress configuration for the locally projected documentation site. */ + +import type { DefaultTheme, PageData } from 'vitepress' +import type { ViteDevServer } from 'vite' +import { withMermaid } from 'vitepress-plugin-mermaid' +import { docsPages, type DocsPage } from '../docs.ts' +import { docsSourceFiles, projectDocs } from '../../scripts/project-doc-site.ts' + +projectDocs() + +const sectionOrder = [ + '入门', + '基础', + '框架能力', + '实战', + '概念', + '生成参考', + 'Cordis API', + '数据结构', + '开发手册', + 'Guide', + 'Basics', + 'Framework', + 'Practice', + 'Concepts', + 'Generated reference', + 'Cordis Core API', + 'Data structures', + 'Cookbook', +] + +function sidebar(collection: DocsPage['sidebar']): DefaultTheme.SidebarItem[] { + const pages = docsPages.filter(page => page.sidebar === collection) + const sections = new Map() + for (const page of pages) { + const entries = sections.get(page.section) ?? [] + entries.push(page) + sections.set(page.section, entries) + } + return [...sections.entries()] + .sort(([left], [right]) => sectionOrder.indexOf(left) - sectionOrder.indexOf(right)) + .map(([text, entries]) => ({ + text, + items: entries + .sort((left, right) => left.order - right.order) + .map(page => ({ text: page.label, link: `/${page.route.replace(/(?:index)?\.md$/, '')}` })), + })) +} + +function watchCanonicalDocs(server: ViteDevServer): void { + const sources = docsSourceFiles() + server.watcher.add(sources) + server.watcher.on('change', (changed) => { + if (!sources.includes(changed)) return + projectDocs() + }) +} + +function escapeVueInterpolation(html: string): string { + return html.replaceAll('{{', '{{').replaceAll('}}', '}}') +} + +const sharedTheme: Pick = { + search: { + provider: 'local', + options: { + locales: { + root: { + translations: { + button: { + buttonText: '搜索文档', + buttonAriaLabel: '搜索文档', + }, + modal: { + displayDetails: '显示详细列表', + resetButtonTitle: '清除搜索', + backButtonTitle: '关闭搜索', + noResultsText: '未找到相关结果', + footer: { + selectText: '选择', + selectKeyAriaLabel: '回车键', + navigateText: '切换', + navigateUpKeyAriaLabel: '上方向键', + navigateDownKeyAriaLabel: '下方向键', + closeText: '关闭', + closeKeyAriaLabel: 'Esc 键', + }, + }, + }, + }, + }, + }, + }, + socialLinks: [ + { icon: 'github', link: 'https://github.com/deepseek-harness/deepseek-harness' }, + ], + editLink: { + pattern: ({ frontmatter }: PageData) => { + const data: unknown = frontmatter + const editSource: unknown = typeof data === 'object' && data !== null ? Reflect.get(data, 'editSource') : undefined + if (typeof editSource !== 'string') throw new Error('Projected documentation page has no editSource frontmatter.') + return `https://github.com/deepseek-harness/deepseek-harness/edit/master/${editSource}` + }, + text: '在 GitHub 上编辑此页', + }, +} + +export default withMermaid({ + title: 'DeepSeek Harness', + description: '用于构建 Agent Harness 的插件化 SDK', + cleanUrls: true, + srcDir: '.generated', + cacheDir: '.cache', + outDir: '.dist', + locales: { + root: { + label: '简体中文', + lang: 'zh-CN', + themeConfig: { + nav: [ + { text: '入门', link: '/guide/', activeMatch: '^/guide/' }, + { text: '开发', link: '/develop/basic/', activeMatch: '^/develop/' }, + { text: '参考', link: '/reference/', activeMatch: '^/reference/' }, + ], + sidebar: { + '/guide/': sidebar('zh-guide'), + '/develop/': sidebar('zh-develop'), + '/reference/': sidebar('zh-reference'), + }, + outline: { label: '本页目录' }, + docFooter: { prev: '上一篇', next: '下一篇' }, + darkModeSwitchLabel: '外观', + lightModeSwitchTitle: '切换到浅色主题', + darkModeSwitchTitle: '切换到深色主题', + sidebarMenuLabel: '菜单', + returnToTopLabel: '返回顶部', + langMenuLabel: '切换语言', + skipToContentLabel: '跳至内容', + }, + }, + en: { + label: 'English', + lang: 'en-US', + link: '/en/', + themeConfig: { + nav: [ + { text: 'Guide', link: '/en/guide/', activeMatch: '^/en/guide/' }, + { text: 'Develop', link: '/en/develop/basic/', activeMatch: '^/en/develop/' }, + { text: 'Reference', link: '/en/reference/', activeMatch: '^/en/reference/' }, + ], + sidebar: { + '/en/guide/': sidebar('en-guide'), + '/en/develop/': sidebar('en-develop'), + '/en/reference/': sidebar('en-reference'), + }, + editLink: { + pattern: ({ frontmatter }: PageData) => { + const data: unknown = frontmatter + const editSource: unknown = typeof data === 'object' && data !== null ? Reflect.get(data, 'editSource') : undefined + if (typeof editSource !== 'string') throw new Error('Projected documentation page has no editSource frontmatter.') + return `https://github.com/deepseek-harness/deepseek-harness/edit/master/${editSource}` + }, + text: 'Edit this page on GitHub', + }, + outline: { label: 'On this page' }, + docFooter: { prev: 'Previous', next: 'Next' }, + }, + }, + }, + vite: { + plugins: [ + { + name: 'deepseek-harness-doc-projector', + configureServer: watchCanonicalDocs, + }, + ], + }, + markdown: { + config(md) { + const renderText = md.renderer.rules.text + const renderCode = md.renderer.rules.code_inline + if (renderText === undefined || renderCode === undefined) { + throw new Error('VitePress Markdown renderer is missing its text or inline-code rule.') + } + md.renderer.rules.text = (...args) => escapeVueInterpolation(renderText(...args)) + md.renderer.rules.code_inline = (...args) => escapeVueInterpolation(renderCode(...args)) + }, + }, + mermaid: {}, + themeConfig: sharedTheme, +}) diff --git a/website/.vitepress/config/api-sidebar.json b/website/.vitepress/config/api-sidebar.json deleted file mode 100644 index 5fb3258805..0000000000 --- a/website/.vitepress/config/api-sidebar.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "cordis": [ - { - "text": "Context", - "link": "/zh-CN/api/cordis/context" - }, - { - "text": "Events", - "link": "/zh-CN/api/cordis/events" - }, - { - "text": "Fiber", - "link": "/zh-CN/api/cordis/fiber" - }, - { - "text": "Registry", - "link": "/zh-CN/api/cordis/registry" - }, - { - "text": "Service", - "link": "/zh-CN/api/cordis/service" - } - ], - "harness": [ - { - "text": "ctx.agentLoop", - "link": "/zh-CN/api/harness/agent-loop" - }, - { - "text": "ctx.agents", - "link": "/zh-CN/api/harness/agents" - }, - { - "text": "ctx.approval", - "link": "/zh-CN/api/harness/approval" - }, - { - "text": "ctx.bash", - "link": "/zh-CN/api/harness/bash" - }, - { - "text": "ctx.bashEnv", - "link": "/zh-CN/api/harness/bash-env" - }, - { - "text": "ctx.codeRuntime", - "link": "/zh-CN/api/harness/code-runtime" - }, - { - "text": "ctx.compact", - "link": "/zh-CN/api/harness/compact" - }, - { - "text": "ctx.fs", - "link": "/zh-CN/api/harness/fs" - }, - { - "text": "ctx.goals", - "link": "/zh-CN/api/harness/goals" - }, - { - "text": "ctx.llm", - "link": "/zh-CN/api/harness/llm" - }, - { - "text": "ctx.permission", - "link": "/zh-CN/api/harness/permission" - }, - { - "text": "ctx.sandbox", - "link": "/zh-CN/api/harness/sandbox" - }, - { - "text": "ctx.sessionPersistence", - "link": "/zh-CN/api/harness/session-persistence" - }, - { - "text": "ctx.sessionQuery", - "link": "/zh-CN/api/harness/session-query" - }, - { - "text": "ctx.sessions", - "link": "/zh-CN/api/harness/sessions" - }, - { - "text": "ctx.skills", - "link": "/zh-CN/api/harness/skills" - }, - { - "text": "ctx.spillStore", - "link": "/zh-CN/api/harness/spill-store" - }, - { - "text": "ctx.subagents", - "link": "/zh-CN/api/harness/subagents" - }, - { - "text": "ctx.systemPrompt", - "link": "/zh-CN/api/harness/system-prompt" - }, - { - "text": "ctx.tasks", - "link": "/zh-CN/api/harness/tasks" - }, - { - "text": "ctx.tokenMeter", - "link": "/zh-CN/api/harness/token-meter" - }, - { - "text": "ctx.tools", - "link": "/zh-CN/api/harness/tools" - }, - { - "text": "ctx.userInteraction", - "link": "/zh-CN/api/harness/user-interaction" - }, - { - "text": "ctx.web", - "link": "/zh-CN/api/harness/web" - }, - { - "text": "ctx.workflows", - "link": "/zh-CN/api/harness/workflows" - }, - { - "text": "Events", - "link": "/zh-CN/api/harness/events" - } - ] -} diff --git a/website/.vitepress/config/index.ts b/website/.vitepress/config/index.ts deleted file mode 100644 index 764d8619a9..0000000000 --- a/website/.vitepress/config/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineConfig } from 'vitepress' -import { zhCN } from './zh-CN' - -export default defineConfig({ - title: 'DeepSeek Harness', - description: '插件化 Agent 开发框架', - - // The design essays (design/revertible-effects, design/context-model) carry - // real TeX; math: true wires markdown-it-mathjax3 into the pipeline. - // markdown-it-mathjax3 is pinned to ^4 (NOT 5.x): v5 injects a