diff --git a/.agents/notes/AGENTS.md b/.agents/notes/AGENTS.md new file mode 100644 index 0000000000..958aff54fc --- /dev/null +++ b/.agents/notes/AGENTS.md @@ -0,0 +1,3 @@ +# AGENTS.md — Agent Notes + +Agent Notes are effectively RFCs written by agents: durable proposals and decision records that preserve rationale, alternatives, consequences, and verification contracts. Follow the [documentation standard](../../docs/AGENTS.md) and the [Agent Note contract](README.md). diff --git a/.agents/notes/README.md b/.agents/notes/README.md new file mode 100644 index 0000000000..e62dc18954 --- /dev/null +++ b/.agents/notes/README.md @@ -0,0 +1,111 @@ +# Agent Notes + +One kind of design doc lives here. An **Agent Note** records a decision or proposal that shapes this codebase — the *why* and *what we gave up*, the parts code and docs can't carry. This file is the front door and contract: where Agent Notes live, when to write one, and [the in-file format](#the-file-format). + +## Layout and naming + +Every Agent Note has two axes, both encoded in its **path** — `{lifecycle}/{class}/yyyy-mm-dd-topic-title.md`: + +- **Lifecycle** (the top-level folder) is the Agent Note's status, and an Agent Note moves between folders as that status changes: + - **`proposed/`** — proposals reviewed before implementation; not yet built (or only partly). + - **`implemented/`** — the decision shipped. The file records what was decided and what was rejected, and is **kept current with what actually shipped**: when the code later moves a file, renames a package, or changes a key/default, the Agent Note is updated in the same change to match (facts only — paths, names, structure — not the decision itself). See [implemented/AGENTS.md](implemented/AGENTS.md). + - **`rejected/`** — the proposal was considered and declined. Kept for the record so the rejection isn't re-litigated. +- **Class** (the nested folder) is the *kind* of decision — see [Classification](#classification) below. + +The date in the filename is when the topic was **first proposed** (per git history). Cross-references between Agent Notes use relative markdown links (`[topic](../../implemented/architecture/2026-…-….md)`) — never bare prose or numbers — so they are mechanically checkable and survive moves between folders. + +The tree is the inventory: browse its lifecycle/class folders or search the repository. Do not add a centralized `INDEX.md`; the [no-index Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.md) owns the rationale. + +## Classification + +Each Agent Note belongs to one path-encoded class from the closed set in `scripts/agent-note-tree.ts`; the classification gate rejects other folders. Adding a class requires updating the canonical set and this section. See the [classification Agent Note](implemented/process/2026-06-20-agent-note-classification.md). + +| Class | What it covers | +|---|---| +| `feature` | A new user- or model-facing capability. | +| `bug-fix` | Corrects a defect or closes a gap a postmortem surfaced. | +| `simplification` | Removes code, behavior, or surface area without adding a capability. | +| `architecture` | A structural decision about the **shipped source** — how packages relate, what the runtime vocabulary is. | +| `process` | Tooling, policy, or workflow **around** the code — gates, the package manager, vendoring — not runtime behavior. | +| `testing` | Test infrastructure and strategy. | + +The `architecture` / `process` line: **architecture** is about the source we ship; **process** is the surrounding tooling and workflow. (`refactor` is deliberately absent — it overlaps `simplification`, whose discriminator, "does observable behavior change?", already covers it.) + +## When to write one + +Every non-trivial change MUST add or update at least one Agent Note in the same PR. A change is non-trivial when it alters behavior, architecture, a cross-file or cross-package contract, process or tooling, testing strategy, an on-disk, wire, or configuration format, or another decision a maintainer may reasonably revisit. A proposal for substantial future work starts in `proposed/`; a decision already made starts in `implemented/`. Pick the class folder that matches the decision (see [Classification](#classification)). + +Updating the Agent Note that already owns the decision satisfies the rule; do not create a duplicate. Only a purely mechanical or local edit with no behavioral, contractual, structural, process, or rationale change is exempt. An Agent Note is never edited into a *different decision*: supersede it with a new one and cross-link. Editing an `implemented/` Agent Note to track where its existing decision lives is required, not forbidden; see [implemented/AGENTS.md](implemented/AGENTS.md). + +## The file format + +Every Agent Note follows one in-file format, enforced by `pnpm run verify-agent-note-format` ([scripts/verify-agent-note-format.ts](../../scripts/verify-agent-note-format.ts), part of `doc-sync`); the rationale for the format — and the alternatives it rejected — is [the uniform-format Agent Note](implemented/process/2026-07-05-uniform-agent-note-format.md). + +### The header block + +The first three lines of every Agent Note are exactly: + +```markdown +# Agent Note: + +Status: <status> +``` + +followed by a blank line. The `Status:` value is one of three forms, and must agree with the lifecycle folder the file sits in — the gate cross-checks them: + +- `Status: proposed` +- `Status: implemented` +- `Status: rejected — <why, in one line>` + +The status carries no dates and no parentheticals: the filename holds the first-proposed date, git holds everything else, and an "accepted in amended form" note is body content (state the amendment where the decision is stated). The rejection reason is the one status with content, because a rejected Agent Note's verdict is the fact readers come for. + +### The body skeleton + +Every Agent Note opens its body with `## Problem` — the motivation, written to stand without the solution. What follows depends on the lifecycle; recurring sections use these canonical names and nothing else, while genuinely bespoke technical sections (package topology, wire contracts, schemas) remain free-form between the required ones. + +#### `proposed/` + +```markdown +## Problem +## Proposal +…bespoke sections… +## Alternatives considered +## Acceptance criteria +## Risks +``` + +`## Proposal` is the intended change and may legitimately speak in the future tense — plans, migration steps, and open questions belong here while the work is unbuilt. `## Acceptance criteria` says what observable state means done. `## Risks` covers both what could go wrong and what the change knowingly gives up. + +#### `implemented/` + +```markdown +## Problem +## Decision +…bespoke sections… +## Alternatives considered +## Consequences +``` + +`## Decision` describes shipped reality in the present tense, and the whole file is kept current with it per [implemented/AGENTS.md](implemented/AGENTS.md). `## Consequences` records what the trade-off cost **and** bought. Proposal-era headings are spec-speak here and the gate rejects them: `## Proposal`, `## Plan`, `## Migration plan`, and `## Acceptance criteria` may not appear in an implemented Agent Note (the [slop checklist](../../docs/AGENTS.md) names why). A `## Testing`, `## Deferred`, or `## Related` section is fine where it states present-tense fact. + +#### `rejected/` + +A rejected Agent Note is the proposal, frozen: it keeps whatever proposal-time sections it had (including `## Acceptance criteria` or `## Plan`), and the verdict lives on the `Status:` line. Only the header block, the `## Problem` opener, a `## Proposal` section, and the Alternatives-considered mandate below apply. + +### Alternatives considered — mandatory + +Every Agent Note carries an `## Alternatives considered` section: each genuine alternative and why it lost, one bold-led paragraph per alternative or a `### Why not <X>?` subsection per contested one. A decision recorded without what it beat invites re-litigation — the failure Agent Notes exist to prevent. + +Alternatives are recorded, never invented. An Agent Note dated before 2026-07-05 whose alternatives are not reconstructible from the record carries this exact comment in place of the section, which the gate accepts for pre-format files only: + +```markdown +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> +``` + +### Moving between lifecycles + +Moving a file between lifecycle folders means updating the `Status:` line and re-satisfying that folder's skeleton in the same change — the gate fails the move otherwise. Concretely, `proposed/` → `implemented/` rewrites `## Proposal` into a present-tense `## Decision`, folds `## Acceptance criteria` and `## Risks` into `## Consequences` (or a present-tense `## Testing`/`## Verification` section for what now pins the behavior), and drops plans in favor of what shipped — the rewrite [implemented/AGENTS.md](implemented/AGENTS.md) requires, made mechanical. `proposed/` → `rejected/` only adds the reason to the `Status:` line and freezes the file. + +### Chinese counterparts + +A `.zh.md` counterpart mirrors its English sibling's structure section-for-section under the [i18n contract](../../docs/i18n/README.md); the machine-checked header tokens (`# Agent Note: ` and the `Status:` line) stay in English verbatim. The format gate skips `.zh.md` files — the pairing gate owns their consistency. diff --git a/.agents/notes/implemented/AGENTS.md b/.agents/notes/implemented/AGENTS.md new file mode 100644 index 0000000000..5fb3fde8e2 --- /dev/null +++ b/.agents/notes/implemented/AGENTS.md @@ -0,0 +1,11 @@ +# AGENTS.md — Implemented Agent Notes + +These Agent Notes describe shipped decisions. Follow the [root instructions](../../../AGENTS.md), [documentation standard](../../../docs/AGENTS.md), and [Agent Note format](../README.md#the-file-format); `verify-agent-note-format` gates the lifecycle-specific structure. + +## Keep an implemented Agent Note current with what actually shipped + +Keep paths, symbols, defaults, and mechanisms current in the same change that alters them. Rewrite stale facts in place; do not append change history. + +### This is not a license to rewrite the *decision* + +Update factual realization in place. A reversal of the decision or its rationale requires a new Agent Note and cross-link; see the [Agent Note contract](../README.md). diff --git a/docs/rfc/implemented/CLAUDE.md b/.agents/notes/implemented/CLAUDE.md similarity index 100% rename from docs/rfc/implemented/CLAUDE.md rename to .agents/notes/implemented/CLAUDE.md diff --git a/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.md b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md similarity index 69% rename from docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.md rename to .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md index 77f521834c..1133b990c3 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md @@ -1,4 +1,4 @@ -# RFC: Provider-neutral content-block vocabulary owned by dsh-llm +# Agent Note: Provider-neutral content-block vocabulary owned by dsh-llm Status: implemented @@ -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 @@ -20,7 +20,7 @@ In-session context injection (`context/message`, `steering/message`) renders as ## Consequences - Reasoning has a core home without provider-specific shapes. -- Multimodal blocks return only with coordinated adapter, UI, and compaction support; see [the drop-image RFC](../simplification/2026-07-04-drop-image-content-block.md). -- Cache hints and assistant prefill remain absent until a shipping adapter can honor them; see the [producer-less variants](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md) and [inert request knobs](../simplification/2026-07-04-drop-inert-request-knobs.md) RFCs. +- Multimodal blocks return only with coordinated adapter, UI, and compaction support; see [the drop-image Agent Note](../simplification/2026-07-04-drop-image-content-block.md). +- Cache hints and assistant prefill remain absent until a shipping adapter can honor them; see the [producer-less variants](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md) and [inert request knobs](../simplification/2026-07-04-drop-inert-request-knobs.md) Agent Notes. - Every adapter pays a translation cost; the first real adapters have since validated the streaming protocol, and new adapters should continue proving their provider-specific mapping in adapter-local tests. - IDs that cross package boundaries are branded (`CallId`, the shared agent/session `SessionId`) — nominal typing at zero runtime cost. diff --git a/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.md b/.agents/notes/implemented/architecture/2026-06-11-custom-schema-dsl.md similarity index 95% rename from docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.md rename to .agents/notes/implemented/architecture/2026-06-11-custom-schema-dsl.md index 18923fbb60..bf8c02140a 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.md +++ b/.agents/notes/implemented/architecture/2026-06-11-custom-schema-dsl.md @@ -1,4 +1,4 @@ -# RFC: Custom typed tool-schema DSL instead of schemastery +# Agent Note: Custom typed tool-schema DSL instead of schemastery Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md similarity index 98% rename from docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md rename to .agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md index aac3991f46..90e215019b 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md +++ b/.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md @@ -1,4 +1,4 @@ -# RFC: Source-owned session immutability and dev-mode invariants +# Agent Note: Source-owned session immutability and dev-mode invariants Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.md b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md similarity index 96% rename from docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.md rename to .agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md index 4539fb40ba..bab36ee783 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.md +++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md @@ -1,4 +1,4 @@ -# RFC: Event-sourced sessions with derived message history +# Agent Note: Event-sourced sessions with derived message history Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md similarity index 91% rename from docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md rename to .agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md index 1d10bfa479..abdadb447b 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md +++ b/.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md @@ -1,4 +1,4 @@ -# RFC: Microkernel — extension via Cordis event taxonomy, one concrete loop +# Agent Note: Microkernel — extension via Cordis event taxonomy, one concrete loop Status: implemented @@ -23,7 +23,7 @@ The event vocabulary lives in interface packages (dsh-agent declares the agent/* ## Consequences -- Every MVP feature maps to a listener (the [feature → mechanism map](../../../cookbook/extension-cookbook.md#the-feature--mechanism-map) is the proof obligation, kept current). +- Every MVP feature maps to a listener (the [feature → mechanism map](../../../../docs/cookbook/extension-cookbook.md#the-feature--mechanism-map) is the proof obligation, kept current). - HMR and disposal come free: listeners and registrations are Cordis effects. - Waterfall semantics (call `next()` or short-circuit) are non-obvious and must be taught — documented in AGENTS.md and covered by composition tests. - The loop must be defensive: plugin exceptions are contained at turn level, steering from any seam is never stranded (regression-tested). diff --git a/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.md b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md similarity index 94% rename from docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.md rename to .agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md index 33bf241c74..454f12d0af 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.md +++ b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md @@ -1,4 +1,4 @@ -# RFC: Runtime arg validation at the model boundary +# Agent Note: Runtime arg validation at the model boundary Status: implemented @@ -19,4 +19,4 @@ The validator mirrors `schemaSpecToJsonSchema` semantics exactly — same struct - `ToolArgsError` is a plain `Error` with a `code` field for now; if a harness-wide error taxonomy lands it becomes a subclass without changing callers that read `.message`. - Validation cost is negligible next to a model call. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.md b/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md similarity index 94% rename from docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.md rename to .agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md index 01e50da2ff..eacd409a57 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.md +++ b/.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md @@ -1,4 +1,4 @@ -# RFC: Structured error taxonomy +# Agent Note: Structured error taxonomy Status: implemented @@ -21,4 +21,4 @@ A single `HarnessError extends Error` base in `dsh-llm` (the leaf package every - `deriveMessages` does not surface `error` into model history — the model still sees the text block; the structured field is for code and replay. - Argument validation and dev invariants retain their existing codes and behavior; the shared base adds cross-seam routing metadata without changing model-facing text. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md b/.agents/notes/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md similarity index 95% rename from docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md rename to .agents/notes/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md index 5c78ef4280..59ea9117dc 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md +++ b/.agents/notes/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md @@ -1,4 +1,4 @@ -# RFC: Tool schemas are part of the system-prompt assembly +# Agent Note: Tool schemas are part of the system-prompt assembly Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-06-13-capability-seams.md b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md similarity index 88% rename from docs/rfc/implemented/architecture/2026-06-13-capability-seams.md rename to .agents/notes/implemented/architecture/2026-06-13-capability-seams.md index 6887660de0..5ca299abc0 100644 --- a/docs/rfc/implemented/architecture/2026-06-13-capability-seams.md +++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md @@ -1,4 +1,4 @@ -# RFC: Capability seams — interface / implementation / consumer split +# Agent Note: Capability seams — interface / implementation / consumer split Status: implemented @@ -6,7 +6,7 @@ Status: implemented The harness has swappable capabilities — bash execution today, sandboxed/remote executors and alternative model providers tomorrow. A capability has three concerns that change at different rates and for different reasons: the *contract* (what the capability is), the *implementation* (how it runs), and the *consumer surface* (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed. -This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + `inject` (a provider registers `ctx.bash`; a consumer declares `inject: ['bash']` and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this RFC does. +This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + `inject` (a provider registers `ctx.bash`; a consumer declares `inject: ['bash']` and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this Agent Note does. ## Decision @@ -23,8 +23,8 @@ The split is not mandatory when the parts are genuinely one concern: the LLM sea ## Alternatives considered - **One combined package** — rejected because it recouples the three rates of change the split exists to separate (the whole point). -- **`@cordisjs/plugin-capability`** — a different axis entirely: it is a permission/capability-*security* service (named permissions with inheritance, tested against a session via `ctx.capability.test`), a candidate for the deferred permissions/sandbox work on the `tools/pre-execute` deny/ask seam, NOT a mechanism for swapping implementations. Confusing the two ("capability") is the trap this RFC names. +- **`@cordisjs/plugin-capability`** — a different axis entirely: it is a permission/capability-*security* service (named permissions with inheritance, tested against a session via `ctx.capability.test`), a candidate for the deferred permissions/sandbox work on the `tools/pre-execute` deny/ask seam, NOT a mechanism for swapping implementations. Confusing the two ("capability") is the trap this Agent Note names. ## Consequences -More packages and more boilerplate per capability (a `package.json`/`tsconfig`/README trio, the inject wiring). Bought: implementations and consumers ship and version independently, and a new backend never risks the model-facing contract. The rule is documented in [AGENTS.md](../../../../AGENTS.md) § Conventions ("Capability seams are three packages") and [architecture.md](../../../architecture.md) § "Capability seams"; the bash trio is the reference template. When to fold vs. split is a judgment call the architecture doc spells out — this RFC records *why* the default is to split. +More packages and more boilerplate per capability (a `package.json`/`tsconfig`/README trio, the inject wiring). Bought: implementations and consumers ship and version independently, and a new backend never risks the model-facing contract. The rule is documented in [AGENTS.md](../../../../AGENTS.md) § Conventions ("Capability seams are three packages") and [architecture.md](../../../../docs/architecture.md) § "Capability seams"; the bash trio is the reference template. When to fold vs. split is a judgment call the architecture doc spells out — this Agent Note records *why* the default is to split. diff --git a/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md similarity index 96% rename from docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md rename to .agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md index 0ded28f598..7f2f5933ad 100644 --- a/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md +++ b/.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md @@ -1,4 +1,4 @@ -# RFC: Two LLM adapters as a design-verification twin +# Agent Note: Two LLM adapters as a design-verification twin Status: implemented @@ -22,4 +22,4 @@ The rule they enforce: **anything the StreamChunk vocabulary cannot express for ## Consequences -The twin doubles adapter and key-gated e2e maintenance—both cover V4 Flash and Pro across representative reasoning modes—in exchange for continuous seam-neutrality validation and a second implementation example. Both use `apiKey`, `baseURL`, and `models`; the hand-rolled adapter exposes `thinking`/`reasoningEffort`, while pi-ai exposes one `reasoning` level. A future conformance suite could justify retiring one adapter through a superseding RFC. +The twin doubles adapter and key-gated e2e maintenance—both cover V4 Flash and Pro across representative reasoning modes—in exchange for continuous seam-neutrality validation and a second implementation example. Both use `apiKey`, `baseURL`, and `models`; the hand-rolled adapter exposes `thinking`/`reasoningEffort`, while pi-ai exposes one `reasoning` level. A future conformance suite could justify retiring one adapter through a superseding Agent Note. diff --git a/docs/rfc/implemented/architecture/2026-06-14-session-persistence.md b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md similarity index 90% rename from docs/rfc/implemented/architecture/2026-06-14-session-persistence.md rename to .agents/notes/implemented/architecture/2026-06-14-session-persistence.md index 3bd3c3fdf1..e84d7fefd3 100644 --- a/docs/rfc/implemented/architecture/2026-06-14-session-persistence.md +++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md @@ -1,10 +1,10 @@ -# RFC: Session persistence as an abstract service over the existing `SessionEvent` +# Agent Note: Session persistence as an abstract service over the existing `SessionEvent` Status: implemented ## Problem -Sessions lived only in memory. The example `session-jsonl.ts` plugin (duplicated byte-for-byte in both examples) was write-only telemetry: it buffered `session/event` and appended JSON lines, with no read/replay path, no crash-safety (no fsync, no atomic write, a fire-and-forget dispose drain), no listing, and no format versioning. Nothing could rehydrate a past session from disk into a live agent, so durable resume ("continue yesterday's task"), durable forking, and the ACP `session/load` method ([ACP support](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md)) were all impossible. +Sessions lived only in memory. The example `session-jsonl.ts` plugin (duplicated byte-for-byte in both examples) was write-only telemetry: it buffered `session/event` and appended JSON lines, with no read/replay path, no crash-safety (no fsync, no atomic write, a fire-and-forget dispose drain), no listing, and no format versioning. Nothing could rehydrate a past session from disk into a live agent, so durable resume ("continue yesterday's task"), durable forking, and the ACP `session/load` method ([ACP support](../feature/2026-06-14-acp-agent-client-protocol.md)) were all impossible. The [event-sourced model](2026-06-11-event-sourced-sessions.md) makes the append-only log the single source of truth and derives LLM history from it. Persistence had to stay faithful to that: persist the existing `SessionEvent` directly, with no parallel "persisted message" type that the log is converted to and from. The backend also had to be swappable — a file store now, a database store later — behind one interface. @@ -31,4 +31,4 @@ Format versioning: the header carries a `version`; `load` rejects any non-curren ## Consequences -Two new packages and the metadata seam in `dsh-session` (`session.header`, the `create(id?, options?)` signature). Bought: durable resume/fork, a read/replay path, crash tolerance, and the foundation the ACP `session/load` ([ACP support](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md)) needs — all over the existing event-sourced log, with the backend swappable behind one interface. The reusable `runPersistenceContract` suite holds every backend to the same append-only, contiguous-seq, lazy-materialization, and serializability semantics. Persisting the full log also settles event fidelity: `assistant/chunk` remains verbatim. +Two new packages and the metadata seam in `dsh-session` (`session.header`, the `create(id?, options?)` signature). Bought: durable resume/fork, a read/replay path, crash tolerance, and the foundation the ACP `session/load` ([ACP support](../feature/2026-06-14-acp-agent-client-protocol.md)) needs — all over the existing event-sourced log, with the backend swappable behind one interface. The reusable `runPersistenceContract` suite holds every backend to the same append-only, contiguous-seq, lazy-materialization, and serializability semantics. Persisting the full log also settles event fidelity: `assistant/chunk` remains verbatim. diff --git a/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md b/.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md similarity index 98% rename from docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md rename to .agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md index a54f735219..74ab8b6bf3 100644 --- a/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md +++ b/.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md @@ -1,4 +1,4 @@ -# RFC: Every session event is enclosed in a turn +# Agent Note: Every session event is enclosed in a turn Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md similarity index 90% rename from docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md rename to .agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md index 0121a11fa0..e76ca22e2d 100644 --- a/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md +++ b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md @@ -1,4 +1,4 @@ -# RFC: Filesystem capability seam — ctx.fs, local backend, and model-facing filesystem tools +# Agent Note: Filesystem capability seam — ctx.fs, local backend, and model-facing filesystem tools Status: implemented @@ -18,7 +18,7 @@ We need the filesystem tools to land in the same capability-seam shape as bash b ## Decision -Filesystem access is a first-class capability seam following [the capability-seam RFC](../../implemented/architecture/2026-06-13-capability-seams.md): +Filesystem access is a first-class capability seam following [the capability-seam Agent Note](2026-06-13-capability-seams.md): 1. `@deepseek-ai/dsh-fs` (`packages/fs/fs`) owns the abstract `ctx.fs` service, the filesystem vocabulary types, and the `fs/*` policy event vocabulary. 2. `@deepseek-ai/dsh-fs-local` (`packages/fs/fs-local`) provides the first implementation, backed by the local filesystem. @@ -26,7 +26,7 @@ Filesystem access is a first-class capability seam following [the capability-sea The consumer package depends only on the interface package, never on `dsh-fs-local`. A deployment that wants a different backend loads a different provider for `ctx.fs` without changing the tool schemas or model-facing prompt guidance. -The read-before-write/edit and observed-state policy is a fourth package, `@deepseek-ai/dsh-fs-policy` (`packages/fs/fs-policy`), contributed through the `fs/*` event gate rather than living on `ctx.fs`; a deployment loading `dsh-tool-fs` also loads `dsh-fs-policy` to get read-before-write/edit. This RFC established the three-package seam; the split of policy off the provider base class is decided by [the split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md), and its realization as an event-gate plugin (not a method service) by [the event-gate RFC](2026-06-26-file-context-as-event-gate.md). This document is updated to describe that landed four-package shape. +The read-before-write/edit and observed-state policy is a fourth package, `@deepseek-ai/dsh-fs-policy` (`packages/fs/fs-policy`), contributed through the `fs/*` event gate rather than living on `ctx.fs`; a deployment loading `dsh-tool-fs` also loads `dsh-fs-policy` to get read-before-write/edit. This Agent Note established the three-package seam; the split of policy off the provider base class is decided by [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md), and its realization as an event-gate plugin (not a method service) by [the event-gate Agent Note](2026-06-26-file-context-as-event-gate.md). This document is updated to describe that landed four-package shape. The first backend is deliberately local-only: `dsh-fs-local` implements `ctx.fs` against the host filesystem. Future sibling backends can provide sandboxed, remote, virtual, or project-scoped filesystems behind the same interface. @@ -34,7 +34,7 @@ The first consumer is deliberately text-file-only: `dsh-tool-fs` exposes model-f Filesystem permissions and sandboxing are not implied by this split. The local backend resolves relative paths from its configured base directory, but containment policy is a separate decision: either a stricter `ctx.fs` implementation enforces it, or a permission/sandbox plugin wraps `tools/execute` and vetoes calls before they reach the consumer. -Read-before-write/edit and observed state belong to `dsh-fs-policy`, not `ctx.fs`. Through the `fs/*` event gate, the policy records versions per opaque actor and supplies optional mutation expectations; the provider enforces freshness atomically. `dsh-tool-fs` emits the events without depending on the policy. See the [split-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [event-gate](2026-06-26-file-context-as-event-gate.md) RFCs. +Read-before-write/edit and observed state belong to `dsh-fs-policy`, not `ctx.fs`. Through the `fs/*` event gate, the policy records versions per opaque actor and supplies optional mutation expectations; the provider enforces freshness atomically. `dsh-tool-fs` emits the events without depending on the policy. See the [split-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [event-gate](2026-06-26-file-context-as-event-gate.md) Agent Notes. ## Package topology @@ -71,7 +71,7 @@ The provider seam also carries the freshness hooks that policy builds on — but - `writeText`/`editText` take an OPTIONAL version expectation: omit it for an unconditional bare-provider mutation, or supply it to guard the mutation inside the backend's atomic critical section. - The `dsh-fs-policy` plugin decides that expectation on `fs/write-intent`/`fs/edit-intent` and records observed versions on `fs/observed`, keyed by an owner it derives from the opaque event actor (normally `exec.agent.session`). -Authorization is version freshness, not a full/partial view distinction: any read records the target's version, and a later write/edit is authorized as long as the file is still at that version — so a windowed read of lines 100-150 authorizes an edit of line 120. The observed-state store is a `WeakMap<owner, Map<targetKey, version>>` inside `dsh-fs-policy`; `dsh-fs` holds none of it and treats the actor as opaque. (This RFC first modeled a `FileState` cache with `full`/`partial` views on `ctx.fs`; the split-fs-seam and event-gate RFCs replaced that with the freshness-based policy plugin described here.) +Authorization is version freshness, not a full/partial view distinction: any read records the target's version, and a later write/edit is authorized as long as the file is still at that version — so a windowed read of lines 100-150 authorizes an edit of line 120. The observed-state store is a `WeakMap<owner, Map<targetKey, version>>` inside `dsh-fs-policy`; `dsh-fs` holds none of it and treats the actor as opaque. (This Agent Note first modeled a `FileState` cache with `full`/`partial` views on `ctx.fs`; the split-fs-seam and event-gate Agent Notes replaced that with the freshness-based policy plugin described here.) Path resolution is explicit and allowed to be async. Local resolution may only normalize a path, but sandboxed/remote/project-scoped backends may need I/O to resolve a user-supplied path into a stable target identity. @@ -135,7 +135,7 @@ The defensive-pattern classes this repo has been bitten by are pinned directly: - **Model-facing tools directly over `node:fs`** — the tool package would own execution policy, path resolution, atomic writes, text decoding, and edit semantics at once, coupling the three independently-changing concerns the Problem names and churning schemas on any backend swap. - **One combined `dsh-fs-tools` package** — the pre-seam shape; rejected for the same interface/implementation/consumer split as bash, and the combined name never became public surface. -- **Observed-state on `ctx.fs`** — the shape this RFC first landed; superseded by [the split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [the event-gate RFC](2026-06-26-file-context-as-event-gate.md): a sandboxed/remote backend must not inherit model-facing observation policy, so the provider keeps only the version token and the optional version-guarded mutation. +- **Observed-state on `ctx.fs`** — the shape this Agent Note first landed; superseded by [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [the event-gate Agent Note](2026-06-26-file-context-as-event-gate.md): a sandboxed/remote backend must not inherit model-facing observation policy, so the provider keeps only the version token and the optional version-guarded mutation. ## Consequences @@ -143,11 +143,11 @@ The defensive-pattern classes this repo has been bitten by are pinned directly: **The interface can become too local.** Returning fields such as `absolutePath` from `ctx.fs` would make remote, sandboxed, or virtual backends awkward. The contract should expose display metadata without requiring consumers to understand host paths. -**The interface can become too thin.** If `ctx.fs` only mirrors `node:fs` primitives, `tool-fs` will reimplement binary detection, pagination, atomic writes, and edit semantics. That recreates the coupling this RFC is trying to avoid. +**The interface can become too thin.** If `ctx.fs` only mirrors `node:fs` primitives, `tool-fs` will reimplement binary detection, pagination, atomic writes, and edit semantics. That recreates the coupling this Agent Note is trying to avoid. **Edit semantics are race-prone by nature.** Literal edit is a read-modify-write operation; the guard is the backend's atomic mutation critical section plus the optional version expectation, so concurrent edits settle deterministically — one wins, the other gets `FS_STALE_VERSION`. -**Observed state does not belong on `ctx.fs`.** Recording what an execution context has seen is workflow policy, not raw filesystem I/O. This RFC first placed it inside the filesystem seam; the split-fs-seam RFC then established that a sandboxed/remote backend should not inherit model-facing observation policy, and moved it into the `dsh-fs-policy` plugin. The provider seam keeps only what write/edit safety genuinely needs at the storage layer — a backend-minted version token and an optional version-guarded mutation — while the policy plugin owns owner derivation, observed-state, and read-before-edit gating over the `fs/*` events. +**Observed state does not belong on `ctx.fs`.** Recording what an execution context has seen is workflow policy, not raw filesystem I/O. This Agent Note first placed it inside the filesystem seam; the split-fs-seam Agent Note then established that a sandboxed/remote backend should not inherit model-facing observation policy, and moved it into the `dsh-fs-policy` plugin. The provider seam keeps only what write/edit safety genuinely needs at the storage layer — a backend-minted version token and an optional version-guarded mutation — while the policy plugin owns owner derivation, observed-state, and read-before-edit gating over the `fs/*` events. **The `resolve`-then-operate shape costs an extra round-trip per call.** Each tool may resolve a path to an `FsTarget` and then issue the read/write/edit as a separate `ctx.fs` call. For the local backend this is negligible (resolution is in-memory path normalization), but a remote/sandboxed backend may turn each step into its own request, so a single `read` can become two network round-trips. Backends where the round-trip matters can cache or fold resolution internally while preserving the observable contract. diff --git a/docs/rfc/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 similarity index 92% rename from docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md rename to .agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md index d75ec379fc..ef5bb5847a 100644 --- a/docs/rfc/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 @@ -1,4 +1,4 @@ -# RFC: Agent lifecycle and ownership seams +# Agent Note: Agent lifecycle and ownership seams Status: implemented @@ -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<string, Age These invariants hold and are pinned by tests: - ACP disconnect/session close leaves no registered agent AND no session-store entry for that session, even when `session/load` races teardown. -- `session/cancel` before a queued prompt starts prevents that prompt from running and cannot batch the next prompt into the cancelled turn. +- `session/cancel` before a queued prompt starts prevents that prompt from running; a later accepted prompt remains an independent queued turn. - A `tool-bash` HMR reload does NOT make an existing background task readable or killable by a different session (ownership survives on the executor). - Existing non-ACP demos still work without managing handles explicitly; config-created agents remain owned by the `AgentLoop` plugin fiber. @@ -41,7 +41,7 @@ The bash owner-token comparison relies on the shared `Agent.id`/`SessionId` bein - **A public `BashTask.owner` field** instead of the `BashExecutor.ownerOf(id)` seam — rejected: one read path, no redundant API. - **Sibling cordis effects for the agent's session lifecycle** — rejected: a fiber unload disposes sibling effects concurrently (`Promise.all`), racing removal of the store-owned append publication hooks against the loop's closing `session/flush`; the single composite effect's ordered LIFO chain is what captures the closing `turn/end` on both disposal paths. -- **A separate step-only `abort()` beside `cancel()`** — shipped originally, then removed as unused; `cancel()` is the single public stop primitive ([the public-stop-surface RFC](../simplification/2026-06-20-public-agent-stop-surface.md)). +- **A separate step-only `abort()` beside `cancel()`** — shipped originally, then removed as unused; `cancel()` is the single public stop primitive ([the public-stop-surface Agent Note](../simplification/2026-06-20-public-agent-stop-surface.md)). ## Consequences diff --git a/docs/rfc/implemented/architecture/2026-06-18-session-surface.md b/.agents/notes/implemented/architecture/2026-06-18-session-surface.md similarity index 95% rename from docs/rfc/implemented/architecture/2026-06-18-session-surface.md rename to .agents/notes/implemented/architecture/2026-06-18-session-surface.md index d639fe2917..1916dc1974 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-session-surface.md +++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.md @@ -1,4 +1,4 @@ -# RFC: Session surface — an ordered projection over the event log +# Agent Note: Session surface — an ordered projection over the event log Status: implemented @@ -68,3 +68,5 @@ Every surface-eligible event must carry `surfaceOp` or it would disappear from d - **`packages/session-persistence/session-persistence`**: Abstract interface unchanged. The surface is the foundation for future history manipulation. A compaction or tool-result-prune plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', start, end }` and `sourceEventSeqs` covering the shadowed entries — the new event takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically. + +A `tool/result` replacement may rewrite exactly one current `tool/result` and must preserve every data field except `content`. Session acceptance enforces this rule together with positional range and provenance validation, independent of optional diagnostic plugins. diff --git a/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md similarity index 93% rename from docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md rename to .agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md index fef6ad034c..7c73cf24a4 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md +++ b/.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md @@ -1,4 +1,4 @@ -# RFC: Shared persistence write coordinator +# Agent Note: Shared persistence write coordinator Status: implemented @@ -10,7 +10,7 @@ Status: implemented Extract a backend-agnostic `PersistenceCoordinator` into `dsh-session-persistence`. The coordinator owns the orchestration once; each first-party backend composes one (`new PersistenceCoordinator(ctx, this)`), implements a small `PersistenceBackend` hook interface, and delegates its four public service methods (`create`/`append`/`load`/`list`) to it. -Composition, not inheritance. The coordinator is a concrete class the backend holds, not a base class the backend extends. The RFC's risk — "a coordinator must not make unusual backends fight an inheritance hierarchy" — is avoided: a backend exposes only the hooks; it cannot reach the coordinator's private orchestration state, and the public `SessionPersistence` service shape is unchanged, so a third-party backend MAY still implement the abstract service directly without the coordinator at all. +Composition, not inheritance. The coordinator is a concrete class the backend holds, not a base class the backend extends. The Agent Note's risk — "a coordinator must not make unusual backends fight an inheritance hierarchy" — is avoided: a backend exposes only the hooks; it cannot reach the coordinator's private orchestration state, and the public `SessionPersistence` service shape is unchanged, so a third-party backend MAY still implement the abstract service directly without the coordinator at all. The coordinator retires each live session from its `session/disposed` notification: it waits for that exact Session object's initialization, serializes a final drain, and then removes the owned state, buffer, and init entries. Failed drains retain their buffers for backend teardown to retry. Settled per-id chain tails remove themselves only when they are still the current tail, so a completion cannot erase a newer operation for the same id. Backend teardown unregisters the write-path listeners before awaiting all admitted retirements, remaining buffers, and chains, then closes the backend. diff --git a/docs/rfc/implemented/architecture/2026-06-20-branded-ids.md b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md similarity index 96% rename from docs/rfc/implemented/architecture/2026-06-20-branded-ids.md rename to .agents/notes/implemented/architecture/2026-06-20-branded-ids.md index fd5c5bf953..e7a3110fce 100644 --- a/docs/rfc/implemented/architecture/2026-06-20-branded-ids.md +++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md @@ -1,4 +1,4 @@ -# RFC: Branded IDs everywhere they belong +# Agent Note: Branded IDs everywhere they belong Status: implemented @@ -50,11 +50,11 @@ The obvious shortcut is to type `owner` as `SessionId` directly — it always *i Kept deliberately narrow per the "not every string needs a brand" policy. Each of these is a plausible future brand, deferred with a reason, not a commitment: -- **`ModelId`** (`GenerateOptions.model`, the `LlmService` adapter-registry key) — a real cross-package lookup key (config → agent → llm → adapter); a reasonable next brand, left out only to keep this RFC's blast radius focused. +- **`ModelId`** (`GenerateOptions.model`, the `LlmService` adapter-registry key) — a real cross-package lookup key (config → agent → llm → adapter); a reasonable next brand, left out only to keep this Agent Note's blast radius focused. - **`ToolName`** (the `ToolRegistry` key) — author-defined, human-readable, and rarely confused with another id; the weakest candidate, likely not worth a brand. - **`ErrorCode`** (`HarnessError.code`) — a closed vocabulary (`ABORTED`, `NO_ADAPTER`, …), not a per-instance id; better served by a string-literal union than a brand, if anything. - **Numeric ordinals** — turn number, step number, and the event `seq` are `number`, not `string`, so `Branded<string>` does not apply; a parallel `number & { readonly [BRAND]: B }` variant could brand them, but they are positional ordinals rarely passed across boundaries, so the payoff is low. -- **Validated construction** — the brand factories are pure casts with no runtime check, and every boundary (ACP `sessionId`, provider-issued `call.id`, the empty-string fallback in `dsh-llm-deepseek`) trusts the raw string today. A `SessionId.parse()` / `isValid()` companion that throws on malformed input at boundaries is a genuine gap, but it is a *runtime-behavior* change with its own design (what is "malformed"? what do we do on failure?) and belongs in its own RFC, not bundled into this type-only pass. +- **Validated construction** — the brand factories are pure casts with no runtime check, and every boundary (ACP `sessionId`, provider-issued `call.id`, the empty-string fallback in `dsh-llm-deepseek`) trusts the raw string today. A `SessionId.parse()` / `isValid()` companion that throws on malformed input at boundaries is a genuine gap, but it is a *runtime-behavior* change with its own design (what is "malformed"? what do we do on failure?) and belongs in its own Agent Note, not bundled into this type-only pass. ## Verification @@ -63,5 +63,5 @@ The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-bash` a ## Consequences - **Mechanical churn across two surfaces.** Propagating brands touches the bash seam (interface + impl + consumer) and the ACP session-id surface plus the persistence coordinator. The churn is broad but low-severity: a missed site is a compile error, not a silent bug. The change is observably type-only — no snapshot or e2e behavioral diff. It sits next to the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md) because both touch the session-id / owner-token boundary; `OwnerToken` stays distinct from the unified id for the decoupling reason above. -- **Brands do not validate.** A brand is a confusability guard, not a correctness proof: a *wrong* session id that is still a well-formed string passes the type checker exactly as before. This RFC does not close that gap (see Out of scope) — it only stops the *category* error of passing the wrong *kind* of id. -- **The "where to stop" line stays a judgment call.** Branding `BashTaskId` but not `ToolName`, `OwnerToken` but not `ModelId`, is a taste call about which strings "could plausibly be confused." Reasonable reviewers may want more or fewer; the policy in `brand.ts` is the tie-breaker, and this RFC errs toward the ids that are model-facing or used for access control. +- **Brands do not validate.** A brand is a confusability guard, not a correctness proof: a *wrong* session id that is still a well-formed string passes the type checker exactly as before. This Agent Note does not close that gap (see Out of scope) — it only stops the *category* error of passing the wrong *kind* of id. +- **The "where to stop" line stays a judgment call.** Branding `BashTaskId` but not `ToolName`, `OwnerToken` but not `ModelId`, is a taste call about which strings "could plausibly be confused." Reasonable reviewers may want more or fewer; the policy in `brand.ts` is the tie-breaker, and this Agent Note errs toward the ids that are model-facing or used for access control. diff --git a/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.md b/.agents/notes/implemented/architecture/2026-06-20-extract-example-app-packages.md similarity index 98% rename from docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.md rename to .agents/notes/implemented/architecture/2026-06-20-extract-example-app-packages.md index 0582515c62..3fa7227d04 100644 --- a/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.md +++ b/.agents/notes/implemented/architecture/2026-06-20-extract-example-app-packages.md @@ -1,4 +1,4 @@ -# RFC: Extract example apps into packages +# Agent Note: Extract example apps into packages Status: implemented @@ -40,7 +40,7 @@ The old `base*.yml`/`acp-tail.yml` includes already deduped the *config*, but a - Example directories contain only their config, README, and tests: `start.ts`, the infrastructure preamble, and the shared YAML includes are gone. - `demo:echo`, `demo:repl`, and `demo:acp` invoke the app-package bins. -- Each new package has a README and per-file 100% coverage; each app package also has a keyless real-Loader-path bin smoke that catches export-shape failures described in [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md). +- Each new package has a README and per-file 100% coverage; each app package also has a keyless real-Loader-path bin smoke that catches export-shape failures described in [postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md). - The ACP replay transcript remains unchanged because the plugin set and load order did not change. ## Consequences diff --git a/docs/rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md similarity index 95% rename from docs/rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md rename to .agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md index c99eb9f0fe..4db0d78910 100644 --- a/docs/rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md +++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md @@ -1,4 +1,4 @@ -# RFC: The background task runtime (`ctx.tasks`) and generic task control tools +# Agent Note: The background task runtime (`ctx.tasks`) and generic task control tools Status: implemented @@ -21,7 +21,7 @@ Long-running tools are producers. `dsh-tool-bash` adapts a `BashProcess` into in ## Runtime contract -The literal types live in the [task data-structure catalog](../../../core-data-structures/tasks.md). A producer calls `ctx.tasks.start()` with a kind, label, optional owning `Agent`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable task id. +The literal types live in the [task data-structure catalog](../../../../docs/core-data-structures/tasks.md). A producer calls `ctx.tasks.start()` with a kind, label, optional owning `Agent`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable task id. The producer hooks define three responsibilities: @@ -123,6 +123,6 @@ Unit coverage pins preflight atomicity, per-kind ids, stream and final reads, wa ## Consequences -Bash commands and subagents share one id vocabulary, listing, notice format, prompt habit, and set of control tools. New long-running producers implement execution hooks instead of another registry and tool family. The [tool cookbook](../../../cookbook/adding-a-tool.md) points producers to this contract. +Bash commands and subagents share one id vocabulary, listing, notice format, prompt habit, and set of control tools. New long-running producers implement execution hooks instead of another registry and tool family. The [tool cookbook](../../../../docs/cookbook/adding-a-tool.md) points producers to this contract. Owned background bash now stops with its agent instead of surviving it. Background processes have no executor timeout; callers must kill irrelevant work or rely on owner/service disposal. Stream reads support one consuming reader, completion notices do not wake idle agents, and a producer that returns from `cancel` without settling `done` can still stall teardown. Durable jobs, independent observation cursors, and foreground promotion remain separate designs. diff --git a/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.md b/.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md similarity index 99% rename from docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.md rename to .agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md index 89b50eb3df..d853696226 100644 --- a/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.md +++ b/.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md @@ -1,4 +1,4 @@ -# RFC: Reorganize packages into a modular hierarchy +# Agent Note: Reorganize packages into a modular hierarchy Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md similarity index 81% rename from docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md rename to .agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md index 102a29d613..fdd98b89cb 100644 --- a/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md +++ b/.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md @@ -1,10 +1,10 @@ -# RFC: Mandatory `User-Agent` attribution for provider requests +# Agent Note: Mandatory `User-Agent` attribution for provider requests Status: implemented ## Problem -LLM provider requests should identify the product making them. That is useful for provider-side support, abuse investigation, compatibility debugging, and traffic analytics. Before this RFC the harness only partially did this: the hand-rolled DeepSeek adapter sent a hand-copied `User-Agent` constant (`packages/llm/llm-deepseek/src/adapter.ts`), while the pi-ai-backed twin sent no harness-owned headers at all (`packages/llm/llm-pi-ai/src/adapter.ts`). New adapters could therefore omit attribution silently, and a library-backed adapter could drift from the hand-rolled adapter even though [the twin-adapter RFC](2026-06-13-twin-llm-adapters.md) exists to keep the provider seam honest across both implementations. +LLM provider requests should identify the product making them. That is useful for provider-side support, abuse investigation, compatibility debugging, and traffic analytics. Before this Agent Note the harness only partially did this: the hand-rolled DeepSeek adapter sent a hand-copied `User-Agent` constant (`packages/llm/llm-deepseek/src/adapter.ts`), while the pi-ai-backed twin sent no harness-owned headers at all (`packages/llm/llm-pi-ai/src/adapter.ts`). New adapters could therefore omit attribution silently, and a library-backed adapter could drift from the hand-rolled adapter even though [the twin-adapter Agent Note](2026-06-13-twin-llm-adapters.md) exists to keep the provider seam honest across both implementations. The immediate prompt came from OpenRouter's [App Attribution](https://openrouter.ai/docs/app-attribution) docs. OpenRouter creates app pages and rankings from `HTTP-Referer` plus display/category headers. That is valuable, but it is not the HTTP standard for application identity. The risk is adopting OpenRouter's exact header set as if it were universal, then leaking provider-specific headers to direct DeepSeek requests, future OpenAI/Anthropic/Vertex adapters, test servers, or proxies that log unknown fields indefinitely. @@ -24,11 +24,11 @@ The immediate prompt came from OpenRouter's [App Attribution](https://openrouter Provider request attribution is mandatory at the LLM adapter boundary, using the standard `User-Agent` header only. The rule: every product LLM adapter sends a static, non-secret application identity on every provider HTTP request, and every adapter has tests proving that `User-Agent` reaches the wire (a mock server asserting received headers; for a library-backed adapter, the library's header hook feeding the same mock-server assertion). -Do **not** implement OpenRouter app attribution in this RFC. `HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, and `X-OpenRouter-Categories` are OpenRouter-specific product-surface headers, not provider-neutral model-request attribution. They can be proposed later by an OpenRouter adapter or explicit OpenRouter mode, with its own privacy/product decision, tests, and docs. Until then, even requests pointed at OpenRouter send only the shared `User-Agent` attribution from this RFC. +Do **not** implement OpenRouter app attribution in this Agent Note. `HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, and `X-OpenRouter-Categories` are OpenRouter-specific product-surface headers, not provider-neutral model-request attribution. They can be proposed later by an OpenRouter adapter or explicit OpenRouter mode, with its own privacy/product decision, tests, and docs. Until then, even requests pointed at OpenRouter send only the shared `User-Agent` attribution from this Agent Note. The provider-neutral identity is owned by `dsh-llm` (`packages/llm/llm/src/attribution.ts`), not by individual adapters. `AppIdentity` contains only public product facts needed to build `User-Agent`, and the default `APP_IDENTITY` settles the values the proposal left open: -- product token for `User-Agent`: `deepseek-harness` (continuity with the pre-RFC wire value and the repo/org identity) +- product token for `User-Agent`: `deepseek-harness` (continuity with the pre-Agent Note wire value and the repo/org identity) - version: read from the owning package's manifest via `createRequire`, never a hand-copied constant - app URL: `https://github.com/deepseek-ai/deepseek-harness-sdk` - the planned public home; a `FIXME` in `attribution.ts` blocks release until that repository actually exists @@ -40,10 +40,10 @@ Wire mapping (`attributionHeaders`; header names lowercase in code - HTTP field |---|---| | All HTTP-based adapters | `User-Agent: {product}/{version} (+{url})` - the parenthesized `+url` comment stays within RFC 9110's conservative product/comment syntax. | | Direct DeepSeek endpoint | `User-Agent`; do not send OpenRouter-only headers unless DeepSeek documents an equivalent contract. | -| OpenRouter endpoints | `User-Agent` only for now. Do not send `HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, or `X-OpenRouter-Categories` under this RFC. | -| Future providers | `User-Agent` only unless a later provider-specific RFC accepts additional headers. Do not reuse `HTTP-Referer` by analogy. | +| OpenRouter endpoints | `User-Agent` only for now. Do not send `HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, or `X-OpenRouter-Categories` under this Agent Note. | +| Future providers | `User-Agent` only unless a later provider-specific Agent Note accepts additional headers. Do not reuse `HTTP-Referer` by analogy. | -Endpoint detection is not part of this RFC because no endpoint-specific mapping is accepted here. If OpenRouter support lands later, detection must be explicit: either a dedicated OpenRouter provider package or an explicit `provider: 'openrouter'` / `attributionTarget: 'openrouter'` config, not arbitrary path fragments or model names. +Endpoint detection is not part of this Agent Note because no endpoint-specific mapping is accepted here. If OpenRouter support lands later, detection must be explicit: either a dedicated OpenRouter provider package or an explicit `provider: 'openrouter'` / `attributionTarget: 'openrouter'` config, not arbitrary path fragments or model names. ## Verification @@ -53,19 +53,19 @@ The landed contract: - A shared helper (`attributionHeaders` / `userAgent`) constructs the app identity and the standard `User-Agent` value from package metadata, so adapters do not hand-copy version constants. - `dsh-llm-deepseek` sends the shared `User-Agent` on every request and its mock-server suite asserts the exact value. - `dsh-llm-pi-ai` sends the same `User-Agent` through pi-ai's `StreamOptions.headers` hook and its mock-server suite asserts the exact value. -- No adapter sends OpenRouter-specific attribution headers (`HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, `X-OpenRouter-Categories`) as part of this RFC. +- No adapter sends OpenRouter-specific attribution headers (`HTTP-Referer`, `X-OpenRouter-Title`, `X-Title`, `X-OpenRouter-Categories`) as part of this Agent Note. - No app-attribution field carries secrets, local paths, session ids, prompt text, model output, user email, or per-user stable identifiers. - The adapter READMEs state the `User-Agent` attribution policy and explicitly avoid documenting OpenRouter app attribution as implemented behavior. ## Alternatives considered -**OpenRouter app attribution now.** Rejected for this RFC. Sending `HTTP-Referer` plus `X-OpenRouter-Title` would satisfy OpenRouter rankings, but those headers are a provider-specific product feature, not the provider-neutral model-request attribution this RFC is trying to standardize. Supporting them should be an explicit OpenRouter adapter/mode decision later, not hidden inside the first shared attribution helper. +**OpenRouter app attribution now.** Rejected for this Agent Note. Sending `HTTP-Referer` plus `X-OpenRouter-Title` would satisfy OpenRouter rankings, but those headers are a provider-specific product feature, not the provider-neutral model-request attribution this Agent Note is trying to standardize. Supporting them should be an explicit OpenRouter adapter/mode decision later, not hidden inside the first shared attribution helper. **OpenRouter headers everywhere.** Rejected. It would treat a custom OpenRouter contract as a universal standard and send fields with misleading semantics to providers that did not ask for them. It also risks using `HTTP-Referer` as a generic app URL field even though standard HTTP already has `User-Agent` for product identity and `Referer` for a different browsing-context concept. **Only provider account/project identity.** Rejected. Organization/project headers, API keys, cloud accounts, and billing projects identify who pays or owns the request, not which application is sending traffic. They also expose no public app title/category and do not help gateways like OpenRouter build app rankings. -**End-user `user`/`metadata` fields.** Rejected for this RFC. Those are valuable for abuse monitoring and customer support but describe the human or tenant behind a request. App attribution must be static product identity and safe to send on every request. +**End-user `user`/`metadata` fields.** Rejected for this Agent Note. Those are valuable for abuse monitoring and customer support but describe the human or tenant behind a request. App attribution must be static product identity and safe to send on every request. **Config-only opt-in attribution.** Rejected. A default-off setting is exactly how adapters keep drifting. The policy is mandatory default attribution with overrideable public values, not optional attribution. diff --git a/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md similarity index 97% rename from docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md rename to .agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md index 2909ccd0e6..b3dad98f18 100644 --- a/docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md +++ b/.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md @@ -1,4 +1,4 @@ -# RFC: Web capability seam - stable tools over multiple providers +# Agent Note: Web capability seam - stable tools over multiple providers Status: implemented @@ -14,7 +14,7 @@ There is also a provider-selection question. Existing `tool-bash` and `tool-fs` ## Decision -Web access is a first-class capability seam following [the capability-seam RFC](../../implemented/architecture/2026-06-13-capability-seams.md): +Web access is a first-class capability seam following [the capability-seam Agent Note](2026-06-13-capability-seams.md): 1. `@deepseek-ai/dsh-web` (`packages/web/web`) owns `ctx.web`, provider registration, provider selection, shared request/result vocabulary, and web-specific errors. 2. Provider packages implement concrete backends and register capabilities with `ctx.web`, for example `@deepseek-ai/dsh-web-search-exa`, `@deepseek-ai/dsh-web-search-perplexity`, `@deepseek-ai/dsh-web-search-deepseek`, and `@deepseek-ai/dsh-web-fetch-local`. @@ -32,7 +32,7 @@ Search and fetch are separate tools but one web-access seam. `ctx.web` owns prov This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. If web search is enabled but no usable search provider exists, `web_search` remains visible and execution fails with a structured `WebError` such as `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. If a provider appears after `dsh-tool-web`, the next execution can use it without changing the schema. If a provider disappears mid-call, execution fails with a structured `WebError` instead of silently choosing another provider or falling through to `UNKNOWN_TOOL`. -The seam deliberately exposes no observation surface — no registry-change event and no aggregated capability-status query. Unavailability is a fact a caller observes by executing: `search()`/`fetch()` resolve the provider at call time and throw the structured `WebError` that names what failed. [The observation-surface RFC](../simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) records that judgment: derived-on-call selection and enablement-based registration leave no consumer that needs a change signal or an availability probe distinct from executing and routing the error, and a future provider-status panel reintroduces the smallest signal or query it actually consumes. +The seam deliberately exposes no observation surface — no registry-change event and no aggregated capability-status query. Unavailability is a fact a caller observes by executing: `search()`/`fetch()` resolve the provider at call time and throw the structured `WebError` that names what failed. [The observation-surface Agent Note](../simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) records that judgment: derived-on-call selection and enablement-based registration leave no consumer that needs a change signal or an availability probe distinct from executing and routing the error, and a future provider-status panel reintroduces the smallest signal or query it actually consumes. ## Package topology @@ -276,7 +276,7 @@ Tool execution lets these errors flow through `ToolRegistry.execute()`, which al ## Testing -Each layer is pinned at its own seam: the registry/selection/truncation/abort contract and the `WebError` codes in `dsh-web`; per-provider request/response mapping over recorded fixtures (Perplexity fixtures include URL-only citations so the optional source fields stay honest) plus a self-skipping with-key smoke per real provider; real local-HTTP behavior in `web-fetch-local`; and enablement-driven registration, structured execution errors, and result formatting through the real tool registry in `dsh-tool-web`. A real-Loader smoke guards the two export shapes ([postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md)): `dsh-web` is a default-exported service, while the providers and `tool-web` are namespace plugins where a stray `export default` would drop `inject`. +Each layer is pinned at its own seam: the registry/selection/truncation/abort contract and the `WebError` codes in `dsh-web`; per-provider request/response mapping over recorded fixtures (Perplexity fixtures include URL-only citations so the optional source fields stay honest) plus a self-skipping with-key smoke per real provider; real local-HTTP behavior in `web-fetch-local`; and enablement-driven registration, structured execution errors, and result formatting through the real tool registry in `dsh-tool-web`. A real-Loader smoke guards the two export shapes ([postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)): `dsh-web` is a default-exported service, while the providers and `tool-web` are namespace plugins where a stray `export default` would drop `inject`. ## Alternatives considered diff --git a/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md b/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md similarity index 92% rename from docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md rename to .agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md index 486d9aafb4..3f34cf3559 100644 --- a/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md +++ b/.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md @@ -1,10 +1,10 @@ -# RFC: Make `dsh-fs-policy` an event-gate plugin, not a method interface +# Agent Note: Make `dsh-fs-policy` an event-gate plugin, not a method interface Status: implemented ## Problem -[The split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) put `ctx.fileContext` between the model-facing tools and the `ctx.fs` provider: `dsh-tool-fs` injects `fileContext` and routes every `read`/`write`/`edit` through its methods. That makes `fileContext` **in-path and mandatory**. The tool cannot reach `ctx.fs` without it, the policy layer owns the fs I/O and the read windowing, and a deployment that does not want observed-state policy cannot simply drop the package — `dsh-tool-fs` would fail to resolve `ctx.fileContext`. +[The split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) put `ctx.fileContext` between the model-facing tools and the `ctx.fs` provider: `dsh-tool-fs` injects `fileContext` and routes every `read`/`write`/`edit` through its methods. That makes `fileContext` **in-path and mandatory**. The tool cannot reach `ctx.fs` without it, the policy layer owns the fs I/O and the read windowing, and a deployment that does not want observed-state policy cannot simply drop the package — `dsh-tool-fs` would fail to resolve `ctx.fileContext`. This couples three things that should be separable: @@ -148,7 +148,7 @@ Both mutations are still atomic (the backend's per-target lock is unconditional) ## Supersedes -This amends — does not reverse — [the split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md). The four-layer split, the provider contract, and the freshness *policy* are all kept. What changes is the **coupling between the tool and the policy layer**: a mandatory method service became a plugin-owned event gate, and the fs I/O + read windowing moved from `fileContext` up into `dsh-tool-fs`. The split-fs-seam RFC's description of `dsh-tool-fs` injecting `fileContext` and of `fileContext` owning `read`/`write`/`edit` was updated to match in the same change. +This amends — does not reverse — [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md). The four-layer split, the provider contract, and the freshness *policy* are all kept. What changes is the **coupling between the tool and the policy layer**: a mandatory method service became a plugin-owned event gate, and the fs I/O + read windowing moved from `fileContext` up into `dsh-tool-fs`. The split-fs-seam Agent Note's description of `dsh-tool-fs` injecting `fileContext` and of `fileContext` owning `read`/`write`/`edit` was updated to match in the same change. ## Verification @@ -156,7 +156,7 @@ Tests pin both paths: without `dsh-fs-policy`, the root tool plugin boots agains ## Alternatives considered -- **Keep `ctx.fileContext` as an in-path method service** — the shape [the split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) first landed; rejected because the tool could not run without the policy layer, making policy load-bearing for basic operation instead of an opt-in tightening. +- **Keep `ctx.fileContext` as an in-path method service** — the shape [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) first landed; rejected because the tool could not run without the policy layer, making policy load-bearing for basic operation instead of an opt-in tightening. - **Policy-side version checking** (`dsh-fs-policy` stats and compares in its waterfall handler) — rejected for the TOCTOU gap between that check and the tool's actual write; the provider's mutation critical section is the only race-free place, so the policy only chooses the CAS basis and gates on prior observation. - **Per-tool `/read`/`/write`/`/edit` subpath plugins** — dropped on implementation: no consumer needed a single-tool deployment, and subpath publishing forced bespoke `tsdown`/`tsconfig`/`files`/workspace-constraint handling no sibling tool package carries; the per-tool registration helpers remain internal modules the root plugin composes. @@ -164,6 +164,6 @@ Tests pin both paths: without `dsh-fs-policy`, the root tool plugin boots agains - **Event indirection over a method call.** A waterfall + emit is less direct than `await ctx.fileContext.edit(...)`. The payoff is removing the tool-to-policy method dependency while keeping the default policy plugin; the cost is one more event vocabulary to learn. Mitigated by keeping the three events narrow and documenting the default-thunk semantics on each. - **Policy events in the storage seam.** `dsh-fs` gains two version-decision events plus a recording event though it is "just storage". This is the price of decoupling (the emitter cannot depend on the policy plugin). The events carry only `dsh-fs` vocabulary plus an opaque `object` actor and no model-facing concepts, so the seam stays free of line-window/observation policy types and of the agent/session owner structure. -- **Single policy occupant, first-wins by convention.** The `fs/write-intent`/`fs/edit-intent` slots hold exactly one decider; the first-registered (or `prepend`ed) listener wins and the rest are short-circuited. `dsh-fs-policy` owning the slot is a deployment convention, not an event-enforced invariant — a second decider registered first would bypass it. This is acceptable because a second fs-version-policy decider is a misconfiguration, not a feature. If a future need for *layered* fs version policy appears, it is a new RFC (a composable value-passing seam), not a silent second listener on these events. Layered permission/audit/sandbox interception already has its home on `tools/execute`. +- **Single policy occupant, first-wins by convention.** The `fs/write-intent`/`fs/edit-intent` slots hold exactly one decider; the first-registered (or `prepend`ed) listener wins and the rest are short-circuited. `dsh-fs-policy` owning the slot is a deployment convention, not an event-enforced invariant — a second decider registered first would bypass it. This is acceptable because a second fs-version-policy decider is a misconfiguration, not a feature. If a future need for *layered* fs version policy appears, it is a new Agent Note (a composable value-passing seam), not a silent second listener on these events. Layered permission/audit/sandbox interception already has its home on `tools/execute`. - **Dropping the post-read confirming stat** makes a follow-up *guarded* edit occasionally fail-closed (`FS_STALE_VERSION` → re-read) under a read/write race. This is a UX nicety lost, never a correctness hole; the provider lock still prevents wrong-version writes. - **The bare provider does no read-before-write/edit and no version check.** A deployment without `dsh-fs-policy` lets the model overwrite or edit any existing file unconditionally. This is the deliberate meaning of keeping the tool independent of a policy service: the safety disciplines live in the `dsh-fs-policy` plugin. A deployment that omits it is opting into an unconstrained filesystem on purpose; that is not the intended stance for a config that ships the fs tools. diff --git a/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md similarity index 94% rename from docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md rename to .agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md index bc91d425b6..cc1de3c53e 100644 --- a/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md +++ b/.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md @@ -1,12 +1,12 @@ -# RFC: stdin + extra env on the bash seam +# Agent Note: stdin + extra env on the bash seam Status: implemented ## Problem -The hooks subsystem runs external hook commands the way Claude Code and Codex do: a hook is a shell command that receives its event payload as **JSON on stdin** and reads context from a handful of **environment variables** (`CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, `PLUGIN_ROOT`, …). The harness already has a perfectly good command runner behind the `ctx.bash` capability seam ([dsh-bash](../../../../packages/bash/bash) → [dsh-bash-local](../../../../packages/bash/bash-local)), with process-group kills, output truncation/spill, and a credential scrub. Reusing it for hook execution means a hook bridge does not re-implement subprocess plumbing — but the seam had no way to write stdin or set extra env. This RFC adds those two inputs. +The hooks subsystem runs external hook commands the way Claude Code and Codex do: a hook is a shell command that receives its event payload as **JSON on stdin** and reads context from a handful of **environment variables** (`CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, `PLUGIN_ROOT`, …). The harness already has a perfectly good command runner behind the `ctx.bash` capability seam ([dsh-bash](../../../../packages/bash/bash) → [dsh-bash-local](../../../../packages/bash/bash-local)), with process-group kills, output truncation/spill, and a credential scrub. Reusing it for hook execution means a hook bridge does not re-implement subprocess plumbing — but the seam had no way to write stdin or set extra env. This Agent Note adds those two inputs. -`stdin` and `env` do not create a new model capability because ordinary shell syntax already supplies both. Ambient credentials are protected by `dsh-bash-local`'s child-environment scrub, not by hiding these seam fields; model tool arguments are static JSON and do not expand shell variables. The fields therefore serve trusted in-process callers, such as hook bridges, that need to pass structured input and `CLAUDE_*` variables without embedding them in model-visible shell text. See [defensive-patterns.md](../../../defensive-patterns.md) for the ambient-environment rule. +`stdin` and `env` do not create a new model capability because ordinary shell syntax already supplies both. Ambient credentials are protected by `dsh-bash-local`'s child-environment scrub, not by hiding these seam fields; model tool arguments are static JSON and do not expand shell variables. The fields therefore serve trusted in-process callers, such as hook bridges, that need to pass structured input and `CLAUDE_*` variables without embedding them in model-visible shell text. See [defensive-patterns.md](../../../../docs/defensive-patterns.md) for the ambient-environment rule. ## Decision @@ -28,4 +28,4 @@ Three deliberate choices: ## Consequences -Hook bridges pass JSON payloads and hook-specific variables through the existing bash seam, retaining its process-group, truncation, and spill behavior. The model surface remains unchanged, and the bash tool remains the sole owner of model-call request construction. The vocabulary lives in [the bash data-structure reference](../../../core-data-structures/bash.md). +Hook bridges pass JSON payloads and hook-specific variables through the existing bash seam, retaining its process-group, truncation, and spill behavior. The model surface remains unchanged, and the bash tool remains the sole owner of model-call request construction. The vocabulary lives in [the bash data-structure reference](../../../../docs/core-data-structures/bash.md). diff --git a/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.md b/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md similarity index 80% rename from docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.md rename to .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md index b38777be38..56c3fdf231 100644 --- a/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.md +++ b/.agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md @@ -1,10 +1,10 @@ -# RFC: Event-domain semantics — session is the fact log, agent is the live surface +# Agent Note: Event-domain semantics — session is the fact log, agent is the live surface Status: implemented ## Problem -The harness extends the agent loop through a Cordis event taxonomy (see [the microkernel event-taxonomy RFC](2026-06-11-microkernel-event-taxonomy.md)). As that taxonomy grew, the line between the three event domains blurred: +The harness extends the agent loop through a Cordis event taxonomy (see [the microkernel event-taxonomy Agent Note](2026-06-11-microkernel-event-taxonomy.md)). As that taxonomy grew, the line between the three event domains blurred: - `session/*` carries the durable, event-sourced log (`SessionEventMap`). - `agent/*` carries live runtime signals that hand a plugin the `Agent` handle. @@ -24,14 +24,14 @@ This vocabulary is the foundation for interception decisions, the durable `hook/ **The boundary rule:** a durable, replayable fact is a `SessionEvent`; a live interception or a transient/live-object signal is an `agent`/`tools` Cordis event. A turn or step boundary is a durable fact, so it lives in the session log and is read off the `session/event` feed — it is NOT mirrored as an `agent/*` emit. -**Applying the rule to the boundary twins:** all four boundary mirrors — `agent/turn-start`, `agent/turn-end`, `agent/step-start`, `agent/step-end` — are **REMOVED**. No production consumer needs the live `Agent` at a boundary: the ACP bridge settles from `session/event` `turn/end` plus `agent/status`, and the only turn-mirror consumer (`dsh-ui-stdio`, a disposable test REPL) renders boundaries from `session/event` while retaining its live target object for the fixed `main` label. The step mirrors were removed first (they had no consumer at all); the turn mirrors followed once ui-stdio was migrated — see [the remove-boundary-mirror-events RFC](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md), which owns that decision. Removing the emits also simplifies the loop's `closeStep`/`closeTurn` (one append each, no paired emit). +**Applying the rule to the boundary twins:** all four boundary mirrors — `agent/turn-start`, `agent/turn-end`, `agent/step-start`, `agent/step-end` — are **REMOVED**. No production consumer needs the live `Agent` at a boundary: the ACP bridge settles from `session/event` `turn/end` plus `agent/status`, and the only turn-mirror consumer (`dsh-ui-stdio`, a disposable test REPL) renders boundaries from `session/event` while retaining its live target object for the fixed `main` label. The step mirrors were removed first (they had no consumer at all); the turn mirrors followed once ui-stdio was migrated — see [the remove-boundary-mirror-events Agent Note](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md), which owns that decision. Removing the emits also simplifies the loop's `closeStep`/`closeTurn` (one append each, no paired emit). ## Consequences - The loop no longer emits any boundary mirror; `closeStep` appends `step/end` only and `closeTurn` appends `turn/end` only. `Session.append` owns post-commit observer containment, so a throwing boundary observer cannot change the turn outcome or starve later consumers; an acceptance or internal validation failure still escapes before the boundary enters the log. - Tests that observed boundaries via the removed emits now observe the durable `turn/start`/`turn/end`/`step/start`/`step/end` session events — the behavior they pin (boundary ordering, step counting) is unchanged; only the feed they read moved to the canonical one. The tests that exercised a *throwing turn-boundary emit listener* were deleted, because that code path no longer exists (there is no emit to throw from). Per [AGENTS.md "tests document behavior, not golden truth"](../../../../AGENTS.md), the behavior and its test moved (or died) together. - The loop marks the step open (`stepOpen = true`) only after `append('step/start')` returns. Internal dispatch validation runs before the log push and may reject without opening a step; post-commit `session/event` observer failures are contained inside `Session.append`. The marker therefore represents exactly the committed boundary that owes a later `step/end`. -- The full realization of this is [the simplification RFC "Stop mirroring durable boundaries as agent events"](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md): all four boundary mirrors are removed and every consumer reads boundaries off `session/event`. `agent/steering` (not a boundary mirror) stayed outside that RFC's scope and was removed by its own follow-up, [Remove the `agent/steering` mirror emit](../simplification/2026-07-04-remove-agent-steering-mirror.md) — it mirrored the durable `steering/message`. +- The full realization of this is [the simplification Agent Note "Stop mirroring durable boundaries as agent events"](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md): all four boundary mirrors are removed and every consumer reads boundaries off `session/event`. `agent/steering` (not a boundary mirror) stayed outside that Agent Note's scope and was removed by its own follow-up, [Remove the `agent/steering` mirror emit](../simplification/2026-07-04-remove-agent-steering-mirror.md) — it mirrored the durable `steering/message`. - The cordis events catalog (`docs/cordis-catalog/events.md`) is regenerated to drop the mirror events. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.md b/.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md similarity index 92% rename from docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.md rename to .agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md index f60607df4d..ece39654ea 100644 --- a/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.md +++ b/.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md @@ -1,10 +1,10 @@ -# RFC: Resolve filesystem paths against the caller's session cwd +# Agent Note: Resolve filesystem paths against the caller's session cwd Status: implemented ## Problem -The ACP bridge gives every session its own workspace: `session/new` records the editor's project directory as `SessionHeader.cwd`, and `dsh-tool-bash` defaults each bash call's `workdir` to the calling agent's `session.header.cwd` (see [the per-session cwd RFC work in `packages/ui/acp`](../../../../packages/ui/acp) and `resolveWorkdir` in `dsh-tool-bash`). So a bash command in session A runs in A's project, and in session B runs in B's — one server process, N workspaces. +The ACP bridge gives every session its own workspace: `session/new` records the editor's project directory as `SessionHeader.cwd`, and `dsh-tool-bash` defaults each bash call's `workdir` to the calling agent's `session.header.cwd` (see [the per-session cwd Agent Note work in `packages/ui/acp`](../../../../packages/ui/acp) and `resolveWorkdir` in `dsh-tool-bash`). So a bash command in session A runs in A's project, and in session B runs in B's — one server process, N workspaces. Filesystem resolution used one plugin-load cwd while bash used the session project directory. Relative paths therefore disagreed whenever the editor project differed from the server launch directory; snapshots hid the bug by making those paths identical. diff --git a/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md b/.agents/notes/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md similarity index 97% rename from docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md rename to .agents/notes/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md index 218cb29297..8ddd1e8941 100644 --- a/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md +++ b/.agents/notes/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md @@ -1,4 +1,4 @@ -# RFC: Result-time applied-hunk diffs for file mutations +# Agent Note: Result-time applied-hunk diffs for file mutations Status: implemented @@ -54,6 +54,6 @@ Per the [capability-seam split](2026-06-13-capability-seams.md), the storage bac ## Related -- Completes the one remaining representation difference named as a non-goal in [Tagged render-intent union](2026-07-02-tool-render-intent-union.md) — that RFC's Non-goals section is updated to record that applied-hunk diffs shipped here. +- Completes the one remaining representation difference named as a non-goal in [Tagged render-intent union](2026-07-02-tool-render-intent-union.md) — that Agent Note's Non-goals section is updated to record that applied-hunk diffs shipped here. - Builds on the [filesystem capability seam](2026-06-17-filesystem-capability-seam.md) (the before/after are storage facts the backend returns) and [event-sourced sessions](2026-06-11-event-sourced-sessions.md) (the `meta` payload persists on the `tool/result` event, so replay reproduces the card). - The `meta` channel is deliberately generic: a future tool (a structured search, a data-table result) can attach its own durable result presentation without another core change. diff --git a/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md b/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md similarity index 92% rename from docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md rename to .agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md index 899d30416a..9d7fac0471 100644 --- a/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md +++ b/.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md @@ -1,4 +1,4 @@ -# RFC: Tagged render-intent union for tool-call presentation +# Agent Note: Tagged render-intent union for tool-call presentation Status: implemented @@ -10,7 +10,7 @@ A tool declares how its calls render in a UI (an editor's tool-call card) throug - Which combinations are *valid* is unwritten: a `terminal` call that also sets `content` means "description above the card"; a generic call that sets `terminal` is meaningless but representable. The type permits nonsense. - There is no way to express the one file-tool affordance an editor most wants — a **diff card** (`{path, oldText, newText}`, which Zed renders as an inline diff / new-file preview). `ToolCallPresentation.content` is the *LLM* `ContentBlock[]` vocabulary (text/image), so a tool literally cannot ask for a diff. -The existing `FIXME(tool-presentation)` in `packages/core/tools/src/index.ts` named the fix: "redesign the type so a tool declares its render INTENT once (e.g. a tagged union over card kinds) rather than a bag of optional fields the bridge stitches together." The rejected RFC [Collapse tool-owned UI presentation](../../rejected/simplification/2026-06-20-generic-tool-rendering.md) deferred it explicitly: rich rendering "should return later as a tagged render-intent union after there are at least two real tools and two real consumers to validate the vocabulary." That bar is now met — two producer families (`dsh-tool-bash`, `dsh-tool-fs`) and two consumers (the ACP bridge live path + the snapshot replay path). +The existing `FIXME(tool-presentation)` in `packages/core/tools/src/index.ts` named the fix: "redesign the type so a tool declares its render INTENT once (e.g. a tagged union over card kinds) rather than a bag of optional fields the bridge stitches together." The rejected Agent Note [Collapse tool-owned UI presentation](../../rejected/simplification/2026-06-20-generic-tool-rendering.md) deferred it explicitly: rich rendering "should return later as a tagged render-intent union after there are at least two real tools and two real consumers to validate the vocabulary." That bar is now met — two producer families (`dsh-tool-bash`, `dsh-tool-fs`) and two consumers (the ACP bridge live path + the snapshot replay path). ## Decision @@ -70,7 +70,7 @@ A new render intent is a compile-breaking change at the bridge switch — delibe ## Non-goals -- **Live incremental `terminal_output_delta` streaming** and **command classification** — the terminal-rendering RFC's own deferred follow-ups, untouched here. +- **Live incremental `terminal_output_delta` streaming** and **command classification** — the terminal-rendering Agent Note's own deferred follow-ups, untouched here. ## Related diff --git a/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md b/.agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md similarity index 98% rename from docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md rename to .agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md index 451cce45d3..d40c50695d 100644 --- a/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md +++ b/.agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md @@ -1,4 +1,4 @@ -# RFC: Add direct directory listing to the filesystem seam +# Agent Note: Add direct directory listing to the filesystem seam Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md similarity index 96% rename from docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md rename to .agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md index 375949a1e8..631e5b570c 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md +++ b/.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md @@ -1,4 +1,4 @@ -# RFC: Prompt variables and tool-guidance ownership +# Agent Note: Prompt variables and tool-guidance ownership Status: implemented @@ -38,16 +38,16 @@ Per-tool semantics and selection guidance live in tool descriptions. Prompt sect ### The subagent conversation-history descriptor -`SubagentProvider.inheritsParentContext` describes conversation seeding, not scope, services, tools, or authority. Spawn and ACP set it to `false`; fork sets it to `true`. `dsh-tool-subagent` derives its tool and prompt-parameter descriptions from the flag, including that fork inherits completed turns but not the in-flight turn. Provider lifecycle events keep that wording synchronized with reactive provider registration; their rationale lives in the [provider-lifecycle-events RFC](2026-07-05-subagent-provider-lifecycle-events.md). +`SubagentProvider.inheritsParentContext` describes conversation seeding, not scope, services, tools, or authority. Spawn and ACP set it to `false`; fork sets it to `true`. `dsh-tool-subagent` derives its tool and prompt-parameter descriptions from the flag, including that fork inherits completed turns but not the in-flight turn. Provider lifecycle events keep that wording synchronized with reactive provider registration; their rationale lives in the [provider-lifecycle-events Agent Note](2026-07-05-subagent-provider-lifecycle-events.md). ## Alternatives considered - **The loop composes an identity line itself** — hardcodes model-facing prose in the one package that must stay thin ("plugins, not loop changes"), and outside the section pipeline it would be a second composition path. (The identity DOES ship as a code literal — but as an ordinary section registered by `dsh-system-prompt`, whose `system-prompt/assemble` waterfall remains the escape valve for a deployment that must drop it.) - **Inject the model name via the `agent/request` waterfall** — prompt text would be composed in two places and the earlier rendered persona could disagree with the final routed header. The request plugin that owns late routing must also own any earlier prompt claim about that model. -- **Hand-write the model name in each persona** — duplicates the `model:` key one line above and silently lies after a config edit; the exact disease this RFC cures. +- **Hand-write the model name in each persona** — duplicates the `model:` key one line above and silently lies after a config edit; the exact disease this Agent Note cures. - **Lenient interpolation (leave unknown refs verbatim, or substitute empty)** — a typo ships `{{modle}}` (or a hole) to the model and nobody notices until transcript review. - **Per-instance subagent wording in config** — returns model-facing prose to every deployment × instance, the P2 disease again. **Keying wording off the provider NAME** — `providerName` is itself config, so a renamed provider silently gets the wrong words. -- **Resolving the provider at `apply` time (a load-order requirement)** and **section-only subagent wording (lazily resolved at assemble)** — the alternatives to the provider-lifecycle events; both rejected in [the provider-lifecycle-events RFC](2026-07-05-subagent-provider-lifecycle-events.md). +- **Resolving the provider at `apply` time (a load-order requirement)** and **section-only subagent wording (lazily resolved at assemble)** — the alternatives to the provider-lifecycle events; both rejected in [the provider-lifecycle-events Agent Note](2026-07-05-subagent-provider-lifecycle-events.md). ## Out of scope diff --git a/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md similarity index 98% rename from docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md rename to .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md index cbbfef6611..162a83ad0c 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md +++ b/.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md @@ -1,4 +1,4 @@ -# RFC: Every LLM request is reconstructable from the session log +# Agent Note: Every LLM request is reconstructable from the session log Status: implemented @@ -6,7 +6,7 @@ Status: implemented The request pipeline did not guarantee prefix stability for provider caching, and the session log could not reconstruct what the model saw. It omitted model, system prompt, and tool schemas while allowing per-call request rewrites. Cache behavior and replay equivalence therefore depended on whichever plugins happened to be loaded. -The reference shape for the happy path is MiniCode's `LLMClient`: a stateful conversation client, appended to — never rebuilt — as the conversation advances, resetting only when the system prompt, tool set, or compaction genuinely changes what the model must see. The design question this RFC answers is how to get that discipline without giving up event-sourcing. +The reference shape for the happy path is MiniCode's `LLMClient`: a stateful conversation client, appended to — never rebuilt — as the conversation advances, resetting only when the system prompt, tool set, or compaction genuinely changes what the model must see. The design question this Agent Note answers is how to get that discipline without giving up event-sourcing. ## Decision diff --git a/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md similarity index 77% rename from docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md rename to .agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md index a04040d540..44733bb8c9 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md +++ b/.agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md @@ -1,12 +1,12 @@ -# RFC: Subagent provider-lifecycle events — `subagent/provider-added` / `subagent/provider-removed` +# Agent Note: Subagent provider-lifecycle events — `subagent/provider-added` / `subagent/provider-removed` Status: implemented ## Problem -[The prompt-variables RFC](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) makes `dsh-tool-subagent` DERIVE its model-facing wording from its provider: `SubagentProvider.inheritsParentContext` (spawn/ACP `false`, fork `true`) drives both the tool description and the `prompt` parameter description, so the fork tool stops lying about context inheritance. That fix created a cross-fiber data dependency: a tool's description is fixed at TOOL REGISTRATION (deliberately — the description is where tool-choice guidance lives), but the provider arrives on its own plugin fiber, on no particular schedule. +[The prompt-variables Agent Note](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) makes `dsh-tool-subagent` DERIVE its model-facing wording from its provider: `SubagentProvider.inheritsParentContext` (spawn/ACP `false`, fork `true`) drives both the tool description and the `prompt` parameter description, so the fork tool stops lying about context inheritance. That fix created a cross-fiber data dependency: a tool's description is fixed at TOOL REGISTRATION (deliberately — the description is where tool-choice guidance lives), but the provider arrives on its own plugin fiber, on no particular schedule. -Resolving the provider at the tool plugin's `apply` time creates an implicit load-order requirement ("list the backend before the tool in cordis.yml"). That requirement fails because the Cordis Loader starts sibling entries concurrently and `Entry.init()` does not await activation: a delayed backend can leave the tool fiber failed even when listed first. The Loader offers no sibling-order guarantee — "async state is not synchronous state" ([defensive patterns](../../../defensive-patterns.md)). +Resolving the provider at the tool plugin's `apply` time creates an implicit load-order requirement ("list the backend before the tool in cordis.yml"). That requirement fails because the Cordis Loader starts sibling entries concurrently and `Entry.init()` does not await activation: a delayed backend can leave the tool fiber failed even when listed first. The Loader offers no sibling-order guarantee — "async state is not synchronous state" ([defensive patterns](../../../../docs/defensive-patterns.md)). ## Decision @@ -23,12 +23,12 @@ The events also complete the seam's vocabulary: `ctx.subagents` is a named regis - **Resolve the provider at `apply` time and throw when absent** — rejected because "list backends first" would claim a Loader ordering guarantee that does not exist. - **Retrying the lookup (poll until the provider appears)** — converges eventually but invents a private readiness protocol beside the one the framework already has (effect registration + disposal); it also cannot notice a provider LEAVING, so HMR would strand a tool whose wording describes a disposed backend. -- **Section-only subagent wording, lazily resolved at assemble time** — tolerates any load order too, but moves tool-choice guidance out of the DESCRIPTION, contradicting the ownership rule the prompt-variables RFC establishes (per-tool semantics and when-to-use belong in the description). Reactive registration keeps the description authoritative AND order-free. +- **Section-only subagent wording, lazily resolved at assemble time** — tolerates any load order too, but moves tool-choice guidance out of the DESCRIPTION, contradicting the ownership rule the prompt-variables Agent Note establishes (per-tool semantics and when-to-use belong in the description). Reactive registration keeps the description authoritative AND order-free. - **Keying wording off the provider NAME instead of the provider object** — `providerName` is itself config, so a renamed provider silently gets the wrong words; deriving from the resolved provider's own `inheritsParentContext` cannot drift. ## Consequences - Consumers deriving state from a named provider react to `subagent/provider-added`/`-removed` instead of reading the registry at `apply` time; `dsh-tool-subagent` is the reference implementation. -- **Addition fails loud; removal is contained per listener.** An addition listener may unwind registration. Removal runs during disposal, so one throwing listener is logged without starving later mirrors or disrupting teardown. `start()` still resolves the provider by name for every run, preventing stale tools from calling a removed backend. See the [events catalog](../../../cordis-catalog/events.md) and [producer/consumer map](../../../event-producer-consumer.md). +- **Addition fails loud; removal is contained per listener.** An addition listener may unwind registration. Removal runs during disposal, so one throwing listener is logged without starving later mirrors or disrupting teardown. `start()` still resolves the provider by name for every run, preventing stale tools from calling a removed backend. See the [events catalog](../../../../docs/cordis-catalog/events.md) and [producer/consumer map](../../../../docs/event-producer-consumer.md). - **A window where the tool is absent.** Between backend disposal and re-registration (an HMR reload), the model sees no subagent tool. This is the honest state — the alternative is a tool that dispatches into nothing — and the tool registry's `tools/change` emit keeps prompt assembly current. - **Two waiting fibers sharing a `toolName` is an invalid config caught late.** If two loads of `dsh-tool-subagent` name different providers but the same `toolName`, both wait, and whichever provider arrives first registers; the second registration throws only when ITS provider arrives. `TODO(subagent-dup-toolname)` in the plugin records this blast radius; the tool registry's duplicate-name rejection remains the backstop. diff --git a/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md similarity index 99% rename from docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md rename to .agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md index 7aa987c60f..335581ecff 100644 --- a/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md +++ b/.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md @@ -1,4 +1,4 @@ -# RFC: A shared timeout/deadline primitive, with hard-kill left to each capability +# Agent Note: A shared timeout/deadline primitive, with hard-kill left to each capability Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-07-06-tool-result-retention-library.md b/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md similarity index 99% rename from docs/rfc/implemented/architecture/2026-07-06-tool-result-retention-library.md rename to .agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md index f35cf56e84..f90e653a7c 100644 --- a/docs/rfc/implemented/architecture/2026-07-06-tool-result-retention-library.md +++ b/.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md @@ -1,4 +1,4 @@ -# RFC: Tool result retention library +# Agent Note: Tool result retention library Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md similarity index 83% rename from docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md rename to .agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md index 1e516a9aa0..040a6c2fdc 100644 --- a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md +++ b/.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md @@ -1,10 +1,10 @@ -# RFC: Tool-call timeout policy as a plugin +# Agent Note: Tool-call timeout policy as a plugin Status: implemented ## Problem -The [timeout/deadline RFC](2026-07-06-timeout-deadline-library.md) extracted the timing-and-classification primitive into `@deepseek-ai/dsh-timeout`, but timeout policy was still attached to individual capabilities and model-facing schemas. `bash` exposed `timeoutMs`; `web_fetch` exposed `timeout_ms`; `web_search` had no model-facing timeout even though providers already honor `exec.signal`; a future grep/glob tool would either import the timeout library directly or invent its own timeout policy. That is the wrong authoring shape for a plugin SDK: a tool author should normally forward `exec.signal` to the implementation it calls, and deployment policy should decide the budget. +The [timeout/deadline Agent Note](2026-07-06-timeout-deadline-library.md) extracted the timing-and-classification primitive into `@deepseek-ai/dsh-timeout`, but timeout policy was still attached to individual capabilities and model-facing schemas. `bash` exposed `timeoutMs`; `web_fetch` exposed `timeout_ms`; `web_search` had no model-facing timeout even though providers already honor `exec.signal`; a future grep/glob tool would either import the timeout library directly or invent its own timeout policy. That is the wrong authoring shape for a plugin SDK: a tool author should normally forward `exec.signal` to the implementation it calls, and deployment policy should decide the budget. At the same time, not every timeout in the repo is a model-facing tool-call budget. Hooks execute command hooks by calling `ctx.bash` directly, not through `ctx.tools.execute()`, and the `bash` model tool multiplexes foreground execution, background start, background polling, and hook reuse through the same backend. Moving every timeout into a tool plugin in one step would conflate those paths and risk breaking hook timeout semantics. @@ -84,7 +84,7 @@ A future model-facing grep/glob tool can be implemented on top of `ctx.bash` wit ## Alternatives considered -**Name the plugin `tool-timeout`.** The literal RFC name matched the `gen-tool-catalog` completeness guard's `packages/*/tool-*` glob, which requires every match to register a model-facing tool. This plugin registers none — it is a `tools/execute` wrapper — so a `tool-*` name would either fail `verify-tool-catalog` or force a misleading boot entry. The package is `@deepseek-ai/dsh-timeout-policy` in a new `packages/timeout/` group; the cordis.yml `id` can still be `timeout-policy`. +**Name the plugin `tool-timeout`.** The literal Agent Note name matched the `gen-tool-catalog` completeness guard's `packages/*/tool-*` glob, which requires every match to register a model-facing tool. This plugin registers none — it is a `tools/execute` wrapper — so a `tool-*` name would either fail `verify-tool-catalog` or force a misleading boot entry. The package is `@deepseek-ai/dsh-timeout-policy` in a new `packages/timeout/` group; the cordis.yml `id` can still be `timeout-policy`. **Keep per-tool timeout handling only.** This was the shape for `bash` and `web_fetch`, and it matches Claude Code and Codex for shell commands. It loses for web-style tools because every new timeout-capable tool must choose validation, cap semantics, docs, snapshots, and classification. The plugin centralizes policy and classification while leaving each tool's schema focused on business input. @@ -98,7 +98,7 @@ A future model-facing grep/glob tool can be implemented on top of `ctx.bash` wit **Use `tools/pre-execute` plus `tools/post-execute` instead of a new around seam.** A pre listener could arm a deadline and mutate `exec.signal`; a post listener could classify and replace. That loses because the deadline lifetime would cross two independent waterfalls: a call-id map, cleanup on every pre-deny/tool-throw/post-throw/dispose path, and ordering rules with every other listener. `tools/pre-execute` is also the allow/deny gate, not an execution wrapper. `tools/execute` gives the timeout one lexical scope: arm, delegate, classify, dispose. -**Use `Promise.race` to enforce timeouts for non-cooperative tools.** Rejected for the same reason as the timeout-library RFC: it returns control to the caller while the underlying process, fetch, or provider operation may still be running. The plugin only sends a signal; termination remains the implementation's responsibility. +**Use `Promise.race` to enforce timeouts for non-cooperative tools.** Rejected for the same reason as the timeout-library Agent Note: it returns control to the caller while the underlying process, fetch, or provider operation may still be running. The plugin only sends a signal; termination remains the implementation's responsibility. ## Consequences @@ -106,4 +106,4 @@ A future model-facing grep/glob tool can be implemented on top of `ctx.bash` wit - Multiple `tools/execute` listeners compose by ordinary Cordis waterfall order: a listener that calls `next()` wraps downstream listeners plus dispatch; one that returns without `next()` short-circuits them. A deployment combining timeout with a future retry/sandbox/metrics wrapper chooses semantics by registration order ("timeout covers the whole retry" vs "timeout covers each attempt"). - Opt-in by declaration is a deliberate misconfiguration risk: a tool can declare a `timeoutMs` without honoring `exec.signal`, and that tool will not stop on timeout. The plugin contract states that declaring a budget means cooperative; the web tools prove the pattern on tools that already forward the signal. - During the transition `bash` and the migrated web tools use different timeout paths on purpose: `TOOL_TIMEOUT` is the model-facing tool-call budget, while `BASH_TIMEOUT` remains the bash backend timeout used by bash and hooks. -- Deviation from the literal proposal, recorded per the implemented-RFC rule: the plugin package is `@deepseek-ai/dsh-timeout-policy` (not `tool-timeout`), signal replacement is in-place `exec.signal` mutation before `next()` (not `next({ ...exec, signal })`, which cordis ignores), and the per-tool budget is declared on the `ToolDefinition` (`timeoutMs`, set by the owning tool plugin from its config) rather than mapped by tool name in this plugin's config — so the enforcer is zero-config and a mistyped tool name is impossible. All three are described in `## Decision` above. +- Deviation from the literal proposal, recorded per the implemented-Agent Note rule: the plugin package is `@deepseek-ai/dsh-timeout-policy` (not `tool-timeout`), signal replacement is in-place `exec.signal` mutation before `next()` (not `next({ ...exec, signal })`, which cordis ignores), and the per-tool budget is declared on the `ToolDefinition` (`timeoutMs`, set by the owning tool plugin from its config) rather than mapped by tool name in this plugin's config — so the enforcer is zero-config and a mistyped tool name is impossible. All three are described in `## Decision` above. diff --git a/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md similarity index 95% rename from docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md rename to .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md index 15477e023a..68c9bd3b3e 100644 --- a/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md +++ b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md @@ -1,4 +1,4 @@ -# RFC: The agent is a registration scope +# Agent Note: The agent is a registration scope Status: implemented @@ -14,7 +14,7 @@ The mechanism also needs a publication boundary. An agent must not become visibl Every live agent owns one flat registration layer exposed as `agent.ctx`. Code registers through the context that owns a contribution; scope-aware services combine deployment-global registrations with exactly one matching agent layer; operations choose that layer from their real agent; and the layer exists for the agent's complete published lifetime. -Cordis is the plugin framework underneath the SDK. A Cordis **context** is the object plugins use to access services and register effects whose cleanup follows that context. The [Cordis primer](../../../cordis-primer.md) explains the framework in more detail. +Cordis is the plugin framework underneath the SDK. A Cordis **context** is the object plugins use to access services and register effects whose cleanup follows that context. The [Cordis primer](../../../../docs/cordis-primer.md) explains the framework in more detail. For most contributors, the complete contract is four rules: @@ -43,7 +43,7 @@ flowchart LR The missing cross-edges are the isolation rule: Agent A's local registrations do not enter Agent B's view, and a parent's registrations do not enter a child merely because the parent owns the child's lifetime. -The companion [runtime-design RFC](2026-07-12-agent-scope-runtime-design.md) explains the implementation and correctness reasoning. The [subagent composition-controls RFC](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns the separate `persona`, `toolFilter`, and `maxDepth` feature. +The companion [runtime-design Agent Note](2026-07-12-agent-scope-runtime-design.md) explains the implementation and correctness reasoning. The [subagent composition-controls Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns the separate `persona`, `toolFilter`, and `maxDepth` feature. ### Registration origin chooses visibility and cleanup @@ -102,7 +102,7 @@ An event about Agent A normally reaches unscoped listeners and A-scoped listener At the Cordis level, `Scoped<T>` is an opaque routing receiver. It carries the filter used to choose listeners but is not the domain object. Event signatures therefore keep the real `Agent`, tool execution, approval request, or other subject as an explicit argument that listeners can inspect. -A listener registered with `{ global: true }` deliberately bypasses contextual audience filtering while its cleanup still follows the registering context. Registry-membership notifications remain unfiltered because they describe shared registry state rather than one agent's operation. The generated [event catalog](../../../cordis-catalog/events.md) is the exhaustive event reference. +A listener registered with `{ global: true }` deliberately bypasses contextual audience filtering while its cleanup still follows the registering context. Registry-membership notifications remain unfiltered because they describe shared registry state rather than one agent's operation. The generated [event catalog](../../../../docs/cordis-catalog/events.md) is the exhaustive event reference. ### Creation publishes last and disposal revokes last diff --git a/docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md similarity index 98% rename from docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md rename to .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md index bbca065d87..1255dc7328 100644 --- a/docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md +++ b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md @@ -1,4 +1,4 @@ -# RFC: Tool output spill policy +# Agent Note: Tool output spill policy Status: implemented @@ -6,7 +6,7 @@ Status: implemented Tool outputs need bounded model-facing previews, but some oversized results are still useful later. A fetched page body or a verbose tool response should not consume the next model request in full, but the model should be able to inspect the complete formatted result later with existing file-reading tools. -Before this change the behavior was uneven. `dsh-bash-local` already writes complete stdout/stderr streams to private temp spill files when its in-memory tail overflows, but ordinary text tool results were returned inline unless the tool hand-rolled its own cap. The [tool result retention library](./2026-07-06-tool-result-retention-library.md) owns preview mechanics, but it does not own storage or an execution-pipeline policy that applies those mechanics to final tool results. +Before this change the behavior was uneven. `dsh-bash-local` already writes complete stdout/stderr streams to private temp spill files when its in-memory tail overflows, but ordinary text tool results were returned inline unless the tool hand-rolled its own cap. The [tool result retention library](2026-07-06-tool-result-retention-library.md) owns preview mechanics, but it does not own storage or an execution-pipeline policy that applies those mechanics to final tool results. The shape matches the timeout policy design: a tool author normally returns the text result, and a policy plugin enforces the deployment's default context budget. Tool-specific early spill remains possible later for outputs that do not survive to the final `ToolExecutionResult`; the first cut proves the default final-result path. diff --git a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml similarity index 81% rename from docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml rename to .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml index c54a0b344d..68b9d2d55b 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.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-10-after-call-compaction-pressure-and-overflow-recovery.md: d88d7aaea8ccec30b10bfeb17f1312cfe87a0ce7 -2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: 42e6114304de9c8022ef8f1341035858c0c7d9ec +2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: d470cceaff68229b3872d0ade93d5fabc2e10c3f +2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: b7753fb226638b16b0f244b681cd2b9bcc9f25c2 diff --git a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md similarity index 63% rename from docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md rename to .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md index d88d7aaea8..d470cceaff 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md +++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md @@ -1,4 +1,4 @@ -# RFC: After-call compaction pressure and context-overflow recovery +# Agent Note: After-call compaction pressure and context-overflow recovery Status: implemented @@ -16,9 +16,9 @@ Successful calls are not the only pressure signal. A provider can reject a reque `agent/pre-step` is narrowed to `(agent, turn, step, signal)`. It remains a generic serial checkpoint before `step/start`, but it carries no compaction-only prompt or prefix fields. -The loop fires awaited serial `agent/post-step(agent, turn, step, signal)` after assistant output, every dispatched or synthetic tool result, post-tool context, and steering are durable, but before `step/end`. This placement gives pressure policy the complete successful-call state without splitting an assistant tool call from its result. A listener failure is an ordinary turn failure; it never enters model-request recovery. +The loop fires awaited serial `agent/post-step(agent, turn, step, signal)` after assistant output, every dispatched or synthetic tool result, post-tool context, and steering are durable, but before `step/end`. This placement gives pressure policy the complete successful-call state without splitting an assistant tool call from its result. A propagated listener failure is an ordinary turn failure; it never enters model-request recovery. Compact-basic contains its expected operational failures as described below. -`dsh-compact-basic` reads the exact latest routed model from the durable request header only to establish that a completed route exists, then asks the singleton `ctx.tokenMeter` to measure the canonical logged envelope and current surface. It does not fall back to `AgentOptions.model` for automatic pressure. A headerless session has no completed routed request to assess and produces no work; any durable non-empty model name uses the same estimator. Operational measurement or summarization failures warn and continue with full history. +`dsh-compact-basic` reads the exact latest routed model from the durable request header only to establish that a completed route exists, then asks the singleton `ctx.tokenMeter` to measure the canonical logged envelope and current surface. It does not fall back to `AgentOptions.model` for automatic pressure. A headerless session has no completed routed request to assess and produces no work; any durable non-empty model name uses the same estimator. Operational measurement or summarization failures warn and continue from the latest durable surface: full history before any replacement, or the pruned surface if pruning already landed. ### Request recovery is limited to the final model boundary @@ -32,17 +32,17 @@ If cancellation lands after assistant tool calls are durable but before all call `CompactService.compactIfNeeded(agent, trigger, signal)` accepts `trigger: 'pressure' | 'context-overflow'`. The interface gains no estimation methods or token types; `ctx.tokenMeter` remains the reusable accounting owner. -For `pressure`, compact-basic applies the service-wide threshold and retained-tail policy to one unified `ctx.tokenMeter.measure()` result. The same singleton meter owns range pricing, provenance, shadowed token counts, and non-shrinking-summary rejection. The common defaults remain threshold ratio `0.8`, retained history `floor(contextWindow × 0.16)`, summarization provider/model `''`, `maxTokens: 8192`, `compactionRetries: 1`, and `auto: true`. +For `pressure`, compact-basic applies the service-wide threshold and retained-tail policy to one unified `ctx.tokenMeter.measure()` result. Below pressure it returns without pruning. Once pressure qualifies, optional `ctx.toolResultPrune` rewrites oversized current results and compact-basic remeasures through the same meter; safe pressure skips the model call, while remaining pressure selects and summarizes from the pruned surface. The same singleton meter owns range pricing, provenance, shadowed token counts, and non-shrinking-summary rejection. The common defaults remain threshold ratio `0.8`, retained history `floor(contextWindow × 0.16)`, summarization provider/model `''`, `maxTokens: 8192`, `compactionRetries: 1`, and `auto: true`. -For canonical overflow, compact-basic bypasses scalar pressure and the normal retained-token budget. It chooses the maximal tool-balanced head range while leaving the newest indivisible unit, then attempts exactly one shrinking compaction under the same signal. The automatic listener snapshots `session.surface.replaceGeneration` and returns `{ action: 'retry' }` only when compaction succeeds and the generation increases. A backend returning a result without replacement cannot authorize retry. +For canonical overflow, compact-basic bypasses scalar pressure and the normal retained-token budget. It prunes first, then chooses the maximal tool-balanced head range while leaving the newest indivisible unit and attempts one shrinking summary compaction under the same signal when a range exists. The automatic listener snapshots `session.surface.replaceGeneration` and returns `{ action: 'retry' }` whenever pruning or summarization increases it. This remains true when pruning lands before later summary work throws; cancellation still wins. A backend returning a result without replacement cannot authorize retry, while pruning-only progress can authorize a retry without a `CompactionResult`. -`maxOverflowRetries` is optional and defaults to `1`; `0` disables overflow recovery without disabling pressure. `auto: false` registers neither automatic listener. Noncanonical errors, exhausted attempts, an already-aborted signal, a missing routed model, no safe range, no generation change, and recovery throws all delegate to the next listener. With no later recovery, the loop reports the original provider error object and code. Cancellation or disposal remains authoritative even if recovery work completes concurrently. +`maxOverflowRetries` is optional and defaults to `1`; `0` disables overflow recovery without disabling pressure. `auto: false` registers neither automatic listener. Noncanonical errors, exhausted attempts, an already-aborted signal, a missing routed model, no safe range, no generation change, and recovery throws before any replacement all delegate to the next listener. With no later recovery, the loop reports the original provider error object and code. A recovery throw after generation advances authorizes retry from durable progress; cancellation or disposal remains authoritative even if recovery work completes concurrently. The default summarizer resolves explicit configuration, then the latest logged route, then agent options. Because direct `llm/stream` middleware may reroute that auxiliary call, `compact/summary.{provider, model}` records the final mutable `GenerateOptions` target observed after dispatch rather than the pre-waterfall candidate. ## Testing -Unit tests cover final-adapter failure provenance and identity, closed-step retry numbering and reset, cancellation and disposal, post-step ordering, routed-envelope pressure, balanced overflow reduction, generation proof, caps, delegation, and auxiliary-call routing. Real-loop tests cover thrown and in-band overflow through compaction to a reconstructed retry request. +Unit tests cover final-adapter failure provenance and identity, closed-step retry numbering and reset, cancellation and disposal, post-step ordering, routed-envelope pressure, pressure-gated pruning, pruning-only relief, pruned-input summarization, balanced overflow reduction, durable prune progress before later failure, generation proof, caps, delegation, and auxiliary-call routing. Real-loop tests cover thrown and in-band overflow through pruning or summary compaction to a reconstructed retry request. ## Alternatives considered @@ -54,8 +54,8 @@ Unit tests cover final-adapter failure provenance and identity, closed-step retr ## Consequences -Post-step pressure describes the completed routed request, including durable tool results and request-only prefix fields. Canonical overflow supplies the backstop when no successful usage anchor exists. Recovery is bounded, cancellation-owned, and monotonic: it retries only after a visible surface generation change. +Post-step pressure describes the completed routed request, including durable tool results and request-only prefix fields. Optional model-free pruning removes predictable tool-output bulk before summary selection and can independently create retry-worthy progress. Canonical overflow supplies the backstop when no successful usage anchor exists. Recovery is bounded, cancellation-owned, and monotonic: it retries only after a visible surface generation change. -The cost is one additional serial checkpoint on successful steps and adapter-maintained overflow classification. Provider wording and heuristic character density remain maintenance risks. Surface compaction still cannot repair an envelope that alone exceeds the window or split one indivisible oversized message/tool unit. +The cost is one additional serial checkpoint on successful steps and adapter-maintained overflow classification. Provider wording and heuristic character density remain maintenance risks. Surface compaction still cannot repair an envelope that alone exceeds the window, split an indivisible non-tool node, or repair a tool unit whose non-prunable remainder remains oversized. The optional pruner can repair an otherwise indivisible tool pair when removable text-bearing tool-result content is the bulk. -This RFC supersedes only the pre-step automatic-trigger portion of the [compaction capability-seam RFC](../feature/2026-06-18-compaction-capability-seam.md). The service split, standalone token meter, balanced range contract, log-recorded lock, summary replacement, and sole `summarize()` subclass hook remain unchanged. +This Agent Note supersedes only the pre-step automatic-trigger portion of the [compaction capability-seam Agent Note](../feature/2026-06-18-compaction-capability-seam.md). The service split, standalone token meter, balanced range contract, log-recorded lock, summary replacement, and sole `summarize()` subclass hook remain unchanged. diff --git a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md similarity index 63% rename from docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md rename to .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md index 42e6114304..b7753fb226 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md @@ -1,4 +1,4 @@ -# RFC:调用后压缩压力与上下文溢出恢复 +# Agent Note:调用后压缩压力与上下文溢出恢复 Status: implemented @@ -16,9 +16,9 @@ Status: implemented `agent/pre-step` 收窄为 `(agent, turn, step, signal)`。它仍是 `step/start` 之前的通用串行检查点,但不再携带压缩专用的提示词或前缀字段。 -循环在 assistant 输出、所有已分发或合成的工具结果、工具后上下文与 steering 都持久化之后、`step/end` 之前,触发等待式串行 `agent/post-step(agent, turn, step, signal)`。该位置让压力策略看到完整的成功调用状态,同时不会拆开 assistant 工具调用与其结果。监听器失败属于普通 turn 失败,绝不会进入模型请求恢复。 +循环在 assistant 输出、所有已分发或合成的工具结果、工具后上下文与 steering 都持久化之后、`step/end` 之前,触发等待式串行 `agent/post-step(agent, turn, step, signal)`。该位置让压力策略看到完整的成功调用状态,同时不会拆开 assistant 工具调用与其结果。向外传播的监听器失败属于普通 turn 失败,绝不会进入模型请求恢复;compact-basic 会按下文所述在内部处理其预期的操作性失败。 -`dsh-compact-basic` 从持久请求头读取精确的最新实际路由模型,只用它确认已经存在完整路由,随后让单例 `ctx.tokenMeter` 计量规范日志信封与当前表层。自动压力不会回退到 `AgentOptions.model`。没有请求头的会话尚无已完成路由请求可供判断,因此不执行工作;任意持久记录的非空模型名都使用同一个估算器。操作性的计量或摘要失败会发出警告,并继续使用完整历史。 +`dsh-compact-basic` 从持久请求头读取精确的最新实际路由模型,只用它确认已经存在完整路由,随后让单例 `ctx.tokenMeter` 计量规范日志信封与当前表层。自动压力不会回退到 `AgentOptions.model`。没有请求头的会话尚无已完成路由请求可供判断,因此不执行工作;任意持久记录的非空模型名都使用同一个估算器。操作性的计量或摘要失败会发出警告,并从最新持久表层继续:任何替换发生前使用完整历史;若剪枝已经落盘,则使用已剪枝表层。 ### 请求恢复只覆盖最终模型边界 @@ -32,17 +32,17 @@ Status: implemented `CompactService.compactIfNeeded(agent, trigger, signal)` 接收 `trigger: 'pressure' | 'context-overflow'`。接口不增加估算方法或 token 类型;`ctx.tokenMeter` 继续作为可复用的核算所有者。 -对于 `pressure`,compact-basic 把服务级阈值与保留尾部策略应用到一次统一的 `ctx.tokenMeter.measure()` 结果。范围定价、来源、被遮蔽 token 数与非缩小摘要拒绝也由同一个单例 meter 完成。通用默认值保持为阈值比例 `0.8`、保留历史 `floor(contextWindow × 0.16)`、摘要提供方/模型 `''`、`maxTokens: 8192`、`compactionRetries: 1` 与 `auto: true`。 +对于 `pressure`,compact-basic 把服务级阈值与保留尾部策略应用到一次统一的 `ctx.tokenMeter.measure()` 结果。低于压力时直接返回,不执行剪枝。压力达到条件后,可选的 `ctx.toolResultPrune` 会改写当前表层中过大的工具结果,compact-basic 再通过同一个 meter 重新计量;若压力恢复安全则跳过模型调用,否则从已剪枝表层选择范围并生成摘要。范围定价、来源、被遮蔽 token 数与非缩小摘要拒绝也由同一个单例 meter 完成。通用默认值保持为阈值比例 `0.8`、保留历史 `floor(contextWindow × 0.16)`、摘要提供方/模型 `''`、`maxTokens: 8192`、`compactionRetries: 1` 与 `auto: true`。 -对于规范化溢出,compact-basic 绕过标量压力与普通保留 token 预算。它在保留最新不可分割单元的同时,选择最大的工具配对平衡头部范围,并在同一 signal 下只尝试一次缩小压缩。自动监听器先记录 `session.surface.replaceGeneration`,只有压缩成功且 generation 增加时才返回 `{ action: 'retry' }`。后端若只返回结果但没有替换表层,不能授权重试。 +对于规范化溢出,compact-basic 绕过标量压力与普通保留 token 预算。它先执行剪枝,再在保留最新不可分割单元的同时选择最大的工具配对平衡头部范围;存在范围时,才在同一 signal 下尝试一次缩小摘要压缩。自动监听器先记录 `session.surface.replaceGeneration`,剪枝或摘要让 generation 增加时就返回 `{ action: 'retry' }`。即使剪枝先落盘而后续摘要工作抛错,这条规则仍然成立;取消依然优先。后端若只返回结果但没有替换表层,不能授权重试;只有剪枝取得进展时,即使没有 `CompactionResult` 也可以授权重试。 -`maxOverflowRetries` 可选且默认为 `1`;`0` 只禁用溢出恢复,不会禁用压力检查。`auto: false` 不注册任何自动监听器。非规范化错误、尝试耗尽、已经中止的 signal、缺失路由模型、没有安全范围、generation 未变化,以及恢复抛错都会委托给下一个监听器。若没有后续恢复,循环报告原始提供方错误对象与代码。即使恢复工作并发完成,取消或销毁仍具有最终优先级。 +`maxOverflowRetries` 可选且默认为 `1`;`0` 只禁用溢出恢复,不会禁用压力检查。`auto: false` 不注册任何自动监听器。非规范化错误、尝试耗尽、已经中止的 signal、缺失路由模型、没有安全范围、generation 未变化,以及在任何替换之前恢复抛错,都会委托给下一个监听器。若没有后续恢复,循环报告原始提供方错误对象与代码。generation 增加后的恢复抛错会基于持久进展授权重试;即使恢复工作并发完成,取消或销毁仍具有最终优先级。 默认摘要器依次解析显式配置、最近记录的路由与 agent options。因为直接 `llm/stream` 中间件可以重新路由该辅助调用,`compact/summary.{provider, model}` 记录分发后最终可变的 `GenerateOptions` 目标,而不是 waterfall 之前的候选值。 ## 测试 -单元测试覆盖最终适配器失败的来源与身份、已关闭 step 的重试编号与重置、取消与销毁、post-step 顺序、已路由信封压力、平衡溢出缩减、generation 证明、上限、委托与辅助调用路由。真实循环测试覆盖抛出式和带内溢出,并验证压缩后的重试请求从替换表层重建。 +单元测试覆盖最终适配器失败的来源与身份、已关闭 step 的重试编号与重置、取消与销毁、post-step 顺序、已路由信封压力、压力门控剪枝、剪枝独立解除压力、从已剪枝输入生成摘要、平衡溢出缩减、后续失败前已落盘的剪枝进展、generation 证明、上限、委托与辅助调用路由。真实循环测试覆盖抛出式和带内溢出,并验证剪枝或摘要压缩后的重试请求从替换表层重建。 ## 考虑过的替代方案 @@ -54,8 +54,8 @@ Status: implemented ## 后果 -Post-step 压力描述已完成的路由请求,包括持久工具结果与仅请求前缀字段。当成功 usage 锚点不存在时,规范化溢出提供兜底路径。恢复有明确上限、以取消为准,并保持单调:只有模型可见的表层 generation 变化后才重试。 +Post-step 压力描述已完成的路由请求,包括持久工具结果与仅请求前缀字段。可选的无模型剪枝会在选择摘要前移除可预测的工具输出体积,也能独立产生足以重试的进展。当成功 usage 锚点不存在时,规范化溢出提供兜底路径。恢复有明确上限、以取消为准,并保持单调:只有模型可见的表层 generation 变化后才重试。 -代价是成功 step 增加一个串行检查点,并需要适配器持续维护溢出分类。提供方措辞与启发式字符密度仍是维护风险。表层压缩依然无法修复仅信封本身就超出窗口的情况,也不能拆分单个不可分割的超大消息或工具单元。 +代价是成功 step 增加一个串行检查点,并需要适配器持续维护溢出分类。提供方措辞与启发式字符密度仍是维护风险。表层压缩依然无法修复仅信封本身就超出窗口的情况,也不能拆分不可分割的非工具节点,或修复非可剪枝剩余部分仍然过大的工具单元。若可移除的文本工具结果是主要体积,可选剪枝器仍可修复原本不可分割的工具配对。 -本 RFC 只取代[压缩能力接缝 RFC](../feature/2026-06-18-compaction-capability-seam.md) 中的 pre-step 自动触发部分。服务拆分、独立 token meter、平衡范围契约、日志记录锁、摘要替换与唯一 `summarize()` 子类 hook 均保持不变。 +本 Agent Note 只取代[压缩能力接缝 Agent Note](../feature/2026-06-18-compaction-capability-seam.md) 中的 pre-step 自动触发部分。服务拆分、独立 token meter、平衡范围契约、日志记录锁、摘要替换与唯一 `summarize()` 子类 hook 均保持不变。 diff --git a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml similarity index 70% rename from docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml rename to .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index 98131dce38..6db1ef4ece 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.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-10-single-file-executable-sdk-runtime-distribution.md: 80d5ab64682f1b72ca1dfa1f96bc34f2a3db5f2d -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: aea1fb66136b31e0a75ba51471fec4dadd960e8f +2026-07-10-single-file-executable-sdk-runtime-distribution.md: 0d4686a5a233785ca4832ef068a118b484a872fe +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: dcc9213c6b3a088b8b8bce2a442c5232ed5b7d0b diff --git a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md similarity index 99% rename from docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md rename to .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index 80d5ab6468..0d4686a5a2 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -1,4 +1,4 @@ -# RFC: Single-file executable SDK runtime distribution (single-exe) +# Agent Note: Single-file executable SDK runtime distribution (single-exe) Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md similarity index 99% rename from docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md rename to .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index aea1fb6613..dcc9213c6b 100644 --- a/docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -1,4 +1,4 @@ -# RFC: 单文件可执行的 SDK 运行时分发(single-exe) +# Agent Note: 单文件可执行的 SDK 运行时分发(single-exe) Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md similarity index 96% rename from docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md rename to .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md index 2c3126793f..2ddbe0a351 100644 --- a/docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md +++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md @@ -1,4 +1,4 @@ -# RFC: Agent-scope runtime design and correctness +# Agent Note: Agent-scope runtime design and correctness Status: implemented @@ -26,9 +26,9 @@ The design can be skimmed as seven choices: | Compose the model-visible prompt and tool surface | One shared tool view plus the authoritative assembly-waterfall result | | Coordinate subagent, worker, and process shutdown | One cancellation signal plus the independent terminal/quiescence facts of that boundary | -The rest of this RFC expands those choices in dependency order: Cordis mechanics, scope routing, creation and session commit, tools and prompts, subagents and workflows, then executable checks. +The rest of this Agent Note expands those choices in dependency order: Cordis mechanics, scope routing, creation and session commit, tools and prompts, subagents and workflows, then executable checks. -The [July 8 RFC](2026-07-08-agent-scope-contexts.md) remains the contributor contract. The separate [subagent composition-controls RFC](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns `persona`, `toolFilter`, and `maxDepth`; this document discusses only how their setup fits the lifecycle. +The [July 8 Agent Note](2026-07-08-agent-scope-contexts.md) remains the contributor contract. The separate [subagent composition-controls Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns `persona`, `toolFilter`, and `maxDepth`; this document discusses only how their setup fits the lifecycle. ## Cordis model: context, fiber, effect, receiver, and waterfall @@ -206,7 +206,7 @@ Tool presentation and execution share one private resolver. Prompt assembly rema The private resolver applies the current presentation mode, live global restrictions, exact local overlay, and local shadowing. Schemas, lookup, execution, Code Mode SDK generation, and restriction validation all use that resolver or its pre-restriction global-name view. -The [subagent composition-controls RFC](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md#tool-filtering-is-one-live-global-view-rule) owns the user-visible allow/deny semantics. The implementation requirement is agreement: a filtered-away global cannot remain executable through a different lookup path, and a locally shadowed definition is the same definition presented and executed. +The [subagent composition-controls Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md#tool-filtering-is-one-live-global-view-rule) owns the user-visible allow/deny semantics. The implementation requirement is agreement: a filtered-away global cannot remain executable through a different lookup path, and a locally shadowed definition is the same definition presented and executed. `ToolRestriction` accepts readonly allow/deny names and compiles them into internal sets. Multiple restrictions intersect. Public `visible()` and `knownNames()` methods are unnecessary because only the registry needs the intermediate views. @@ -328,13 +328,13 @@ The plugin does not police trusted setup by scanning registries or reject prompt ### Generated artifacts keep public contracts aligned -The event catalog, service catalog, producer/consumer matrix, configuration catalog, module graph, tool catalog, type-equivalence blocks, and scoped-event resolver map are generated or freshness-gated from source. The [TypeScript semantic-gates RFC](../process/2026-07-14-typescript-program-backed-semantic-gates.md) owns Program construction, semantic event discovery, and resolver-generation rules. +The event catalog, service catalog, producer/consumer matrix, configuration catalog, module graph, tool catalog, type-equivalence blocks, and scoped-event resolver map are generated or freshness-gated from source. The [TypeScript semantic-gates Agent Note](../process/2026-07-14-typescript-program-backed-semantic-gates.md) owns Program construction, semantic event discovery, and resolver-generation rules. Behavioral tests pin scoped routing and disposal, final-entry collision cleanup, publication rollback, ordered quiescence, durable pre/post-commit behavior, live tool filtering across presentation and execution, cooperative prompt assembly, structured-output commit in native and Code Mode, async subagent startup and signal cancellation, worker terminal arbitration, ACP settlement, and process teardown. ## Alternatives considered -The [July 8 RFC](2026-07-08-agent-scope-contexts.md#alternatives-considered) owns alternatives to the public flat-scope contract. The alternatives here concern implementation shape. +The [July 8 Agent Note](2026-07-08-agent-scope-contexts.md#alternatives-considered) owns alternatives to the public flat-scope contract. The alternatives here concern implementation shape. ### Use a transparent proxy as the scope carrier diff --git a/docs/rfc/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml similarity index 62% rename from docs/rfc/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml rename to .agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml index 48055ef6b7..3ed38c1282 100644 --- a/docs/rfc/implemented/architecture/2026-07-14-provider-routed-llm-adapters.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.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-14-provider-routed-llm-adapters.md: 75ef047a7f95621d9a9c018b6dc57439e6f2bb22 -2026-07-14-provider-routed-llm-adapters.zh.md: 75ac9adbe5f8e96930a72a977c1969ff3a119ee8 +2026-07-14-provider-routed-llm-adapters.md: b7944bd31fdb5f63894e867d7c1224215d694f11 +2026-07-14-provider-routed-llm-adapters.zh.md: 7dcadf2521bab079e328b5f0d0a45185778b3b8d diff --git a/docs/rfc/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md similarity index 95% rename from docs/rfc/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md rename to .agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md index 75ef047a7f..b7944bd31f 100644 --- a/docs/rfc/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md +++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.md @@ -1,4 +1,4 @@ -# RFC: Provider-routed LLM adapters and a generic pi-ai backend +# Agent Note: Provider-routed LLM adapters and a generic pi-ai backend Status: implemented @@ -20,7 +20,7 @@ The adapter configuration also assumes one DeepSeek API key and endpoint. A gene `GenerateOptions` and `LlmCallConfig` carry `provider: string` beside `model: string`; `AgentOptions` carries the corresponding optional creation field. A loop request is valid only after both values are non-empty, and both values are part of the logged request header. `agent/request` may return a replacement pair on any step, so a session can switch providers and models without changing the Cordis plugin lifecycle. -`LlmService` registers and resolves adapters by provider. `registerAdapter(providers, adapter)` checks the entire provider list before mutating the registry, rejects a duplicate with `DUPLICATE_ADAPTER`, and disposes the whole registration as one effect. Model ids are not registration keys; the selected adapter still validates or forwards them. The later [LLM catalog and ACP selection RFC](2026-07-15-llm-model-catalog-and-acp-selection.md) added advisory `listProviders()` / `listModels()` discovery without turning model membership into request validation. +`LlmService` registers and resolves adapters by provider. `registerAdapter(providers, adapter)` checks the entire provider list before mutating the registry, rejects a duplicate with `DUPLICATE_ADAPTER`, and disposes the whole registration as one effect. Model ids are not registration keys; the selected adapter still validates or forwards them. The later [LLM catalog and ACP selection Agent Note](2026-07-15-llm-model-catalog-and-acp-selection.md) added advisory `listProviders()` / `listModels()` discovery without turning model membership into request validation. A provider has exactly one adapter owner in a Cordis context. `dsh-llm-deepseek` registers `deepseek`; `dsh-llm-pi-ai` may also register `deepseek`, but loading both owners is a configuration error rather than an ordering rule or fallback. A deployment that wants the hand-rolled DeepSeek implementation excludes `deepseek` from the pi-ai profiles. A deployment that wants pi-ai's DeepSeek implementation does not mount `dsh-llm-deepseek`. @@ -44,7 +44,7 @@ A terminal successful `finish` chunk may carry replay state, and `BlockAssembler The pi-ai replay state is a versioned, minimal projection of its successful `AssistantMessage`: source API/provider/model, response id/model, stop reason, and index-aligned text, thinking, and tool-call signatures. It does not duplicate text or tool arguments already carried by Harness content blocks, and it omits diagnostics, timestamps, usage, and errors. On a later request, `LlmService` gives replay state to the target adapter only when the historical provider and target provider are currently owned by the same adapter instance. That adapter combines the logged Harness content with replay state when it can restore the historical response, and owns any required cross-model or cross-provider conversion. An adapter receiving replay state with an unknown version or mismatched block shape fails explicitly; a different adapter receives only provider-neutral content and provenance. -This state is model-visible replay input and therefore follows the existing [reconstructable-request rule](../../implemented/architecture/2026-07-05-reconstructable-requests.md): it is present in both the terminal `finish` chunk and the assembled `assistant/message` provenance that drives derivation. Resume and fork preserve it verbatim. Compaction that shadows the assistant message also removes its replay state from the active surface; the summary is ordinary provider-neutral content. +This state is model-visible replay input and therefore follows the existing [reconstructable-request rule](2026-07-05-reconstructable-requests.md): it is present in both the terminal `finish` chunk and the assembled `assistant/message` provenance that drives derivation. Resume and fork preserve it verbatim. Compaction that shadows the assistant message also removes its replay state from the active surface; the summary is ordinary provider-neutral content. ### Propagate the target through every request producer diff --git a/docs/rfc/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md similarity index 95% rename from docs/rfc/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md rename to .agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md index 75ac9adbe5..7dcadf2521 100644 --- a/docs/rfc/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-14-provider-routed-llm-adapters.zh.md @@ -1,4 +1,4 @@ -# RFC: 基于提供方路由的 LLM 适配器与通用 pi-ai 后端 +# Agent Note: 基于提供方路由的 LLM 适配器与通用 pi-ai 后端 Status: implemented @@ -20,7 +20,7 @@ Status: implemented `GenerateOptions` 与 `LlmCallConfig` 在 `model: string` 之外携带 `provider: string`,`AgentOptions` 则携带对应的可选创建字段。只有两个值都非空时,agent loop(智能体循环)请求才有效;两个值也都会写入请求头日志。`agent/request` 可以在任意步骤返回替换后的字段组合,因此会话可以切换提供方与模型,无需改变 Cordis 插件生命周期。 -`LlmService` 按提供方注册和解析适配器。`registerAdapter(providers, adapter)` 在修改注册表前检查整个提供方列表,遇到重复项时返回 `DUPLICATE_ADAPTER`,并将整组注册作为一个 effect 释放。模型 ID 不作为注册键;仍由选中的适配器负责验证或转发。后续的 [LLM 目录与 ACP 模型选择 RFC](2026-07-15-llm-model-catalog-and-acp-selection.md) 增加了建议性的 `listProviders()` / `listModels()` 发现接口,但不会把目录成员关系变成请求校验规则。 +`LlmService` 按提供方注册和解析适配器。`registerAdapter(providers, adapter)` 在修改注册表前检查整个提供方列表,遇到重复项时返回 `DUPLICATE_ADAPTER`,并将整组注册作为一个 effect 释放。模型 ID 不作为注册键;仍由选中的适配器负责验证或转发。后续的 [LLM 目录与 ACP 模型选择 Agent Note](2026-07-15-llm-model-catalog-and-acp-selection.md) 增加了建议性的 `listProviders()` / `listModels()` 发现接口,但不会把目录成员关系变成请求校验规则。 在一个 Cordis 上下文中,一个提供方只能有一个适配器所有者。`dsh-llm-deepseek` 注册 `deepseek`;`dsh-llm-pi-ai` 也可以注册 `deepseek`,但同时加载两个所有者属于配置错误,不采用顺序规则或回退行为。若部署选择手写的 DeepSeek 实现,需从 pi-ai 配置中排除 `deepseek`;若部署选择 pi-ai 的 DeepSeek 实现,则不挂载 `dsh-llm-deepseek`。 @@ -44,7 +44,7 @@ pi-ai 的通用流选项不支持停止序列。若 Harness `stop` 选项已定 pi-ai 回放状态是其成功 `AssistantMessage` 的带版本最小投影,包含源 API/provider/model、响应 ID/model、停止原因,以及按索引对齐的文本、thinking 和工具调用签名。它不会重复 Harness 内容块中已有的文本或工具参数,也不包含诊断信息、时间戳、用量或错误。后续请求中,只有历史提供方和目标提供方当前归同一个适配器实例所有时,`LlmService` 才会把回放状态交给目标适配器。适配器在能够恢复历史响应时,将 Harness 记录的内容与回放状态组合,并负责所需的跨模型或跨提供方转换。适配器收到未知版本或块形状不匹配的回放状态时会显式失败;其他适配器只能收到提供方无关的内容与来源信息。 -该状态属于模型可见的回放输入,因此遵循现有的[请求可重建规则](../../implemented/architecture/2026-07-05-reconstructable-requests.md):它同时存在于终止 `finish` 分片和驱动派生的已组装 `assistant/message` 来源信息中。恢复和 fork 会原样保留该状态。压缩(compaction)遮蔽助手消息时,也会从活动 surface 中移除其回放状态;摘要属于普通的提供方无关内容。 +该状态属于模型可见的回放输入,因此遵循现有的[请求可重建规则](2026-07-05-reconstructable-requests.md):它同时存在于终止 `finish` 分片和驱动派生的已组装 `assistant/message` 来源信息中。恢复和 fork 会原样保留该状态。压缩(compaction)遮蔽助手消息时,也会从活动 surface 中移除其回放状态;摘要属于普通的提供方无关内容。 ### 在所有请求生产方中传播目标 diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml similarity index 64% rename from docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml rename to .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml index 52128ea344..d5e16246fe 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.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-15-agent-initiator-scope.md: a9df15beb8744216e020c259934db9fdf8b28b79 -2026-07-15-agent-initiator-scope.zh.md: 4198f066ef27042bda0d12fbcaf86f143482d596 +2026-07-15-agent-initiator-scope.md: 69648100e76cfc212469854188d664357fec22f1 +2026-07-15-agent-initiator-scope.zh.md: 835d7a5b2ab6d2d6fce7971de4fd9d6c69e50d77 diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md similarity index 98% rename from docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md rename to .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md index a9df15beb8..69648100e7 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md +++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md @@ -1,4 +1,4 @@ -# RFC: Initiating Agent scope over AsyncLocalStorage +# Agent Note: Initiating Agent scope over AsyncLocalStorage Status: implemented @@ -12,7 +12,7 @@ Deep process-local infrastructure sometimes needs a trusted initiating Agent bel ## Decision -The mandatory `ctx.agents` service uses Node `AsyncLocalStorage` to carry the initiating Agent. It stores the exact `Agent` directly rather than introducing a one-field frame; a separate private run token records nested boundary lineage only for teardown bookkeeping and carries no identity. The [core-data catalog](../../../core-data-structures/core.md#initiating-agent) identifies the carried type. +The mandatory `ctx.agents` service uses Node `AsyncLocalStorage` to carry the initiating Agent. It stores the exact `Agent` directly rather than introducing a one-field frame; a separate private run token records nested boundary lineage only for teardown bookkeeping and carries no identity. The [core-data catalog](../../../../docs/core-data-structures/core.md#initiating-agent) identifies the carried type. `currentInitiator()` reads optionally, `requireInitiator()` throws `no initiating agent is active`, and `withInitiator(agent, operation)` preserves the operation's exact synchronous value or Promise. `withoutInitiator(operation)` establishes a clearing boundary for work that must not inherit an Agent. Session remains derived as `agent.session`; turn, step, tool call, `signal`, model, `cwd`, sandbox, and authorization stay with their existing owners. diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md similarity index 98% rename from docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md rename to .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md index 4198f066ef..835d7a5b2a 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md @@ -1,4 +1,4 @@ -# RFC: 基于 AsyncLocalStorage 的发起 Agent 作用域 +# Agent Note: 基于 AsyncLocalStorage 的发起 Agent 作用域 Status: implemented @@ -12,7 +12,7 @@ Harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负 ## 决策 -必需的 `ctx.agents` 服务使用 Node `AsyncLocalStorage` 携带发起 Agent。它直接存储同一个 `Agent`,不引入只有一个字段的帧;另一个私有运行标记只记录嵌套边界的谱系,供 teardown 记账使用,不携带身份。[核心数据目录](../../../core-data-structures/core.md#initiating-agent)标明了所携带的类型。 +必需的 `ctx.agents` 服务使用 Node `AsyncLocalStorage` 携带发起 Agent。它直接存储同一个 `Agent`,不引入只有一个字段的帧;另一个私有运行标记只记录嵌套边界的谱系,供 teardown 记账使用,不携带身份。[核心数据目录](../../../../docs/core-data-structures/core.md#initiating-agent)标明了所携带的类型。 `currentInitiator()` 用于可选读取,`requireInitiator()` 抛出 `no initiating agent is active`,`withInitiator(agent, operation)` 保留操作返回的同步值或 Promise 本身。`withoutInitiator(operation)` 会建立清空边界,供不得继承 Agent 的工作使用。会话仍通过 `agent.session` 推导;轮次、步骤、工具调用、`signal`、模型、`cwd`、沙箱和授权继续由现有归属方管理。 diff --git a/docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml similarity index 60% rename from docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml rename to .agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml index d1756c9186..e83cfff95e 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.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-15-llm-model-catalog-and-acp-selection.md: d84fe9fdb75bd2d28a00269c84d29c4223798253 -2026-07-15-llm-model-catalog-and-acp-selection.zh.md: 019819c4aa5ab4ad281b5b32daa76a004c9d6466 +2026-07-15-llm-model-catalog-and-acp-selection.md: 6cc8afc6c7431fbf3eb29fc358b432db4f72b529 +2026-07-15-llm-model-catalog-and-acp-selection.zh.md: 1cce7a58d0ec83dc01feaf72ccb61d294a78ddd5 diff --git a/docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md similarity index 99% rename from docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md rename to .agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md index d84fe9fdb7..6cc8afc6c7 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md +++ b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md @@ -1,4 +1,4 @@ -# RFC: Advisory LLM catalogs and per-session ACP model selection +# Agent Note: Advisory LLM catalogs and per-session ACP model selection Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md similarity index 99% rename from docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md rename to .agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md index 019819c4aa..1cce7a58d0 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.zh.md @@ -1,4 +1,4 @@ -# RFC: 建议性 LLM 目录与 ACP 会话级模型选择 +# Agent Note: 建议性 LLM 目录与 ACP 会话级模型选择 Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml similarity index 63% rename from docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml rename to .agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml index 3c1e0a2573..d49aedb545 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.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-15-replay-token-meter-service.md: a86696a1077c9fa21d9984280009f9a0df0b4bbb -2026-07-15-replay-token-meter-service.zh.md: a6803fc9921a832b7614a7dc26b545a9145a08df +2026-07-15-replay-token-meter-service.md: 9bbc177f456e006179c466f8c245e4599db3dd5a +2026-07-15-replay-token-meter-service.zh.md: 4437626c8651a80537d45197a93733271a592173 diff --git a/docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.md b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md similarity index 99% rename from docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.md rename to .agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md index a86696a107..9bbc177f45 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.md +++ b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md @@ -1,4 +1,4 @@ -# RFC: Replay token meter service +# Agent Note: Replay token meter service Status: implemented diff --git a/docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md similarity index 99% rename from docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md rename to .agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md index a6803fc992..4437626c86 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md @@ -1,4 +1,4 @@ -# RFC: 回放式 token 计量服务 +# Agent Note: 回放式 token 计量服务 Status: implemented diff --git a/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.md b/.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md similarity index 97% rename from docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.md rename to .agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md index ea4b98c31d..7c47fc78e9 100644 --- a/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.md +++ b/.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md @@ -1,4 +1,4 @@ -# RFC: Agent Client Protocol (ACP) support — drive the coding agent from external editors +# Agent Note: Agent Client Protocol (ACP) support — drive the coding agent from external editors Status: implemented @@ -48,7 +48,7 @@ The precise supported and deferred protocol rows live in [`packages/ui/acp/acp-f Editors can create, load, prompt, cancel, render, ask, and reconfigure multiple harness sessions over one ACP connection without a loop-specific dependency. The session event log remains the durable source for replay, prompt settlement, cwd, and per-session configuration. Tool presentation and human-answer channels remain extensible plugin contracts instead of ACP-specific behavior. -The bridge deliberately does not implement session list/delete/resume/close capabilities, MCP passthrough, additional directories, image/audio/embedded-resource prompts, plans, slash commands, usage updates, editor filesystem delegation, or the ACP terminal execution sub-protocol. Runtime model selection was added later through standard session config options by the [LLM catalog and ACP selection RFC](../architecture/2026-07-15-llm-model-catalog-and-acp-selection.md). +The bridge deliberately does not implement session list/delete/resume/close capabilities, MCP passthrough, additional directories, image/audio/embedded-resource prompts, plans, slash commands, usage updates, editor filesystem delegation, or the ACP terminal execution sub-protocol. Runtime model selection was added later through standard session config options by the [LLM catalog and ACP selection Agent Note](../architecture/2026-07-15-llm-model-catalog-and-acp-selection.md). An idle config selection is truthful in the live response but not durable until the next `agent/prompt-submit` anchors it inside the open turn. Crashing before that boundary loses the pending selection; this is the cost of keeping session events turn-enclosed and replay-safe. diff --git a/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.md b/.agents/notes/implemented/feature/2026-06-14-acp-multi-session.md similarity index 98% rename from docs/rfc/implemented/feature/2026-06-14-acp-multi-session.md rename to .agents/notes/implemented/feature/2026-06-14-acp-multi-session.md index 64961bee89..def0604df9 100644 --- a/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.md +++ b/.agents/notes/implemented/feature/2026-06-14-acp-multi-session.md @@ -1,4 +1,4 @@ -# RFC: Multiplex concurrent ACP sessions over one connection +# Agent Note: Multiplex concurrent ACP sessions over one connection Status: implemented diff --git a/docs/rfc/implemented/feature/2026-06-15-code-mode.md b/.agents/notes/implemented/feature/2026-06-15-code-mode.md similarity index 89% rename from docs/rfc/implemented/feature/2026-06-15-code-mode.md rename to .agents/notes/implemented/feature/2026-06-15-code-mode.md index 3bc07b839f..329eaf2d0a 100644 --- a/docs/rfc/implemented/feature/2026-06-15-code-mode.md +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.md @@ -1,23 +1,23 @@ -# RFC: Code Mode — the model writes TypeScript against the tool registry +# Agent Note: Code Mode — the model writes TypeScript against the tool registry Status: implemented ## Problem -In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. `ToolRegistry` contributes its schemas to the system-prompt assembly, the assembly's `tools` land on the wire (and in the logged request header), the model invokes one `tool-call` block per step, and the loop dispatches each call through `ctx.tools.execute()` **sequentially** (parallel tool execution is an explicit open TODO in `dsh-tools` and [docs/architecture.md](../../../architecture.md)), with **every** intermediate `tool-result` re-entering the model's context on the next request. +In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. `ToolRegistry` contributes its schemas to the system-prompt assembly, the assembly's `tools` land on the wire (and in the logged request header), the model invokes one `tool-call` block per step, and the loop dispatches each call through `ctx.tools.execute()` **sequentially** (parallel tool execution is an explicit open TODO in `dsh-tools` and [docs/architecture.md](../../../../docs/architecture.md)), with **every** intermediate `tool-result` re-entering the model's context on the next request. For multi-step tool work this is token-heavy and serial. The model cannot compose tools — loop over a result set, branch on an intermediate value, fan out, post-process — without a full model round-trip per call, and each round-trip drags the entire intermediate result back into context whether the model needs it or not. Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode/) proposes an alternative grounded in a simple observation: LLMs are better at writing code than at emitting tool calls, because they have seen millions of lines of real code and comparatively few contrived tool-calling traces. Instead of one tool call per step, the model writes a TypeScript program against a generated API over the tools, the program executes in a sandboxed runtime, and the model curates what comes back — only what it prints or returns — instead of every intermediate result. -Tool presentation belongs to the registry that owns tool visibility: implementing a second presentation as an after-the-fact waterfall transform would make correctness depend on listener order and fight [reconstructable requests](../../implemented/architecture/2026-07-05-reconstructable-requests.md). The execution substrate is also part of the foundation rather than a placeholder: Node `worker_threads` provides a separate isolate, an empty environment, heap caps, and termination of a hot synchronous loop, while fitting the harness's existing trust model (§Trust posture). +Tool presentation belongs to the registry that owns tool visibility: implementing a second presentation as an after-the-fact waterfall transform would make correctness depend on listener order and fight [reconstructable requests](../architecture/2026-07-05-reconstructable-requests.md). The execution substrate is also part of the foundation rather than a placeholder: Node `worker_threads` provides a separate isolate, an empty environment, heap caps, and termination of a hot synchronous loop, while fitting the harness's existing trust model (§Trust posture). ## Decision Three decisions, each elaborated in its own section below: 1. **Code Mode is a first-class presentation mode of `ToolRegistry`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'code'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry shapes its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation. -2. **Code execution is a capability seam** — `packages/code-runtime/` contains the interface package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../../implemented/architecture/2026-06-13-capability-seams.md); consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop` → `dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another implementation package, not a redesign. +2. **Code execution is a capability seam** — `packages/code-runtime/` contains the interface package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop` → `dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another implementation package, not a redesign. 3. **The shipped implementation is `@deepseek-ai/dsh-code-runtime-worker`**: one fresh Node worker thread per run, executing the model's TypeScript after type-strip, with bindings bridged over the message port, an empty environment, configurable heap/output/time caps, and hard termination. Its trust posture is bash-equivalent by design — no unsafe-acknowledgement flags — because the harness already ships `dsh-bash-local`, which executes arbitrary model-written shell commands with strictly *more* ambient authority. ### The registry owns the mode @@ -46,7 +46,7 @@ Under `'code'` and `'both'` the registry owns `run_code` as a reserved presentat **Concurrency is serialized.** Each run owns a dispatch queue, so even `Promise.all` executes tool calls in submission order. Settlement abandons queued calls that have not started. Parallelism requires per-tool concurrency-safety metadata. -**Presentation.** `run_code`'s render intent is decided here per the [render-intent RFC](../../implemented/architecture/2026-07-02-tool-render-intent-union.md): `presentCall` → a `generic` card, `kind: 'execute'`, title = the program text, `rawInput` = the same program text; `presentResult` → a `generic` card whose content is the captured output (from `meta`). The program is the title because ACP execute cards reliably render that field while some clients omit body and raw-input content. This is not a `terminal` card: that card's semantics are "a shell command in a working directory", which a program is not. +**Presentation.** `run_code`'s render intent is decided here per the [render-intent Agent Note](../architecture/2026-07-02-tool-render-intent-union.md): `presentCall` → a `generic` card, `kind: 'execute'`, title = the program text, `rawInput` = the same program text; `presentResult` → a `generic` card whose content is the captured output (from `meta`). The program is the title because ACP execute cards reliably render that field while some clients omit body and raw-input content. This is not a `terminal` card: that card's semantics are "a shell command in a working directory", which a program is not. ### Observability: `tool/code-dispatch` @@ -60,7 +60,7 @@ Each sub-dispatch appends a log-only `tool/code-dispatch` event containing paren - `CodeBindingNamespace = { global: string; functions: Record<string, (args: unknown) => Promise<unknown>> }` — the runtime exposes each namespace as a global object of async functions inside the program; binding arguments and resolutions must be structured-cloneable (a runtime may cross a serialization boundary; ours does). - `CodeRunResult = { value?: unknown; logs: CodeLogEntry[]; error?: CodeRunFailure }` — program execution outcomes, including exception, timeout, abort, and worker exit, resolve as the `error` field. `run()` may reject only for caller/seam misuse (for example a duplicate binding namespace); consumers still contain a non-conforming backend rejection at their own error boundary. - `CodeLogEntry = { source: 'console' | 'stdout' | 'stderr'; level?: 'log' | 'info' | 'warn' | 'error' | 'debug'; text: string }` -- `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit'; message: string }` — orthogonal outcomes reported independently per [defensive patterns](../../../defensive-patterns.md); a timed-out run is not an exception, an abort is not a timeout. +- `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit'; message: string }` — orthogonal outcomes reported independently per [defensive patterns](../../../../docs/defensive-patterns.md); a timed-out run is not an exception, an abort is not a timeout. - Two readonly backend descriptors, informational not gating: `language` (what the program must be written in — `'typescript'` for the shipped backend; a Python backend would say so, and pair with its own SDK generator on the presentation side) and `isolation` (`'worker-thread'` for the shipped backend; `'process'`, `'container'`, … for future ones). `dsh-tools` requires `language === 'typescript'` in the MVP — its codegen emits TS — and fails the assembly loudly otherwise, the same misconfiguration idiom as `toolOrder` violations (as when `mode` is non-native with no `ctx.codeRuntime` loaded at all). Requests contain every runtime input; implementations own validated timeout and cap defaults. The registry looks up the optional runtime only when Code Mode is assembled, so native mode does not depend on one. Missing or language-incompatible runtimes fail loudly. Alternate substrates or languages can replace the implementation behind the same seam, paired with the appropriate SDK generator. @@ -74,7 +74,7 @@ Requests contain every runtime input; implementations own validated timeout and 3. **Execute** in the bootstrap: the stripped program becomes the body of an `AsyncFunction` whose parameters are the binding globals and a capturing `console` shim, so top-level `await` and `return` work and the program's completion value is the run's `value` (structured-cloneable values cross as-is; anything else is replaced by its `util.inspect` rendering, documented). 4. **Bridge bindings over the message port**: each binding function in the worker posts `{ id, global, name, args }` and awaits the reply; the host validates the name against the request's bindings, invokes, and replies `{ id, ok, value }` or `{ id, ok: false, message }` (a host-side binding rejection becomes a program-side rejection). The worker-side namespace objects are built null-prototype via `defineProperty`, so a binding named `__proto__`, `constructor`, or `toString` is an ordinary own property, not a prototype collision. Unknown names, duplicate ids, and post-settlement messages are rejected or ignored — the port protocol assumes a hostile peer, because the peer runs model code. 5. **Enforce independent budgets.** `computeMs` meters worker busy time, allowing slow awaited tools without excusing a hot loop. `maxWallMs` bounds total elapsed time, including unresolved waits. Expiry, cancellation, and completion terminate the worker. Heap exits and truncation are reported explicitly; compute, wall, heap, log, and return-value caps are validated configuration. -6. **Dispose to quiescence**: the service's own disposal terminates in-flight workers and *awaits* their exits before resolving, per [defensive patterns](../../../defensive-patterns.md). +6. **Dispose to quiescence**: the service's own disposal terminates in-flight workers and *awaits* their exits before resolving, per [defensive patterns](../../../../docs/defensive-patterns.md). ### Trust posture @@ -97,7 +97,7 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem ## Alternatives considered -**An add-on consumer plugin with zero core changes.** Rejected because `agent/request` is call-config-only under [reconstructable requests](../../implemented/architecture/2026-07-05-reconstructable-requests.md), while transforming an assembled tool list would have to undo `toolOrder` canonicalization without owning its config and would depend on listener order. Which tools the model is offered, and in which representation, is the registry's single concern: native schemas and the SDK are two projections of one visible store. +**An add-on consumer plugin with zero core changes.** Rejected because `agent/request` is call-config-only under [reconstructable requests](../architecture/2026-07-05-reconstructable-requests.md), while transforming an assembled tool list would have to undo `toolOrder` canonicalization without owning its config and would depend on listener order. Which tools the model is offered, and in which representation, is the registry's single concern: native schemas and the SDK are two projections of one visible store. **`node:vm` as the reference runtime, with hardening deferred.** Rejected: `node:vm` is not isolation (prototype-chain escapes reach the host realm) and cannot interrupt a hot loop. A worker thread provides a separate isolate, empty environment, `resourceLimits`, and reliable `terminate()` at bash-equivalent trust, so the reference and production implementation are one package without an unsafe-acknowledgement ceremony. @@ -119,7 +119,7 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem **`stripTypeScriptTypes` is marked experimental.** It is the same engine (amaro/swc) behind Node's own native `.ts` execution, exposed as an API across this repo's whole engines range. Mitigations: the runtime's unit suite pins the behaviors relied on (position preservation, erasable-only rejection message shape loosely), the call sits behind one private function, and `amaro`/`sucrase` are drop-in replacements if the API shifts. The erasable-only subset is a model-facing contract line, and the error path is a working feedback loop, not a dead end. -**Prompt cost of the SDK, especially under `'both'`.** The `.d.ts` can rival the native schemas it complements; `'both'` carries two representations. Prefix stability + provider caching amortize per-session cost; the mode is per-deployment; the RFC makes no unconditional-savings claim. Measured guidance (when to prefer which mode) is explicitly post-ship learning. +**Prompt cost of the SDK, especially under `'both'`.** The `.d.ts` can rival the native schemas it complements; `'both'` carries two representations. Prefix stability + provider caching amortize per-session cost; the mode is per-deployment; the Agent Note makes no unconditional-savings claim. Measured guidance (when to prefer which mode) is explicitly post-ship learning. **Registry scope growth.** `dsh-tools` absorbs codegen, a tool, a bridge, and an event. Contained by module boundaries inside the package (`ts-types.ts`, `code-mode.ts` beside `schema.ts`/`json-schema.ts`/`presentation.ts`) and by the seam: everything substrate-shaped lives behind `ctx.codeRuntime`. diff --git a/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md b/.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md similarity index 86% rename from docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md rename to .agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md index adad17472d..a318956ddf 100644 --- a/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md +++ b/.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md @@ -1,12 +1,12 @@ -# RFC: Filesystem tool schemas — model-facing read/write/edit shapes +# Agent Note: Filesystem tool schemas — model-facing read/write/edit shapes Status: implemented ## Problem -[The filesystem capability-seam RFC](../architecture/2026-06-17-filesystem-capability-seam.md) defines the filesystem capability seam (`ctx.fs`), the package split (`dsh-fs`, `dsh-fs-local`, `dsh-tool-fs`, plus the `dsh-fs-policy` policy plugin), and the observed-file/stale-version policy for read-before-write/edit checks — which the [split-fs-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [event-gate](../architecture/2026-06-26-file-context-as-event-gate.md) RFCs moved off `ctx.fs` into the `dsh-fs-policy` plugin on the `fs/*` event gate. The remaining decision for the first filesystem tool delivery is the model-facing schema surface: what arguments the model sees for `read`, `write`, and `edit`. +[The filesystem capability-seam Agent Note](../architecture/2026-06-17-filesystem-capability-seam.md) defines the filesystem capability seam (`ctx.fs`), the package split (`dsh-fs`, `dsh-fs-local`, `dsh-tool-fs`, plus the `dsh-fs-policy` policy plugin), and the observed-file/stale-version policy for read-before-write/edit checks — which the [split-fs-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [event-gate](../architecture/2026-06-26-file-context-as-event-gate.md) Agent Notes moved off `ctx.fs` into the `dsh-fs-policy` plugin on the `fs/*` event gate. The remaining decision for the first filesystem tool delivery is the model-facing schema surface: what arguments the model sees for `read`, `write`, and `edit`. -The schema should be small enough to implement in the first `dsh-tool-fs` pass, but stable enough that future local/remote/sandboxed filesystem backends do not require model-facing churn. It should also avoid importing every option from reference systems. Claude Code and OpenCode expose similar core file tools but differ in naming style and extra flags; this RFC chooses the minimal shared surface for the prototype. +The schema should be small enough to implement in the first `dsh-tool-fs` pass, but stable enough that future local/remote/sandboxed filesystem backends do not require model-facing churn. It should also avoid importing every option from reference systems. Claude Code and OpenCode expose similar core file tools but differ in naming style and extra flags; this Agent Note chooses the minimal shared surface for the prototype. ## Decision @@ -103,8 +103,8 @@ Schema tests pin the required/optional argument set per tool, empty-`old_string` ## Consequences -**The first schema is intentionally smaller than Claude Code's.** Dropping PDF pages, multimodal read, rich grep/list flags, and expected hash fields keeps the implementation focused, but users may ask for those quickly. They arrive as separate RFCs or focused follow-ups rather than overloads of the initial schema. +**The first schema is intentionally smaller than Claude Code's.** Dropping PDF pages, multimodal read, rich grep/list flags, and expected hash fields keeps the implementation focused, but users may ask for those quickly. They arrive as separate Agent Notes or focused follow-ups rather than overloads of the initial schema. **No explicit model-facing stale guard in v1.** The schema does not ask the model to provide an expected hash/version. That is intentional: stale checks come from backend-produced versions and the `dsh-fs-policy` plugin's observed state, not from fragile model-copied tokens. Filesystem safety failures surface through structured `FsError` codes owned by `dsh-fs`, not through model-supplied version fields. -**Naming becomes public surface.** Once shipped, changing `file_path` to `filePath` or `old_string` to `oldString` would churn prompts, examples, and downstream clients. This RFC chooses snake_case up front and treats it as the stable model-facing contract. +**Naming becomes public surface.** Once shipped, changing `file_path` to `filePath` or `old_string` to `oldString` would churn prompts, examples, and downstream clients. This Agent Note chooses snake_case up front and treats it as the stable model-facing contract. diff --git a/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md b/.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md similarity index 90% rename from docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md rename to .agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md index 3b8cd810a8..8d9c0197eb 100644 --- a/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md +++ b/.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md @@ -1,10 +1,10 @@ -# RFC: Rich ACP bash rendering — the terminal card via the `_meta` convention +# Agent Note: Rich ACP bash rendering — the terminal card via the `_meta` convention Status: implemented ## Problem -The ACP bridge lets each tool own its call rendering via `presentCall`/`presentResult` (see [tool-call UI presentation](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md) and `packages/core/tools`). For `bash` we surface the exact command as the `tool_call` title, the model's `description` as a content text block, `kind: 'execute'`, and the completed output wrapped in a fenced ` ```console ` text block. +The ACP bridge lets each tool own its call rendering via `presentCall`/`presentResult` (see [tool-call UI presentation](2026-06-14-acp-agent-client-protocol.md) and `packages/core/tools`). For `bash` we surface the exact command as the `tool_call` title, the model's `description` as a content text block, `kind: 'execute'`, and the completed output wrapped in a fenced ` ```console ` text block. Reference editors render terminal metadata as a dedicated card with cwd, command, live-style output, and exit status; plain text loses that structure. The command is the title because execute cards hide raw input, while the human-readable description remains a separate block above the card. @@ -43,4 +43,4 @@ Keep `dsh-bash` agent-side execution; render the terminal card via the `_meta` c ## Out of scope / non-goals -The text-block baseline stays the no-capability default. Two follow-ups are deliberately NOT built here and would each warrant their own RFC when someone takes them on: **live incremental streaming** (`_meta.terminal_output_delta` as chunks arrive, which needs an incremental-output seam on `dsh-bash`), and **command classification** (parsing a `cat`/`sed` as a `read` card with a file location, a `grep` as a `search`, etc., falling back to the terminal card — display-only, must never change what executes). +The text-block baseline stays the no-capability default. Two follow-ups are deliberately NOT built here and would each warrant their own Agent Note when someone takes them on: **live incremental streaming** (`_meta.terminal_output_delta` as chunks arrive, which needs an incremental-output seam on `dsh-bash`), and **command classification** (parsing a `cat`/`sed` as a `read` card with a file location, a `grep` as a `search`, etc., falling back to the terminal card — display-only, must never change what executes). diff --git a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md b/.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md similarity index 70% rename from docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md rename to .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md index 1d767d49fc..30a0c83a31 100644 --- a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md +++ b/.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md @@ -1,4 +1,4 @@ -# RFC: Compaction as a capability seam (abstract contract + basic backend) +# Agent Note: Compaction as a capability seam (abstract contract + basic backend) Status: implemented @@ -6,23 +6,24 @@ Status: implemented A long-running agent conversation grows without bound. As the event log accumulates turns, the derived message history eventually approaches the model's context window — the model then truncates mid-response (`max-tokens`) or degrades. **Compaction** is the mitigation: replace a run of older history with a concise summary, keeping recent context intact. -The [session surface](../../implemented/architecture/2026-06-18-session-surface.md) was built as the foundation for exactly this — an ordered projection over the event log with a `surfaceOp: { op: 'replace', start, end }` operation purpose-built to shadow a range of entries and insert a replacement, with `sourceEventSeqs` recording provenance so the decision replays deterministically. What remained was the plugin that *decides what to compact and produces the summary*. +The [session surface](../architecture/2026-06-18-session-surface.md) was built as the foundation for exactly this — an ordered projection over the event log with a `surfaceOp: { op: 'replace', start, end }` operation purpose-built to shadow a range of entries and insert a replacement, with `sourceEventSeqs` recording provenance so the decision replays deterministically. What remained was the plugin that *decides what to compact and produces the summary*. -Two forces shape the design. First, compaction policy and reusable token measurement vary independently: measurement belongs to the LLM-family [`ctx.tokenMeter` service](../../implemented/architecture/2026-07-15-replay-token-meter-service.md), while summarization can be a model call, a template, or a remote service. Second, `SurfaceEventType` is closed to five event types (`user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`); only those may carry `surfaceOp`. A bespoke `compaction/*` event therefore **cannot** itself appear on the surface — the compiler rejects `surfaceOp` on it and the invariants plugin rejects it at runtime. +Two forces shape the design. First, compaction policy and reusable token measurement vary independently: measurement belongs to the LLM-family [`ctx.tokenMeter` service](../architecture/2026-07-15-replay-token-meter-service.md), while summarization can be a model call, a template, or a remote service. Second, `SurfaceEventType` is closed to five event types (`user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`); only those may carry `surfaceOp`. A bespoke `compaction/*` event therefore **cannot** itself appear on the surface — the compiler rejects `surfaceOp` on it and the invariants plugin rejects it at runtime. ## Decision ### Compaction is a capability seam, split interface / implementation -Per the [capability-seams RFC](../../implemented/architecture/2026-06-13-capability-seams.md), compaction ships as separate packages so the contract, the algorithm, and (later) the consumer surface evolve independently: +Per the [capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md), compaction ships as separate packages so the contract, the algorithm, and (later) the consumer surface evolve independently: 1. **Interface** — `@deepseek-ai/dsh-compact`: an abstract `CompactService` owning the `ctx.compact` key, the `CompactionResult` vocabulary, and the `compact/*` session events. It declares `compactIfNeeded()` and `compactRegion()` as **abstract** — the contract states *what* compaction does, not *how*. 2. **Implementation** — `@deepseek-ai/dsh-compact-basic`: a concrete `BasicCompactService` that consumes `ctx.tokenMeter` and owns the tail→head retention walk, summarization via `ctx.llm.stream()`, the surface replacement, the lock, post-step pressure, and canonical context-overflow recovery. `summarize()` is its sole subclass hook; pricing and replay stay with the meter. -3. **Consumer** — deferred. A `/compact` tool and slash command will `inject: ['compact']` and call the contract; they are intentionally out of scope here so the seam settles first. +3. **Model-free companion** — `@deepseek-ai/dsh-compact-tool-result-prune`: a concrete optional service that rewrites oversized current `tool/result` nodes before the backend selects a summary range. It is not a second compaction implementation and does not implement `CompactService`. +4. **Consumer** — deferred. A `/compact` tool and slash command will `inject: ['compact']` and call the contract; they are intentionally out of scope here so the seam settles first. ### The contract depends on `dsh-session` and `dsh-llm` — a deliberate deviation -The capability-seams RFC states the interface package "depends only on cordis" (true of `dsh-bash`, whose vocabulary is self-contained). Compaction **cannot** honor that: its verbs act on an agent-owned `Session` (`compactRegion(start, end, agent)`) and its output uses the content vocabulary (`CompactionResult.summary: ContentBlock[]`). There is no way to express the contract without naming `Session`/`SessionEvent` (from `dsh-session`) and `ContentBlock` (from `dsh-llm`). +The capability-seams Agent Note states the interface package "depends only on cordis" (true of `dsh-bash`, whose vocabulary is self-contained). Compaction **cannot** honor that: its verbs act on an agent-owned `Session` (`compactRegion(start, end, agent)`) and its output uses the content vocabulary (`CompactionResult.summary: ContentBlock[]`). There is no way to express the contract without naming `Session`/`SessionEvent` (from `dsh-session`) and `ContentBlock` (from `dsh-llm`). This is not a coupling smell — it is the contract's domain. The "only cordis" guidance was always shorthand for "the interface depends only on what the contract genuinely names, and never on an implementation." `dsh-session` and `dsh-llm` are themselves interface/vocabulary packages, not implementations; `dsh-compact` still imports no backend. The seam's real invariant — *consumers and implementations evolve independently behind an abstract service* — holds intact. @@ -34,9 +35,9 @@ An earlier draft put the full algorithm (the retention walk, token-summing, text ### Automatic pressure runs after successful durable step work -Successful-call pressure cannot run at pre-step because final `agent/request` routing, provider output, tool results, buffered context, and steering do not exist there. Serial `agent/post-step(agent, turn, step, signal)` fires after those facts are durable and before `step/end`. `dsh-compact-basic` measures the canonical logged request through `ctx.tokenMeter`, so the next request sees any replacement without a speculative envelope override. +Successful-call pressure cannot run at pre-step because final `agent/request` routing, provider output, tool results, buffered context, and steering do not exist there. Serial `agent/post-step(agent, turn, step, signal)` fires after those facts are durable and before `step/end`. `dsh-compact-basic` measures the canonical logged request through `ctx.tokenMeter`, so the next request sees any replacement without a speculative envelope override. Once pressure qualifies, optional `ctx.toolResultPrune` rewriting runs before summary selection; compact-basic remeasures the durable surface and skips summarization if pruning restores safe pressure. -Canonical provider context overflow takes a separate path. The failed step closes, `agent/request-error` receives the original request error and consecutive retry count, and compact-basic forces one useful balanced reduction. It returns retry only if `session.surface.replaceGeneration` increases; the loop then opens a new numbered step and reconstructs its request from the durable log. No range, no replacement, recovery failure, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. The complete lifecycle decision is in the [after-call recovery RFC](../../implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md). +Canonical provider context overflow takes a separate path. The failed step closes, `agent/request-error` receives the original request error and consecutive retry count, and compact-basic prunes before forcing one useful balanced reduction. It returns retry only if `session.surface.replaceGeneration` increases, including pruning-only progress when no summary range exists; the loop then opens a new numbered step and reconstructs its request from the durable log. No replacement, a recovery failure before any replacement, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. If pruning already advanced the generation before later summary work fails, recovery retries from that durable pruned surface unless cancellation or disposal wins. The complete lifecycle decision is in the [after-call recovery Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md). ``` assistant/message → tool/result/context/steering @@ -56,7 +57,7 @@ Auto-compaction checks after **every successful** step, not once per turn. This A runaway turn thus compacts exactly like any other history: its early *closed* steps get summarized while its recent steps stay verbatim. When the only compactable content left is an un-splittable open tail step (its tool-calls have no results yet), compaction declines (`null`) and retries once that step closes. -**Single-unit overflow is out of scope, by design.** If a single retained unit — one closed step, or a large free entry such as a pasted `user/message` — *alone* exceeds the budget, compaction cannot help and the next model call may go out over-budget. Bounding an individual unit's size is a separate concern (output truncation), handled elsewhere; compaction makes no promise about it, and the harness without such a mechanism can still break on a single oversized unit. This is named honestly rather than papered over. +**Some single-unit overflow remains out of scope.** Summary range selection cannot split an indivisible unit. The optional pruner can repair a closed tool pair when removable text-bearing tool-result content is the bulk and the pruned remainder fits. Envelope-only pressure, an oversized indivisible non-tool node such as a pasted `user/message`, and a tool unit whose non-prunable remainder is still oversized remain outside compaction; bounding those units is a separate concern. ### Head-anchoring: one auto checkpoint, always at the head @@ -94,8 +95,8 @@ The `compact/start … compact/end` bracket is justified, in order of what now d Two failure paths, both documented: -- **Crash** (the loop dies mid-summarization): a dangling `compact/start`, no closer. Because `compact/*` are **log-only**, the orphan is **inert** — the surface replacement never landed, so the full, uncompacted history derives correctly. Generic turn-repair (`interruptedTurnClosers`) closes the turn with a synthetic `turn/end`; the orphan sits *before* that `turn/end`, so the turn-scoped in-progress check never sees it and a crash cannot wedge future compaction. -- **Recoverable** (summarization throws but the loop survives): the backend appends `compact/end` with its **`error`** field set and leaves the surface untouched. Post-step pressure warns and continues; overflow recovery delegates so the original provider error remains authoritative. +- **Crash** (the loop dies mid-summarization): a dangling `compact/start`, no closer. Because `compact/*` are **log-only**, the orphan is **inert** — no summary replacement lands. The derived surface remains the durable surface present at `compact/start`: full history when pruning made no replacement, or the already-pruned history when it did. Generic turn-repair (`interruptedTurnClosers`) closes the turn with a synthetic `turn/end`; the orphan sits *before* that `turn/end`, so the turn-scoped in-progress check never sees it and a crash cannot wedge future compaction. +- **Recoverable** (summarization throws but the loop survives): the backend appends `compact/end` with its **`error`** field set and lands no summary replacement. Post-step pressure warns and continues from the latest durable surface — full history if no replacement preceded the attempt, or the pruned surface if pruning already landed. Overflow recovery delegates only before any replacement; generation progress from earlier pruning authorizes a retry from that durable surface unless cancellation or disposal wins. `compact/end` keeps its `error?` field (mirroring `tool/result`'s self-contained error — one event tells success from failure without correlating a sibling). There is no separate `compact/error` event. @@ -110,16 +111,16 @@ Two failure paths, both documented: ## Consequences -- **Packages**: `packages/compact/compact` supplies the interface and `compact-basic` supplies the backend. `packages/llm/token-meter` owns replay-aware measurement independently. The consumer tier is deferred. +- **Packages**: `packages/compact/compact` supplies the interface, `compact-basic` supplies the backend, and `compact-tool-result-prune` supplies optional deterministic rewriting. `packages/llm/token-meter` owns replay-aware measurement independently. The consumer tier is deferred. - **Automatic seams**: `agent/post-step` (`@mode serial`) handles successful-call pressure and `agent/request-error` (`@mode waterfall`) handles final request failures after the failed step closes. Generic `agent/pre-step` remains a four-argument checkpoint with no compaction-only prompt/prefix payload. - **`SessionEventMap`** gains `compact/start` / `compact/summary` / `compact/end` by declaration merging (merge-extensible); `SurfaceEventType` is **not** touched. These are session events, not cordis `Events`, so the event-taxonomy gate needs no entry. -- **`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` continues to own the surface `replace` operation, ordered event sequences, and rewrite generation. -- **`dsh-invariants`** drops its `surface replace: start must be <= end` assertion: a head-anchored compaction lands a high-seq replacement entry at an older range's *position*, so `start > 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/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md similarity index 82% rename from docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md rename to .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md index 3f44940848..3ed2090b22 100644 --- a/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md +++ b/.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md @@ -1,12 +1,12 @@ -# RFC: Subagent capability seam +# Agent Note: Subagent capability seam Status: implemented -> The full seam is shipped: the `dsh-subagent` interface and `dsh-tool-subagent` consumer; the two in-process backends (`dsh-subagent-spawn`, `dsh-subagent-fork`); the nested-agent snapshot infrastructure ([per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md)); and the out-of-process `dsh-subagent-acp` backend ([its RFC](2026-06-22-acp-subagent-backend.md)). +> The full seam is shipped: the `dsh-subagent` interface and `dsh-tool-subagent` consumer; the two in-process backends (`dsh-subagent-spawn`, `dsh-subagent-fork`); the nested-agent snapshot infrastructure ([per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md)); and the out-of-process `dsh-subagent-acp` backend ([its Agent Note](2026-06-22-acp-subagent-backend.md)). ## Problem -The harness has a long-deferred seam for **subagents** — an agent delegating work to another agent. The intent was sketched in the `Agent`/`AgentLoop` interfaces ([packages/core/agent/src/types.ts](../../../../packages/core/agent/src/types.ts), [packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)): a creation option referencing a parent agent (fork = seed the child session with the parent's event log; spawn = fresh session), with the child returned as an `Agent` handle so steering and event subscription work uniformly. This RFC realizes that seam; the banner above lists what shipped. +The harness has a long-deferred seam for **subagents** — an agent delegating work to another agent. The intent was sketched in the `Agent`/`AgentLoop` interfaces ([packages/core/agent/src/types.ts](../../../../packages/core/agent/src/types.ts), [packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)): a creation option referencing a parent agent (fork = seed the child session with the parent's event log; spawn = fresh session), with the child returned as an `Agent` handle so steering and event subscription work uniformly. This Agent Note realizes that seam; the banner above lists what shipped. The distinctive requirement — the one that shapes the whole design — is that **multiple subagent implementations must coexist at runtime**. A parent may want a cheap in-process child for a scoped subtask AND an isolated out-of-process child (over ACP) in the same session. The transports we foresee: @@ -18,7 +18,7 @@ The distinctive requirement — the one that shapes the whole design — is that ### Why not the bash seam shape -The bash seam ([capability seams](../../implemented/architecture/2026-06-13-capability-seams.md)) registers exactly one `BashExecutor` per context; loading a second throws. That is correct for bash (one machine, one way to run a command) but wrong here: coexistence is the requirement. So the subagent service is a **named-provider registry** — each implementation registers under a unique name and a caller picks one by name — mirroring the **LLM adapter registry** (`LlmService.registerAdapter`), not the single-service bash executor. The seam is still three-package (interface / implementation / consumer); only the "one vs. many implementations" axis differs. +The bash seam ([capability seams](../architecture/2026-06-13-capability-seams.md)) registers exactly one `BashExecutor` per context; loading a second throws. That is correct for bash (one machine, one way to run a command) but wrong here: coexistence is the requirement. So the subagent service is a **named-provider registry** — each implementation registers under a unique name and a caller picks one by name — mirroring the **LLM adapter registry** (`LlmService.registerAdapter`), not the single-service bash executor. The seam is still three-package (interface / implementation / consumer); only the "one vs. many implementations" axis differs. ## Decision @@ -61,11 +61,11 @@ Each subagent runs in its **own `Session`** (own id, `parentSession` lineage), p ## Testing -Registry and tool tests replace only the nondeterministic child boundary with a package-local scripted provider while exercising the real `SubagentService`, lifecycle, task integration, and model-facing tool. Provider and consumer export shapes retain their Loader regression coverage for the failure described in [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md). Registry tests cover reload safety, duplicate names, and start-time capability rejection; nested-agent scenarios replay keylessly through [per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md); in-process backends also have real-loop unit tests and a with-key e2e. +Registry and tool tests replace only the nondeterministic child boundary with a package-local scripted provider while exercising the real `SubagentService`, lifecycle, task integration, and model-facing tool. Provider and consumer export shapes retain their Loader regression coverage for the failure described in [postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md). Registry tests cover reload safety, duplicate names, and start-time capability rejection; nested-agent scenarios replay keylessly through [per-session snapshot replay](../testing/2026-06-22-subagent-snapshot-replay.md); in-process backends also have real-loop unit tests and a with-key e2e. ## Consequences -- **Recursion.** Without a bound, an in-process child can see the delegation tool and recurse. The in-process backends implement the optional absolute depth limit and scoped live-global `toolFilter`; ACP advertises both capabilities off and rejects such a request. The [subagent composition-controls RFC](2026-07-12-subagent-persona-tool-filter-and-depth.md) owns their exact semantics and security limits. +- **Recursion.** Without a bound, an in-process child can see the delegation tool and recurse. The in-process backends implement the optional absolute depth limit and scoped live-global `toolFilter`; ACP advertises both capabilities off and rejects such a request. The [subagent composition-controls Agent Note](2026-07-12-subagent-persona-tool-filter-and-depth.md) owns their exact semantics and security limits. - **Blocking the parent turn.** Foreground collection holds the parent's step open for the child's full duration. Background delegation uses the shared `ctx.tasks` runtime and generic `task_*` tools, the same collection mechanism as background bash; the subagent seam itself remains task-agnostic. - **Live progress.** This cut surfaces only lifecycle + final result; a per-chunk child→parent update stream is deferred with the background redesign. - **ACP client surface.** Proxying `fs`/`terminal` from the ACP child back to the parent (a shared-workspace mode) is future work; the first cut advertises neither, so the child self-serves in its own process. diff --git a/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.md b/.agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md similarity index 84% rename from docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.md rename to .agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md index 0b98adb0de..1c5b5b533d 100644 --- a/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.md +++ b/.agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md @@ -1,10 +1,10 @@ -# RFC: ACP subagent backend (out-of-process delegation) +# Agent Note: ACP subagent backend (out-of-process delegation) Status: implemented ## Problem -The subagent seam ([the seam RFC](2026-06-21-subagent-capability-seam.md)) was built so multiple backends coexist by name on `ctx.subagents`. The in-process backends (`-spawn`/`-fork`) run a child as a second `Agent` on the SAME cordis context — cheap, but the child shares the parent's process, model client, and tools. The seam's whole point was to also support an OUT-OF-PROCESS child reached over a protocol, proving the abstraction generalizes across a process boundary. This RFC adds the first such backend: an Agent Client Protocol (ACP) client. +The subagent seam ([the seam Agent Note](2026-06-21-subagent-capability-seam.md)) was built so multiple backends coexist by name on `ctx.subagents`. The in-process backends (`-spawn`/`-fork`) run a child as a second `Agent` on the SAME cordis context — cheap, but the child shares the parent's process, model client, and tools. The seam's whole point was to also support an OUT-OF-PROCESS child reached over a protocol, proving the abstraction generalizes across a process boundary. This Agent Note adds the first such backend: an Agent Client Protocol (ACP) client. ## Decision @@ -16,7 +16,7 @@ Each `start` spawns a new child, runs exactly one ACP session (`initialize` → ### Minimal client stub -The client advertises NO optional capabilities (no `fs`, no `terminal`): the child self-serves file/terminal access in its own process. `session/update` notifications are consumed — the backend accumulates `agent_message_chunk` text as the result output and ignores the rest (thoughts, tool-call cards) in this cut, which surfaces only the child's final answer. `session/request_permission` is auto-answered by a configured policy (`reject` declines every prompt, `allow` approves via the first allow-shaped option) — the first cut surfaces no prompt to a human. Proxying `fs`/`terminal` back to the parent (a shared-workspace mode) remains future work, as the seam RFC noted. +The client advertises NO optional capabilities (no `fs`, no `terminal`): the child self-serves file/terminal access in its own process. `session/update` notifications are consumed — the backend accumulates `agent_message_chunk` text as the result output and ignores the rest (thoughts, tool-call cards) in this cut, which surfaces only the child's final answer. `session/request_permission` is auto-answered by a configured policy (`reject` declines every prompt, `allow` approves via the first allow-shaped option) — the first cut surfaces no prompt to a human. Proxying `fs`/`terminal` back to the parent (a shared-workspace mode) remains future work, as the seam Agent Note noted. ### No start-time capabilities @@ -52,4 +52,4 @@ Every run pays a fresh subprocess (spawn + `initialize` + `newSession`). The par ## Future providers -The same out-of-process spawn/prompt/stream/cancel shape generalizes to other transports named in the seam RFC — A2A, the Codex app-server, and the Claude Code Agent SDK — each a sibling provider registered by name. The ACP backend is the proof that the seam supports the boundary; those are mechanically similar. +The same out-of-process spawn/prompt/stream/cancel shape generalizes to other transports named in the seam Agent Note — A2A, the Codex app-server, and the Claude Code Agent SDK — each a sibling provider registered by name. The ACP backend is the proof that the seam supports the boundary; those are mechanically similar. diff --git a/docs/rfc/implemented/feature/2026-06-24-workspace-context.md b/.agents/notes/implemented/feature/2026-06-24-workspace-context.md similarity index 95% rename from docs/rfc/implemented/feature/2026-06-24-workspace-context.md rename to .agents/notes/implemented/feature/2026-06-24-workspace-context.md index cc11fd19ff..a3eec63b8f 100644 --- a/docs/rfc/implemented/feature/2026-06-24-workspace-context.md +++ b/.agents/notes/implemented/feature/2026-06-24-workspace-context.md @@ -1,4 +1,4 @@ -# RFC: Workspace context instruction files +# Agent Note: Workspace context instruction files Status: implemented @@ -40,7 +40,7 @@ After a successful first-party `read`, `write`, or `edit` call, the `tools/post- A content edit appends `Updated instructions from: <path>`, 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: <path>` 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 `<context source="...">` 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/docs/rfc/implemented/feature/2026-06-25-ask-user-question.md b/.agents/notes/implemented/feature/2026-06-25-ask-user-question.md similarity index 99% rename from docs/rfc/implemented/feature/2026-06-25-ask-user-question.md rename to .agents/notes/implemented/feature/2026-06-25-ask-user-question.md index 8320dc4aa5..4227341aec 100644 --- a/docs/rfc/implemented/feature/2026-06-25-ask-user-question.md +++ b/.agents/notes/implemented/feature/2026-06-25-ask-user-question.md @@ -1,4 +1,4 @@ -# RFC: Ask-user question capability +# Agent Note: Ask-user question capability Status: implemented diff --git a/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.md b/.agents/notes/implemented/feature/2026-06-29-todo-write-tool.md similarity index 98% rename from docs/rfc/implemented/feature/2026-06-29-todo-write-tool.md rename to .agents/notes/implemented/feature/2026-06-29-todo-write-tool.md index 127da5d09f..ab9421c2ed 100644 --- a/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.md +++ b/.agents/notes/implemented/feature/2026-06-29-todo-write-tool.md @@ -1,4 +1,4 @@ -# RFC: The `todo_write` tool — model task list as event-sourced session state +# Agent Note: The `todo_write` tool — model task list as event-sourced session state Status: implemented diff --git a/docs/rfc/implemented/feature/2026-06-30-hook-bridges.md b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md similarity index 88% rename from docs/rfc/implemented/feature/2026-06-30-hook-bridges.md rename to .agents/notes/implemented/feature/2026-06-30-hook-bridges.md index af5c7e928d..0c3c1e13ef 100644 --- a/docs/rfc/implemented/feature/2026-06-30-hook-bridges.md +++ b/.agents/notes/implemented/feature/2026-06-30-hook-bridges.md @@ -1,16 +1,16 @@ -# RFC: dsh-hooks-claude + dsh-hooks-codex — the Claude Code / Codex hook bridges +# Agent Note: dsh-hooks-claude + dsh-hooks-codex — the Claude Code / Codex hook bridges Status: implemented ## Problem -The harness's extension surface is its typed interception seams ([the interception-seams RFC](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/prompt-submit`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation`, `subagent/start`, `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This RFC introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib RFC](2026-06-30-hook-protocol-lib.md)). +The harness's extension surface is its typed interception seams ([the interception-seams Agent Note](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/prompt-submit`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-continuation`, `subagent/start`, `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)). The framing that shapes the whole design: **a bridge is a compatibility adapter, not a power tool.** Anything a bridge does (block a tool, inject context, force continuation, observe a subagent) a native cordis plugin does more powerfully — typed returns, full `ctx`, no serialization boundary. The bridge's reason to exist is to run the explicitly supported subset of external CC/Codex command hooks. That keeps each bridge thin: parse the config, pick a matcher mode, build the per-event payload, call `runHook` + `mergeHookOutputs` from the shared lib, and map the neutral outcome onto a seam Decision. The package READMEs own the exact current unsupported-event and partial-field inventory against the official protocols. ## Decision -Two independent plugins in the `packages/hooks/` group, each a function/namespace plugin (`name`/`inject`/`Config`/`apply`, NO default export — see [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md)) injecting only `bash`: +Two independent plugins in the `packages/hooks/` group, each a function/namespace plugin (`name`/`inject`/`Config`/`apply`, NO default export — see [postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)) injecting only `bash`: - **`dsh-hooks-claude`** — the CC dialect. Seven of Claude Code's current hook points: `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStart`, and `SubagentStop`. Owns CC-shaped per-event stdin payloads (a base of `session_id`/`transcript_path`/`cwd`/`hook_event_name` plus per-event fields), `CLAUDE_PROJECT_DIR` plus `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` substitution, and the literal-or-regex matcher mode. `transcript_path` is the persistence locator result or `''`; stdin carries a **trailing newline**. - **`dsh-hooks-codex`** — five of Codex's current hook points: `PreToolUse`, `PostToolUse`, `SessionStart`, `UserPromptSubmit`, and `Stop`. It uses an always-regex matcher, Codex-shaped snake_case payloads with `turn_id`/`model`/`permission_mode` extras written WITHOUT a trailing newline, no Codex plugin-env injection or config-time placeholder substitution, and no pre-tool approval or rewrite path. `transcript_path` is the same locator result or `null`; tool payloads carry the real `tool_name` in the reduced `tool_input: { command }` shape. @@ -53,7 +53,7 @@ Hooks run in the agent's session workspace, so relative paths target the user's ## Deferred compatibility gaps -- **Tool-input rewrite.** A CC/Codex `updatedInput` is logged + warned, not honored — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite RFC](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)), because the pre-execution args are read by `tool/call` audit + `assistant/message` history + ACP/tool-bash presentation, so an honest rewrite is a design unit, not a field. +- **Tool-input rewrite.** A CC/Codex `updatedInput` is logged + warned, not honored — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite Agent Note](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)), because the pre-execution args are read by `tool/call` audit + `assistant/message` history + ACP/tool-bash presentation, so an honest rewrite is a design unit, not a field. - **Stop loop-guard** (`TODO(stop-loop-guard)`). Claude Code supplies `stop_hook_active` and overrides a hook after eight consecutive blocks; Codex supplies `stop_hook_active` but documents no equivalent cap. Both bridges always report `false`, so a Stop hook that unconditionally blocks force-continues every step — a hook author must self-limit until state tracking lands. - **Hook `continue:false` (hard halt).** A hook can ask to halt the whole run (CC/Codex `continue:false`); the shared merge folds it into `MergedHookOutcome.stop`/`stopReason`, but no bridge acts on it (`TODO(hook-continue-false)`) — the interception seams have no "hard-halt the agent" primitive yet (a Decision blocks/steers a single point, not the run). Deferred with the loop-guard work; the halt request is recorded in the `hook/result` log, and the hook keeps its per-point effect (decision/context) meanwhile. - **Config discovery.** The path is explicit in `cordis.yml` and process-level (see above); the full multi-layer CC/Codex precedence walk, per-session project-local discovery, and the trust/hash model are not reimplemented (`TODO(per-session-hook-config)`). diff --git a/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.md b/.agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md similarity index 90% rename from docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.md rename to .agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md index ac28345791..bb1822504a 100644 --- a/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.md +++ b/.agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md @@ -1,4 +1,4 @@ -# RFC: dsh-hook-protocol — the shared Claude Code / Codex hook wire-protocol core +# Agent Note: dsh-hook-protocol — the shared Claude Code / Codex hook wire-protocol core Status: implemented @@ -6,7 +6,7 @@ Status: implemented The hooks subsystem ships two bridge plugins: one that runs a user's existing Claude Code (CC) hooks, one for Codex hooks. Studying the reference implementations (`~/repos/refs/claude-code`, `~/repos/refs/codex`) surfaced a decisive fact: **Codex deliberately reimplements a SUBSET of the CC hook protocol.** Its engine reads the same `hooks.json`, uses the same matcher-group shape, the same exit-code/structured-stdout output contract, and the same command-hook execution model — Codex's source even names the engine after Claude's and comments where it "intentionally diverges." So the two bridges would otherwise duplicate the bulk of the protocol. -This RFC introduces `@deepseek-ai/dsh-hook-protocol`, a **library** (not a plugin — it registers and injects nothing) holding the genuinely-identical primitives both bridges build on. The split between shared and per-dialect is the design's center of gravity. +This Agent Note introduces `@deepseek-ai/dsh-hook-protocol`, a **library** (not a plugin — it registers and injects nothing) holding the genuinely-identical primitives both bridges build on. The split between shared and per-dialect is the design's center of gravity. ## Decision @@ -15,7 +15,7 @@ A new `packages/hooks/` group with `hook-protocol` as a pure library. It owns fo **Shared (here):** - **Matcher** — `matchesMatcher(pattern, query, mode)`. The ONE axis the dialects differ on is collapsed to the `mode` parameter: `claude` treats a pure `[A-Za-z0-9_|]+` pattern as a literal (pipe = exact-match alternation) and anything else as a regex; `codex` is always an unanchored regex. Match-all on absent/`''`/`'*'`; an invalid regex matches nothing (never throws into the loop). - **Execution** — `runHook(bash, hook, options)`. Runs a command hook through the `ctx.bash` seam rather than a bespoke `spawn`: the executor already provides the scrubbed-but-overridable env, process-group kills, and timeout the protocol needs, and `dsh-bash`'s `stdin`/`env` fields (added for exactly this) are the trusted-plugin surface an in-process bridge is allowed to use. It serializes the bridge-built payload to stdin (trailing newline iff CC), honors the hook's `timeoutSec` (else `DEFAULT_HOOK_TIMEOUT_MS`, the 10-minute reference default both dialects share), and never throws (an executor rejection becomes a non-blocking-error `HookOutput`). -- **Decode** — `parseHookOutput(exit, stdout, stderr)`, the exit-code + structured-stdout codec, producing a dialect-neutral `HookOutput`. Exit `0` → lenient JSON parse of stdout; exit `2` → blocking error with `stderr` as the reason (surfaced as `decision: 'block'` so no caller needs a separate exit-code branch); other → non-blocking error. Parses the CC structured-stdout fields that have a consumer on some path (`continue`/`stopReason`/`decision`/`hookSpecificOutput.{permissionDecision,additionalContext,updatedInput}`/`systemMessage`); the bridge honors only the subset meaningful for its dialect. Fields with no consumer on any path are not parsed at all (CC's `suppressOutput` — hook stdout never enters a transcript here, so there is nothing to suppress; see [the tighten-hook-protocol-contract RFC](../simplification/2026-07-04-tighten-hook-protocol-contract.md)). +- **Decode** — `parseHookOutput(exit, stdout, stderr)`, the exit-code + structured-stdout codec, producing a dialect-neutral `HookOutput`. Exit `0` → lenient JSON parse of stdout; exit `2` → blocking error with `stderr` as the reason (surfaced as `decision: 'block'` so no caller needs a separate exit-code branch); other → non-blocking error. Parses the CC structured-stdout fields that have a consumer on some path (`continue`/`stopReason`/`decision`/`hookSpecificOutput.{permissionDecision,additionalContext,updatedInput}`/`systemMessage`); the bridge honors only the subset meaningful for its dialect. Fields with no consumer on any path are not parsed at all (CC's `suppressOutput` — hook stdout never enters a transcript here, so there is nothing to suppress; see [the tighten-hook-protocol-contract Agent Note](../simplification/2026-07-04-tighten-hook-protocol-contract.md)). - **Merge** — `mergeHookOutputs(outputs)`, folding multiple matched hooks into one most-restrictive `MergedHookOutcome`: permission precedence **deny > ask > allow**, halt sticky on the first `continue:false`, block reasons joined `\n\n`, context/system-messages accumulated in order. - **`hook/*` session events** — `hook/invoked` / `hook/result`, declaration-merged into `SessionEventMap` (log-only, like `compact/*` — NOT `SurfaceEventType`s), with `appendHookInvoked`/`appendHookResult` helpers so the invoked/result pairing and turn-enclosure stay consistent across bridges. `appendHookResult` also owns the durable record's semantics — the decision string (the hook's parsed decision, else `'stop'` on `continue:false`, else `'pass'`) and the 500-character `stderrSummary` truncation derive from the `HookOutput` here, not per-bridge. diff --git a/docs/rfc/implemented/feature/2026-06-30-interception-seams.md b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md similarity index 83% rename from docs/rfc/implemented/feature/2026-06-30-interception-seams.md rename to .agents/notes/implemented/feature/2026-06-30-interception-seams.md index 1356afa06b..2535c1c564 100644 --- a/docs/rfc/implemented/feature/2026-06-30-interception-seams.md +++ b/.agents/notes/implemented/feature/2026-06-30-interception-seams.md @@ -1,4 +1,4 @@ -# RFC: Interception seams — the typed-Decision surface a hook programs against +# Agent Note: Interception seams — the typed-Decision surface a hook programs against Status: implemented @@ -6,7 +6,7 @@ Status: implemented The harness needs a hooks subsystem: users extend or gate the agent at lifecycle points the way Claude Code (CC) and Codex do. The key reframe driving this design is that **"native hooks" are not a package** — a native hook is just an ordinary Cordis plugin subscribing to the canonical lifecycle events. So the real product is a *powerful, well-typed canonical event surface*; the CC/Codex bridges (the `dsh-hooks-claude` / `dsh-hooks-codex` packages) are merely translators that map an external shell-hook protocol onto that same surface. Anything a bridge can do, a plain plugin can do directly — more powerfully (no serialization boundary, full `ctx`, typed returns). -The surface needs distinct contracts for per-prompt policy (CC's `UserPromptSubmit`), session-start observation (CC's `SessionStart`), pre-tool policy, around-dispatch control, post-tool transformation, final-result observation, and continuation with a model-facing reason. Conflating those phases gives plugins mutation channels they do not need and makes finality depend on listener ordering. The [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md) supplies the three-domain rule and the typed-Decision idiom; this RFC applies them to the lifecycle seams. +The surface needs distinct contracts for per-prompt policy (CC's `UserPromptSubmit`), session-start observation (CC's `SessionStart`), pre-tool policy, around-dispatch control, post-tool transformation, final-result observation, and continuation with a model-facing reason. Conflating those phases gives plugins mutation channels they do not need and makes finality depend on listener ordering. The [event-domain-semantics Agent Note](../architecture/2026-06-30-event-domain-semantics.md) supplies the three-domain rule and the typed-Decision idiom; this Agent Note applies them to the lifecycle seams. ## Decision @@ -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. @@ -55,4 +55,4 @@ The seam package does **not** declare `hook/*` session events (the durable hook- ## Consequences -The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, prompt-submit, post-tool context buffering, and continuation; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../architecture.md), package READMEs, [core interception decisions](../../../core-data-structures/core.md#interception-decisions), and [tool structures](../../../core-data-structures/tools.md). The ACP bridge maps `rejected` turns to its `cancelled` codec value, while hook-driven snapshots verify the observable bridge behavior end to end. +The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, prompt-submit, post-tool context buffering, and continuation; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/core-data-structures/core.md#interception-decisions), and [tool structures](../../../../docs/core-data-structures/tools.md). The ACP bridge maps `rejected` turns to its `cancelled` codec value, while hook-driven snapshots verify the observable bridge behavior end to end. diff --git a/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.md b/.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md similarity index 83% rename from docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.md rename to .agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md index 8d8813563b..ee67af7d96 100644 --- a/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.md +++ b/.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md @@ -1,4 +1,4 @@ -# RFC: SessionStore fork API +# Agent Note: SessionStore fork API Status: implemented @@ -6,7 +6,7 @@ Status: implemented The event-sourced session log already has the primitive a fork needs: create a new session with a seed event prefix, then derive model history from that seeded log exactly as replay does. That primitive is intentionally low-level: `ctx.sessions.create(id, { seed, meta })` accepts any valid seed, but ordinary live-session branching needs policy around which prefix can be copied, which metadata is stamped on the child, and how errors are classified. -The semantic hazard is the fork boundary. A valid user-visible fork seed must be contiguous and turn-enclosed. Forking inside an active turn would copy an open `turn/start`, possibly an open `step/start`, and possibly dangling tool calls. That violates turn-enclosure and provider-transcript invariants, and it creates a misleading child history that appears to have participated in an unfinished parent turn. The existing [subagent seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) deliberately solves a different problem: tool-triggered subagent forks usually happen while the parent turn is open, so `dsh-subagent-fork` clips the seed to the parent's last completed-turn prefix. A general session fork should not silently clip; it should either fork the requested boundary or reject it. +The semantic hazard is the fork boundary. A valid user-visible fork seed must be contiguous and turn-enclosed. Forking inside an active turn would copy an open `turn/start`, possibly an open `step/start`, and possibly dangling tool calls. That violates turn-enclosure and provider-transcript invariants, and it creates a misleading child history that appears to have participated in an unfinished parent turn. The existing [subagent seam](2026-06-21-subagent-capability-seam.md) deliberately solves a different problem: tool-triggered subagent forks usually happen while the parent turn is open, so `dsh-subagent-fork` clips the seed to the parent's last completed-turn prefix. A general session fork should not silently clip; it should either fork the requested boundary or reject it. ## Decision @@ -38,4 +38,4 @@ An empty prefix is forkable; any non-empty boundary must be a safe existing sequ The public surface stays small and discoverable: live session branching is part of `ctx.sessions`, next to `create({ seed })`, rather than a standalone service or a two-step helper pair. Persistence continues to work through existing `session/created` and `session/flush` behavior: a forked child starts life with seeded events, so existing backends persist that seed once and preserve `parentSession` / `seedLength` in the header. -The v1 scope still excludes ACP `session/fork`, unloaded persisted-session forking, model-facing tools, and subagent refactors. If a future ACP method is added, it should advertise the capability only after it has transcript/snapshot coverage; this RFC adds no editor-facing updates, so no ACP snapshot is required now. Fork-child replay remains covered by the existing [seed-boundary testing RFC](../../implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md), while this API gets focused `dsh-session` unit tests plus JSONL persistence coverage. +The v1 scope still excludes ACP `session/fork`, unloaded persisted-session forking, model-facing tools, and subagent refactors. If a future ACP method is added, it should advertise the capability only after it has transcript/snapshot coverage; this Agent Note adds no editor-facing updates, so no ACP snapshot is required now. Fork-child replay remains covered by the existing [seed-boundary testing Agent Note](../testing/2026-06-22-fork-child-replay-seed-boundary.md), while this API gets focused `dsh-session` unit tests plus JSONL persistence coverage. diff --git a/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md b/.agents/notes/implemented/feature/2026-06-30-subagent-observe-enrich.md similarity index 57% rename from docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md rename to .agents/notes/implemented/feature/2026-06-30-subagent-observe-enrich.md index aa853edd24..861779fc50 100644 --- a/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.md +++ b/.agents/notes/implemented/feature/2026-06-30-subagent-observe-enrich.md @@ -1,12 +1,12 @@ -# RFC: Subagent lifecycle enrichment — lastAssistantMessage (observe-only) +# Agent Note: Subagent lifecycle enrichment — lastAssistantMessage (observe-only) Status: implemented ## Problem -The hooks subsystem ([interception seams RFC](2026-06-30-interception-seams.md)) lets a plugin observe and gate the agent at lifecycle points. Claude Code and Codex both expose **SubagentStart / SubagentStop** hooks, and CC's carry the subagent's final message. The harness already emits `subagent/start` and `subagent/end` lifecycle events ([the subagent capability-seam](2026-06-21-subagent-capability-seam.md)), but their payloads were minimal (`provider`, `id`, and on end `stopReason`) — not enough for a hooks bridge to report WHAT a subagent produced without separately reaching for the live run. +The hooks subsystem ([interception seams Agent Note](2026-06-30-interception-seams.md)) lets a plugin observe and gate the agent at lifecycle points. Claude Code and Codex both expose **SubagentStart / SubagentStop** hooks, and CC's carry the subagent's final message. The harness already emits `subagent/start` and `subagent/end` lifecycle events ([the subagent capability-seam](2026-06-21-subagent-capability-seam.md)), but their payloads were minimal (`provider`, `id`, and on end `stopReason`) — not enough for a hooks bridge to report WHAT a subagent produced without separately reaching for the live run. -This RFC enriches the end payload. It is deliberately **observe-only**: no control-flow change and no waterfall. A run-affecting subagent-stop decision (continuation, injection that changes the run) is a separate, larger redesign and stays out of scope. +This Agent Note enriches the end payload. It is deliberately **observe-only**: no control-flow change and no waterfall. A run-affecting subagent-stop decision (continuation, injection that changes the run) is a separate, larger redesign and stays out of scope. ## Decision @@ -16,14 +16,14 @@ Both events stay plain **`emit`s**. Async `SubagentService.start()` attaches res ## Alternatives considered -**An `agentType` subagent-kind label** (the harness analogue of CC's `subagent_type`) on the request + both lifecycle payloads — an earlier draft shipped it; dropped in review because it is a Claude-Code concept that does not fit our own seam (nothing here interprets it, and the only consumer was a CC-dialect bridge). The CC bridge instead feeds Claude Code's own default matcher value `"general-purpose"` for its SubagentStart/Stop `agent_type` matcher, so this RFC ships ONE enrichment: `lastAssistantMessage`. +**An `agentType` subagent-kind label** (the harness analogue of CC's `subagent_type`) on the request + both lifecycle payloads — an earlier draft shipped it; dropped in review because it is a Claude-Code concept that does not fit our own seam (nothing here interprets it, and the only consumer was a CC-dialect bridge). The CC bridge instead feeds Claude Code's own default matcher value `"general-purpose"` for its SubagentStart/Stop `agent_type` matcher, so this Agent Note ships ONE enrichment: `lastAssistantMessage`. **A control-flow `subagent/end`** — deferred; see below. ## Why observe-only, and what is deferred -A control-flow `subagent/end` (an awaited waterfall returning a stop/continue decision, like the other interception seams) would require: reshaping `subagent/end` from emit to waterfall, restructuring `SubagentService.start` to await listeners before settling, and implementing the `resume` capability in the in-process provider so a "continue" can actually re-run the child. That belongs to the background/steering subagent redesign the [capability-seam RFC](2026-06-21-subagent-capability-seam.md) already defers (the same redesign that unifies long-running-tool handling across subagents and bash). This RFC ships the observe-only enrichment a hooks bridge needs today; `FIXME(subagent-continuation)` / `TODO` anchors mark where the control-flow version would land if and when that redesign happens. +A control-flow `subagent/end` (an awaited waterfall returning a stop/continue decision, like the other interception seams) would require: reshaping `subagent/end` from emit to waterfall, restructuring `SubagentService.start` to await listeners before settling, and implementing the `resume` capability in the in-process provider so a "continue" can actually re-run the child. That belongs to the background/steering subagent redesign the [capability-seam Agent Note](2026-06-21-subagent-capability-seam.md) already defers (the same redesign that unifies long-running-tool handling across subagents and bash). This Agent Note ships the observe-only enrichment a hooks bridge needs today; `FIXME(subagent-continuation)` / `TODO` anchors mark where the control-flow version would land if and when that redesign happens. ## Consequences -A hooks bridge (or a native plugin) can now forward the child's `lastAssistantMessage` to a SubagentStop handler by subscribing to the existing emits — no new control-flow surface. The vocabulary addition is documented in [docs/core-data-structures/subagent.md](../../../core-data-structures/subagent.md) (the events prose) and the two subagent READMEs; the catalog is regenerated. No production behavior changes — the events fire exactly as before, with one more (optional) field on the end payload — so no snapshot or e2e change is needed. +A hooks bridge (or a native plugin) can now forward the child's `lastAssistantMessage` to a SubagentStop handler by subscribing to the existing emits — no new control-flow surface. The vocabulary addition is documented in [docs/core-data-structures/subagent.md](../../../../docs/core-data-structures/subagent.md) (the events prose) and the two subagent READMEs; the catalog is regenerated. No production behavior changes — the events fire exactly as before, with one more (optional) field on the end payload — so no snapshot or e2e change is needed. diff --git a/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md b/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md similarity index 96% rename from docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md rename to .agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md index 6302584103..599c895ae2 100644 --- a/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md +++ b/.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md @@ -1,4 +1,4 @@ -# RFC: Dynamic workflows — a script-driven multi-agent orchestration seam +# Agent Note: Dynamic workflows — a script-driven multi-agent orchestration seam Status: implemented @@ -18,7 +18,7 @@ One deliberate strictness DIVERGENCE from CC: hook misuse — unknown or deferre ### The seam (dsh-workflow) -`ctx.workflows` is an abstract `WorkflowService` in the bash shape — one engine per context, no named-provider registry (engines are deployment swaps, not co-residents). `start(request)` throws synchronously for a script that cannot begin; a returned `WorkflowRun`'s `result` NEVER rejects (failures resolve as `stopReason: 'error' | 'cancelled'`). The `workflow/*` events are observe-only emits carrying DATA SNAPSHOTS (id + meta; `workflow/end` omits the result value), per-listener contained, mirroring `subagent/start`/`subagent/end` — control stays with the run's holder. Vocabulary details: [core-data-structures/workflow.md](../../../core-data-structures/workflow.md). +`ctx.workflows` is an abstract `WorkflowService` in the bash shape — one engine per context, no named-provider registry (engines are deployment swaps, not co-residents). `start(request)` throws synchronously for a script that cannot begin; a returned `WorkflowRun`'s `result` NEVER rejects (failures resolve as `stopReason: 'error' | 'cancelled'`). The `workflow/*` events are observe-only emits carrying DATA SNAPSHOTS (id + meta; `workflow/end` omits the result value), per-listener contained, mirroring `subagent/start`/`subagent/end` — control stays with the run's holder. Vocabulary details: [core-data-structures/workflow.md](../../../../docs/core-data-structures/workflow.md). ### The engine (dsh-workflow-workerthread): one worker thread per run @@ -26,7 +26,7 @@ One deliberate strictness DIVERGENCE from CC: hook misuse — unknown or deferre **Why `node:worker_threads`**: each run gets one unpooled worker. A vm context limits the documented script surface, while message-port RPC bridges `agent()` to host-side child loops. The worker prevents synchronous script work from blocking the host, provides a serialization boundary, and permits forced termination after cancellation. `isolated-vm` was rejected because of its maintenance state and deployment requirements. -The host validates metadata and parses the body before publication. Private enum-keyed payload maps define the wire protocol; pending starts, published child records, one cancellation signal, worker-death reaping, result precedence, and disposal quiescence preserve the subagent run contract across it. The [agent-scope runtime-design RFC](../architecture/2026-07-12-agent-scope-runtime-design.md#workflow-children-are-pending-starts-or-published-records) owns those race algorithms. +The host validates metadata and parses the body before publication. Private enum-keyed payload maps define the wire protocol; pending starts, published child records, one cancellation signal, worker-death reaping, result precedence, and disposal quiescence preserve the subagent run contract across it. The [agent-scope runtime-design Agent Note](../architecture/2026-07-12-agent-scope-runtime-design.md#workflow-children-are-pending-starts-or-published-records) owns those race algorithms. The engine exposes an in-process `MessageChannel` test path because main-process V8 coverage cannot see worker execution. @@ -44,7 +44,7 @@ A `workflow` tool mirroring `dsh-tool-subagent`'s synchronous shape: start, awai An output schema makes a schema-valid committed capture mandatory for successful child completion. The scoped runtime presents the capture tool and instruction, commits only a successful final outcome—including the enclosing `run_code` outcome for an SDK call—denies later side effects after capture becomes pending, and stops the child without another model step after commit. A validation failure remains a retryable tool error; clean completion without a committed capture settles as an error. -`StructuredOutputSchema` is the raw enforceable JSON-Schema subset in `dsh-tools` (single-string `type`, `properties`/`required`/`additionalProperties`, `items`, scalar `enum`/`const`), and unsupported keywords fail loudly because that wire data becomes the capture tool's parameters verbatim. The [agent-scope runtime-design RFC](../architecture/2026-07-12-agent-scope-runtime-design.md#structured-output-commits-only-authoritative-outcomes) owns the assembly, commit, guard, and terminal-stop correctness algorithms. +`StructuredOutputSchema` is the raw enforceable JSON-Schema subset in `dsh-tools` (single-string `type`, `properties`/`required`/`additionalProperties`, `items`, scalar `enum`/`const`), and unsupported keywords fail loudly because that wire data becomes the capture tool's parameters verbatim. The [agent-scope runtime-design Agent Note](../architecture/2026-07-12-agent-scope-runtime-design.md#structured-output-commits-only-authoritative-outcomes) owns the assembly, commit, guard, and terminal-stop correctness algorithms. ## Testing diff --git a/docs/rfc/implemented/feature/2026-07-05-skill-system.md b/.agents/notes/implemented/feature/2026-07-05-skill-system.md similarity index 96% rename from docs/rfc/implemented/feature/2026-07-05-skill-system.md rename to .agents/notes/implemented/feature/2026-07-05-skill-system.md index 0b74aa00ae..e59013c0a8 100644 --- a/docs/rfc/implemented/feature/2026-07-05-skill-system.md +++ b/.agents/notes/implemented/feature/2026-07-05-skill-system.md @@ -1,4 +1,4 @@ -# RFC: Skill system — progressive disclosure instructions for agents +# Agent Note: Skill system — progressive disclosure instructions for agents Status: implemented @@ -24,7 +24,7 @@ Local skill filesystem I/O goes through `ctx.fs` when a filesystem service is lo The `skill({ name })` tool loads one full skill for the current agent cwd and returns a tool result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`. `resourceBase` supplies a directory, URL, or opaque provider-managed base for explicitly referenced scripts, references, and assets; resources load only as needed, without directory enumeration. An unresolved name reports that the skill is unknown or no longer available; invalid names and skills marked `disableModelInvocation` retain distinct tool errors. The tool result is the model-visible disclosure path. -The data structures and catalog/tool contract are documented in [skills.md](../../../core-data-structures/skills.md), with service signatures in the generated [services catalog](../../../cordis-catalog/services.md). +The data structures and catalog/tool contract are documented in [skills.md](../../../../docs/core-data-structures/skills.md), with service signatures in the generated [services catalog](../../../../docs/cordis-catalog/services.md). ## Alternatives considered diff --git a/docs/rfc/implemented/feature/2026-07-06-approval-seam.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md similarity index 90% rename from docs/rfc/implemented/feature/2026-07-06-approval-seam.md rename to .agents/notes/implemented/feature/2026-07-06-approval-seam.md index 81e60a726f..af063895e2 100644 --- a/docs/rfc/implemented/feature/2026-07-06-approval-seam.md +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md @@ -1,10 +1,10 @@ -# RFC: The approval seam — one-shot permission decisions over a waterfall of answerers +# Agent Note: The approval seam — one-shot permission decisions over a waterfall of answerers Status: implemented ## Problem -Two callers need to put one question — "may this specific action proceed?" — to a human: `tools/pre-execute`'s `ask` decision (including the Claude-Code hook bridge's `permissionDecision: ask`) and the [sandbox RFC](2026-07-06-sandbox.md)'s post-denial one-shot escalation retry. A shared seam keeps them from inventing separate outcome vocabularies, UI routing, cancellation, and audit trails, while guaranteeing that a deployment with no UI can never grant an unanswerable request. +Two callers need to put one question — "may this specific action proceed?" — to a human: `tools/pre-execute`'s `ask` decision (including the Claude-Code hook bridge's `permissionDecision: ask`) and the [sandbox Agent Note](2026-07-06-sandbox.md)'s post-denial one-shot escalation retry. A shared seam keeps them from inventing separate outcome vocabularies, UI routing, cancellation, and audit trails, while guaranteeing that a deployment with no UI can never grant an unanswerable request. The routing problem is ownership: an approval prompt must reach the editor session that owns the asking agent (the ACP bridge multiplexes N sessions over one connection), fail closed for agents nobody owns (in-process subagents, tests), and stay out of deployments that compose no UI (headless, CI). @@ -61,13 +61,13 @@ Answerers are `approval/request` waterfall listeners. Zero listeners fall throug #### The per-session policy tier -The seam also owns the session-scoped `'ask' | 'never'` policy described by [the sandbox RFC](2026-07-06-sandbox.md). Effective policy is folded from logged switches over the deployment default. `'never'` resolves to `rejected` inside `request()` before any answerer can run; `'ask'` dispatches and otherwise falls through to `unavailable`. The prompt states only deterministic `'never'`, switch narration is coalesced, and every request still records the audit pair. +The seam also owns the session-scoped `'ask' | 'never'` policy described by [the sandbox Agent Note](2026-07-06-sandbox.md). Effective policy is folded from logged switches over the deployment default. `'never'` resolves to `rejected` inside `request()` before any answerer can run; `'ask'` dispatches and otherwise falls through to `unavailable`. The prompt states only deterministic `'never'`, switch narration is coalesced, and every request still records the audit pair. #### The ACP answerer The ACP bridge answers only for an exact agent object owned by its forward session map. It attaches `session/request_permission` to the existing `callId`, advertises one-shot allow/reject options, maps cancellation separately, and never grants an unknown option. Foreign or call-less requests delegate; a failed client RPC becomes `unavailable`. Hooks and `tools/pre-execute` decide whether a call asks at all. -The answerer routes through the bridge's exact-agent ownership check described by [the ACP support RFC](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md), implementing the per-session permission ownership required by [the multi-session RFC](../../implemented/feature/2026-06-14-acp-multi-session.md). +The answerer routes through the bridge's exact-agent ownership check described by [the ACP support Agent Note](2026-06-14-acp-agent-client-protocol.md), implementing the per-session permission ownership required by [the multi-session Agent Note](2026-06-14-acp-multi-session.md). #### Audit, and what the model sees @@ -85,7 +85,7 @@ Snapshots record allowed and rejected sandbox escalation through `session/reques ## Deferred -- **`allow_always` grant storage** — honoring a persistent grant means designing storage, scope identity (call? path? prefix? session? time window?), and revocation; until designed, only the one-shot options are advertised ([the sandbox RFC](2026-07-06-sandbox.md) § Escalation records the open scope question). +- **`allow_always` grant storage** — honoring a persistent grant means designing storage, scope identity (call? path? prefix? session? time window?), and revocation; until designed, only the one-shot options are advertised ([the sandbox Agent Note](2026-07-06-sandbox.md) § Escalation records the open scope question). - **A recorded hook-driven `ask` through a composed answerer** — the human-prompt wire is recorded through the sandbox example's escalation branches. The hook matrix's `hook-cc-pretool-ask` pins the no-ApprovalService fallback denial, while the hook-producer-plus-answerer composition remains on the unit tier. - **Routing a child agent's approvals to the parent session** — `subagent-acp`'s child auto-answers its own `permission` requests; surfacing them to the parent's editor is its own design. @@ -131,7 +131,7 @@ Costs and accepted limits: In-repo precedents this design copies or contrasts with: - The `fs/write-intent` gate (`packages/fs/fs/`) — the documented single-occupancy decision-slot waterfall semantics (first answer wins, delegate via `next()`) the answerer contract reuses. -- `hook/invoked`/`hook/result` — the log-only audit-pair precedent `approval/asked`/`approval/decided` follows; [the hook-bridges RFC](2026-06-30-hook-bridges.md) ships `permissionDecision: ask`, the first producer. -- [The interception-seams RFC](2026-06-30-interception-seams.md) — the `tools/pre-execute` `allow`/`deny`/`ask` vocabulary whose `ask` this seam services. -- [The ACP support RFC](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md) — the exact-agent ownership check against the forward session map that the answerer routes through; [the multi-session RFC](../../implemented/feature/2026-06-14-acp-multi-session.md) — the per-session permission-ownership blocker this implements. +- `hook/invoked`/`hook/result` — the log-only audit-pair precedent `approval/asked`/`approval/decided` follows; [the hook-bridges Agent Note](2026-06-30-hook-bridges.md) ships `permissionDecision: ask`, the first producer. +- [The interception-seams Agent Note](2026-06-30-interception-seams.md) — the `tools/pre-execute` `allow`/`deny`/`ask` vocabulary whose `ask` this seam services. +- [The ACP support Agent Note](2026-06-14-acp-agent-client-protocol.md) — the exact-agent ownership check against the forward session map that the answerer routes through; [the multi-session Agent Note](2026-06-14-acp-multi-session.md) — the per-session permission-ownership blocker this implements. - The opportunistic `ctx.get()` consumption pattern (`tool-bash`'s owner-token lookup, the loop's persistence probe) — how `dsh-tools` consumes the seam without gating its fiber on it. diff --git a/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md b/.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md similarity index 96% rename from docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md rename to .agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md index e841de8973..d46579dee1 100644 --- a/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md +++ b/.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md @@ -1,4 +1,4 @@ -# RFC: Explicit model-facing tool order +# Agent Note: Explicit model-facing tool order Status: implemented @@ -32,7 +32,7 @@ Config plumbing follows the `persona` precedent, and `toolOrder` sits beside it: - **A `LlmService` config + `orderTools()` method the loop calls before logging the header** — works, but adds a public service method and a loop edit solely to apply a policy at a distance; every future request composer must remember the call. Canonicalizing where the list is born makes an unordered list unrepresentable, with zero new surface. - **Normalizing inside `llm.stream()`** — runs after the header event is logged (the flake survives) and rebuilds the deep-frozen envelope, silently disarming the reconstruction invariant. - **An exhaustive list (no rest entry)** — every newly loaded tool plugin would break boot; the mandatory rest entry keeps unlisted tools deterministic and their position explicit. -- **A boot-time validation pass (a `SystemPrompt.assertToolOrderSatisfied()` called by `dsh-app-boot` after `loader.await()`)** — would turn the misconfiguration into a startup death instead of a first-turn failure, but costs a public service method plus a structural coupling from the generic boot glue to one service, and cannot replace the assembly-time check anyway (embedded callers never run app boot; registrations change after boot). No existing event can host the check either: cordis v4 has no ready-like event, `loader/entry-init`/`internal/status` fire mid-load (racy against tool registration, the very entropy this RFC kills), and the agent lifecycle events are no earlier than the assembly. One enforcement point at `assemble()` was judged worth the later failure moment. +- **A boot-time validation pass (a `SystemPrompt.assertToolOrderSatisfied()` called by `dsh-app-boot` after `loader.await()`)** — would turn the misconfiguration into a startup death instead of a first-turn failure, but costs a public service method plus a structural coupling from the generic boot glue to one service, and cannot replace the assembly-time check anyway (embedded callers never run app boot; registrations change after boot). No existing event can host the check either: cordis v4 has no ready-like event, `loader/entry-init`/`internal/status` fire mid-load (racy against tool registration, the very entropy this Agent Note kills), and the agent lifecycle events are no earlier than the assembly. One enforcement point at `assemble()` was judged worth the later failure moment. ## Consequences diff --git a/docs/rfc/implemented/feature/2026-07-06-sandbox.md b/.agents/notes/implemented/feature/2026-07-06-sandbox.md similarity index 88% rename from docs/rfc/implemented/feature/2026-07-06-sandbox.md rename to .agents/notes/implemented/feature/2026-07-06-sandbox.md index e236154cdc..2cc6516523 100644 --- a/docs/rfc/implemented/feature/2026-07-06-sandbox.md +++ b/.agents/notes/implemented/feature/2026-07-06-sandbox.md @@ -1,4 +1,4 @@ -# RFC: The subprocess sandbox — confinement seam, native runners, escalation, and per-session modes +# Agent Note: The subprocess sandbox — confinement seam, native runners, escalation, and per-session modes Status: implemented @@ -12,7 +12,7 @@ Confinement alone leaves two gaps. A denial with no escalation path is terminal ## Decision -One seam, one per-platform chain of local backends, one consumer, and two levers on top: a per-call escalation path and per-session runtime modes. Everything below composes from the leaf `cordis.yml`; nothing touches `agent-loop`. The scope is deliberately bounded: the phases this RFC names but does not design — per-session workspace root, cross-family fs enforcement, the `subagent-acp` consumer, more environments, a Windows chain — are listed under § Deferred phases, each a follow-up design, not a config knob. +One seam, one per-platform chain of local backends, one consumer, and two levers on top: a per-call escalation path and per-session runtime modes. Everything below composes from the leaf `cordis.yml`; nothing touches `agent-loop`. The scope is deliberately bounded: the phases this Agent Note names but does not design — per-session workspace root, cross-family fs enforcement, the `subagent-acp` consumer, more environments, a Windows chain — are listed under § Deferred phases, each a follow-up design, not a config knob. ### How a deployment uses it @@ -27,7 +27,7 @@ Four `cordis.yml` entries turn an unconfined coding agent into the sandboxed pro mode: workspace-write # the deployment default every session starts from workspaceRoot: !!js process.cwd() # the boundary workspace-write may write under - id: approval - name: '@deepseek-ai/dsh-user-approval' # the escalation gate's channel (the approval RFC) + name: '@deepseek-ai/dsh-user-approval' # the escalation gate's channel (the approval Agent Note) config: policy: ask - id: permission @@ -62,9 +62,7 @@ Left open, for the phase that needs them: whether network restriction arrives as The launcher is a ~300-line C program (plain C11 over the raw Landlock UAPI — no libraries beyond a statically linked musl, so the audit surface is that one file plus the kernel's stable syscall contract): `--ro <path>` / `--rw <path>` grants, `--`, the wrapped argv; it installs the ruleset on itself and `exec`s (rulesets are inherited across `execve`, and it sets `no_new_privs` before restricting); `--probe` enforces a maximal ruleset in a short-lived child and exits 0 only when the kernel actually enforces; launcher failures exit 125 without exec'ing. -The Landlock launcher ships through [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run), with platform binaries selected by npm. That package owns path resolution, probing, and CLI flags; the harness maps sandbox modes to grants. Versioning the entry point with its binaries keeps probe parsing and launch syntax aligned. - -FIXME: Revisit the separate-repository boundary and try to maintain the launcher source and its platform package family inside this monorepo, so the native release surface and harness contract evolve together. +The Landlock launcher source and package workspace live at `native/landlock-run`, next to the harness consumers. The standalone [`node-addon-landlock-run`](https://github.com/deepseek-harness/node-addon-landlock-run) repository is the release mirror used to pack and publish the npm package family; `native/README.md` owns the export procedure. Platform binaries are selected by npm, and the entry package owns path resolution, probing, and CLI flags while the harness maps sandbox modes to grants. Versioning the entry point with its binaries keeps probe parsing and launch syntax aligned. Backend profiles share the mode contract but differ in necessary host grants. Landlock and Seatbelt allow only `/dev/null` in read-only mode; workspace-write also permits their required host temp roots. Each wrap carries backend-specific denial signatures. Landlock reports partial enforcement on older ABIs that cannot govern every operation, while successful bwrap and Seatbelt profiles report full enforcement. @@ -98,12 +96,12 @@ 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' } } ``` -Each owner exports the same three-piece kit: the event declaration, a pure fold (`effectiveSandboxMode(events)` / `effectiveApprovalPolicy(events)` — a `findLast`, typed to the domain's closed union), and THE write path (`setSandboxMode(session, mode)` / `setApprovalPolicy(session, policy)` — a switch IS its event; nothing mutates state out of band). No shared owner service, no generic facts map, no registry: a third knob copies the ~40-line pattern into its own package. Execution follows the fold on both sides — the bash tool's per-call stamp reads it as the middle rung of the § Escalation precedence chain, and the approval seam's `'never'` gate is [the approval RFC](2026-07-06-approval-seam.md)'s side of the same pattern. +Each owner exports the same three-piece kit: the event declaration, a pure fold (`effectiveSandboxMode(events)` / `effectiveApprovalPolicy(events)` — a `findLast`, typed to the domain's closed union), and THE write path (`setSandboxMode(session, mode)` / `setApprovalPolicy(session, policy)` — a switch IS its event; nothing mutates state out of band). No shared owner service, no generic facts map, no registry: a third knob copies the ~40-line pattern into its own package. Execution follows the fold on both sides — the bash tool's per-call stamp reads it as the middle rung of the § Escalation precedence chain, and the approval seam's `'never'` gate is [the approval Agent Note](2026-07-06-approval-seam.md)'s side of the same pattern. Sandbox mode is not narrated in the prompt; denial results report the mode when it matters, avoiding preemptive refusal based on a standing label. Approval policy is different: only `'never'` is stated because automatic rejection otherwise looks like a user decision. Policy-change notices are coalesced and delivered by the next pre-step, with log-derived fallback after restart. The notice source is inferred from event position: a knob event after the last request header is user-driven; unlogged drift is operator or config driven. @@ -113,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 @@ -128,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. @@ -173,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. @@ -193,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`. @@ -203,8 +198,8 @@ Costs and accepted limits: In-repo precedents this design copies or contrasts with: -- [The capability-seams RFC](../architecture/2026-06-13-capability-seams.md) — the interface/implementation/consumer split and the "don't split preemptively" timing rule the second consumer satisfied. -- The `dsh-bash` request/spec split ([the bash vocabulary catalog](../../../core-data-structures/bash.md)) — the per-call carrier template `sandboxMode` rides, and the explicit-`resolve()` defaulting convention. -- [The approval seam RFC](2026-07-06-approval-seam.md) — the channel escalation asks through; its answerer waterfall, audit pair, and one-package rationale are recorded there. +- [The capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md) — the interface/implementation/consumer split and the "don't split preemptively" timing rule the second consumer satisfied. +- The `dsh-bash` request/spec split ([the bash vocabulary catalog](../../../../docs/core-data-structures/bash.md)) — the per-call carrier template `sandboxMode` rides, and the explicit-`resolve()` defaulting convention. +- [The approval seam Agent Note](2026-07-06-approval-seam.md) — the channel escalation asks through; its answerer waterfall, audit pair, and one-package rationale are recorded there. - [Event-sourced sessions](../architecture/2026-06-11-event-sourced-sessions.md) and [the turn-enclosure invariant](../architecture/2026-06-15-turn-enclosure-invariant.md) — the log-as-store foundation the per-session modes fold over, and the commit boundary the anchoring design obeys. -- [The interception-seams RFC](2026-06-30-interception-seams.md) — the `tools/pre-execute` vocabulary the escalation gate deliberately does not reuse (an escalating call has no pre-execute moment of its own). +- [The interception-seams Agent Note](2026-06-30-interception-seams.md) — the `tools/pre-execute` vocabulary the escalation gate deliberately does not reuse (an escalating call has no pre-execute moment of its own). diff --git a/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.md b/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md similarity index 98% rename from docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.md rename to .agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md index 27c108f0ce..95cad58b46 100644 --- a/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.md +++ b/.agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md @@ -1,4 +1,4 @@ -# RFC: MCP client plugin — connect to external MCP servers and bridge their tools +# Agent Note: MCP client plugin — connect to external MCP servers and bridge their tools Status: implemented @@ -12,7 +12,7 @@ The `ToolRegistry` already accepts raw JSON Schema tool definitions (documented ### Package -A single package `@deepseek-ai/dsh-mcp-client` at `packages/mcp/mcp-client/`. No capability-seam three-package split — there is no foreseeable second MCP client implementation, and the convention is "don't split preemptively" ([capability seams RFC](../../implemented/architecture/2026-06-13-capability-seams.md)). +A single package `@deepseek-ai/dsh-mcp-client` at `packages/mcp/mcp-client/`. No capability-seam three-package split — there is no foreseeable second MCP client implementation, and the convention is "don't split preemptively" ([capability seams Agent Note](../architecture/2026-06-13-capability-seams.md)). ### SDK @@ -141,7 +141,7 @@ A unified `execute` handler for all tools from one MCP server: 1. Resolve `rawName` (the executor closes over it) and call `client.callTool({ name: rawName, arguments }, { signal: exec.signal })` with the configured timeout — the public name is never sent to the server. 2. Map the result: - Multiple `text` content blocks → join with `'\n'` into a single `TextBlock` (required: `flattenText` uses `join('')` without separator, so multiple blocks would lose inter-block boundaries). - - `image` content blocks → discard with a `ctx.logger.warn` (the harness has no image content block type; [drop-image RFC](../../implemented/simplification/2026-07-04-drop-image-content-block.md)). + - `image` content blocks → discard with a `ctx.logger.warn` (the harness has no image content block type; [drop-image Agent Note](../simplification/2026-07-04-drop-image-content-block.md)). - `isError: true` → map to the harness `isError` result path (`{ content: [...], isError: true }`). 3. Cancellation: `exec.signal` (from the agent loop's cancel) is passed through to the MCP SDK's `callTool`, which sends `$/cancelRequest` to the server. diff --git a/docs/rfc/implemented/feature/2026-07-07-session-prefix.md b/.agents/notes/implemented/feature/2026-07-07-session-prefix.md similarity index 87% rename from docs/rfc/implemented/feature/2026-07-07-session-prefix.md rename to .agents/notes/implemented/feature/2026-07-07-session-prefix.md index 0c642e7c68..c2f58dac0c 100644 --- a/docs/rfc/implemented/feature/2026-07-07-session-prefix.md +++ b/.agents/notes/implemented/feature/2026-07-07-session-prefix.md @@ -1,4 +1,4 @@ -# RFC: The session prefix — request-only messages in front of the derived history +# Agent Note: The session prefix — request-only messages in front of the derived history Status: implemented @@ -6,16 +6,16 @@ Status: implemented A plugin often owns a session-stable opener the model must always see — a skills catalog, an AGENTS.md digest, a workspace baseline. Before this seam the harness offered two homes, and both are wrong for that content. The system prompt is one rendered string: message-shaped content (a user-role `<system-reminder>` envelope, a multi-message primer) does not fit it, and providers weight conversation messages differently from system text. Durable history (`agent.inject()`, a `context/message` at session start) makes the opener permanent: every `deriveMessages()` consumer replays it, the compaction retention walk owns it, forks bake it in stale, and a resume cannot refresh it — a catalog captured at session birth outlives the world it described. -The obvious third option — let a plugin edit the request's `messages` on the way out — is banned by [the reconstructable-requests RFC](../architecture/2026-07-05-reconstructable-requests.md): every loop-built request is a pure function of the session log, so whatever channel carries the opener must log exactly what it sends. What was missing was a request-only message channel with a durable record. +The obvious third option — let a plugin edit the request's `messages` on the way out — is banned by [the reconstructable-requests Agent Note](../architecture/2026-07-05-reconstructable-requests.md): every loop-built request is a pure function of the session log, so whatever channel carries the opener must log exactly what it sends. What was missing was a request-only message channel with a durable record. ## Decision -`agent/session-prefix` is a waterfall on the agent event map ([`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)): listeners receive a frozen empty seed and return an extension (the canonical contribution is a prepend, `[mine, ...await next()]`, which yields registration order on the wire). The loop ([`packages/core/agent-loop/src/loop.ts`](../../../../packages/core/agent-loop/src/loop.ts)) fires it once per loop instance, lazily before the instance's first `agent/pre-step`; the composed list is deep-cloned, deep-frozen, cached on the instance, and placed in front of the ENTIRE derived history — directly after the provider's system slot — on every request the instance sends ([wire order](../../../core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header)). +`agent/session-prefix` is a waterfall on the agent event map ([`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)): listeners receive a frozen empty seed and return an extension (the canonical contribution is a prepend, `[mine, ...await next()]`, which yields registration order on the wire). The loop ([`packages/core/agent-loop/src/loop.ts`](../../../../packages/core/agent-loop/src/loop.ts)) fires it once per loop instance, lazily before the instance's first `agent/pre-step`; the composed list is deep-cloned, deep-frozen, cached on the instance, and placed in front of the ENTIRE derived history — directly after the provider's system slot — on every request the instance sends ([wire order](../../../../docs/core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header)). Three properties carry the design: -- **Request-only, header-logged.** `deriveMessages()` never returns the prefix; its one durable record is `EpochHeader.messagePrefix` on the instance's anchoring `request/header` snapshot — the channel the reconstructable-requests RFC already owns for the request's non-history half, so no new session event exists. The dev invariant ([dsh-invariants](../../../../packages/support/invariants/src/index.ts)) recomputes `messagePrefix + boundary derivation` against every loop-built request; an unlogged prefix cannot reach the wire. -- **Frozen per instance.** Reuse is structural, not disciplined: the cached product cannot change mid-session, so the provider's prompt cache holds by construction and the prefix extends the cacheable region at zero marginal cost per step. A process restart or `ctx.agents.resume()` is a new instance: it recomposes, and any drift lands attributably on the `'resume'` header snapshot. This is the routing rule the seam creates: session-frozen openers ride the prefix; content that changes mid-session rides the append-only history channels (`agent.inject()` or tool/prompt-submit `additionalContexts` — [the interception-seams RFC](2026-06-30-interception-seams.md)), each a durable `context/message` paid once and prefix-cached thereafter. +- **Request-only, header-logged.** `deriveMessages()` never returns the prefix; its one durable record is `EpochHeader.messagePrefix` on the instance's anchoring `request/header` snapshot — the channel the reconstructable-requests Agent Note already owns for the request's non-history half, so no new session event exists. The dev invariant ([dsh-invariants](../../../../packages/support/invariants/src/index.ts)) recomputes `messagePrefix + boundary derivation` against every loop-built request; an unlogged prefix cannot reach the wire. +- **Frozen per instance.** Reuse is structural, not disciplined: the cached product cannot change mid-session, so the provider's prompt cache holds by construction and the prefix extends the cacheable region at zero marginal cost per step. A process restart or `ctx.agents.resume()` is a new instance: it recomposes, and any drift lands attributably on the `'resume'` header snapshot. This is the routing rule the seam creates: session-frozen openers ride the prefix; content that changes mid-session rides the append-only history channels (`agent.inject()` or tool/prompt-submit `additionalContexts` — [the interception-seams Agent Note](2026-06-30-interception-seams.md)), each a durable `context/message` paid once and prefix-cached thereafter. - **Exact in the durable request envelope.** Composition precedes the instance's first `agent/pre-step` and request boundary. The first routed request logs the current prefix on its header, so post-step token pressure reads the exact prefix together with the actual prompt, tools, and routed model; no compaction-only parameter is carried through the generic pre-step seam. A composition interrupted by cancel/dispose is discarded, never cached: an abort-aware listener's degraded fallback cannot leak into later requests, and the next turn recomposes under a live signal. Because composition runs before the boundary snapshot, a composing listener's session append joins the CURRENT request's derived history. Compaction structurally cannot touch the prefix (or the system prompt): it rewrites surface nodes, and header state never enters the surface. diff --git a/docs/rfc/implemented/feature/2026-07-08-background-subagent-tasks.md b/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md similarity index 99% rename from docs/rfc/implemented/feature/2026-07-08-background-subagent-tasks.md rename to .agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md index 830d93577b..83ea99dc75 100644 --- a/docs/rfc/implemented/feature/2026-07-08-background-subagent-tasks.md +++ b/.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md @@ -1,4 +1,4 @@ -# RFC: Background subagent tasks +# Agent Note: Background subagent tasks Status: implemented diff --git a/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md b/.agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md similarity index 91% rename from docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md rename to .agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md index 74f4216565..08f5bb01cf 100644 --- a/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md +++ b/.agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md @@ -1,4 +1,4 @@ -# RFC: Repeat-tool-call guard plugin +# Agent Note: Repeat-tool-call guard plugin Status: implemented @@ -6,13 +6,13 @@ Status: implemented A model stuck in a loop re-issues the same tool call with byte-identical arguments — re-running a failing grep, re-reading an unchanged file, polling a command that already gave its answer — and each round trip burns tokens, wall-clock, and (for paid APIs) money without adding information. The harness has nothing that notices: the loop has no step budget, no plugin tracks call repetition, and the model only escapes when it happens to vary its own behavior. The failure mode is real and cheap to detect — [pi-repeat-tool-guard](https://github.com/Kingwl/pi-repeat-tool-guard) ships exactly this as a pi coding-agent extension: count consecutive identical calls and, past a threshold, append a `<system-reminder>` telling the model to stop repeating itself and change course. -The harness already has every seam the pi extension uses, and better ones: [the interception-seams RFC](2026-06-30-interception-seams.md) gives `tools/post-execute` a sanctioned way to attach model-facing context to a finished call, the loop buffers and injects that context with call/result adjacency preserved, and injected context is a logged `context/message` — so a native guard satisfies the model-visible ⟺ logged rule with no new session event. What was missing was only the plugin itself. +The harness already has every seam the pi extension uses, and better ones: [the interception-seams Agent Note](2026-06-30-interception-seams.md) gives `tools/post-execute` a sanctioned way to attach model-facing context to a finished call, the loop buffers and injects that context with call/result adjacency preserved, and injected context is a logged `context/message` — so a native guard satisfies the model-visible ⟺ logged rule with no new session event. What was missing was only the plugin itself. ## Decision The guard is a loop-hygiene plugin, not a model-facing tool. It counts consecutive calls to the same tool with identical canonical arguments and injects advisory reminders at configured thresholds. It never delays, blocks, or rewrites a call; the model decides whether to retry differently or finish. -The plugin is `@deepseek-ai/dsh-repeat-tool-guard` at `packages/guard/repeat-tool-guard/`, opening the `guard/` group for loop-hygiene plugins (single-package groups have precedent: [the todo-write RFC](2026-06-29-todo-write-tool.md) shipped `todo/tool-todo`). It registers two listeners and holds state in a `WeakMap` keyed by the live `Agent` object — the tool registry is a context-level singleton whose waterfalls interleave every agent's calls (subagents run on the same context), so per-agent keying is correctness, not polish; weak object keys also make a disposal-only cleanup listener unnecessary. +The plugin is `@deepseek-ai/dsh-repeat-tool-guard` at `packages/guard/repeat-tool-guard/`, opening the `guard/` group for loop-hygiene plugins (single-package groups have precedent: [the todo-write Agent Note](2026-06-29-todo-write-tool.md) shipped `todo/tool-todo`). It registers two listeners and holds state in a `WeakMap` keyed by the live `Agent` object — the tool registry is a context-level singleton whose waterfalls interleave every agent's calls (subagents run on the same context), so per-agent keying is correctness, not polish; weak object keys also make a disposal-only cleanup listener unnecessary. - **`tools/post-execute` (waterfall)** — the one detection point. The listener receives `(exec, result)` together, so counting and reminder delivery need no cross-event pending map (the pi extension needs one only because its `tool_call`/`tool_result` hooks are separate events). It always delegates via `next()` and, when a threshold is hit, prepends a reminder to the downstream decision's `additionalContexts` — the observe-and-enrich posture [the hooks bridges](2026-06-30-hook-bridges.md) already use, honoring the waterfall contract. Counting happens here rather than in `tools/pre-execute` because post-execute also runs for denied calls (`ToolRegistry.execute` routes a deny through the same pipeline), and a model hammering a denied call is exactly the loop worth breaking. - **`agent/prompt-submit` (waterfall)** — pure reset hook: delegate via `next()`, clear the submitting agent's chain. A user interjection changes the context; repetition across it is not a loop. diff --git a/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md similarity index 92% rename from docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md rename to .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md index 94d11ee931..144bdd018f 100644 --- a/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md +++ b/.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md @@ -1,4 +1,4 @@ -# RFC: The self-referential cordis toolset +# Agent Note: The self-referential cordis toolset Status: implemented @@ -22,7 +22,7 @@ The vm isolates accidental global pollution, and the context façade hides frame | `cordis_mount` | Evaluates `code` (the body of an async JavaScript function) in a `node:vm` sandbox; the code must `return` a cordis plugin, which is mounted as a child of the `cordis-dynamic` group fiber and tracked under a fresh id (`dyn-1`, `dyn-2`, …). | | `cordis_unmount` | Disposes one dynamic mount by id and returns only after disposal reaches quiescence — every registration the plugin made is unwound, not merely requested to stop. | -`cordis_inspect` sections: `services` (every provided ctx service and the owning fiber, non-active owners flagged), `plugins` (a flat list of every loaded plugin with its lifecycle state, from `ctx.registry` — what capabilities are loaded, deliberately not the tree shape), `tools` (what the model can call), `dynamic` (the mount table: id, name, state, provided services, awaited services), `api` (live service signatures + the type shapes they reference, from the generated catalog), and `events` (harness events with dispatch mode and signature). Broad `api` and `events` reports omit full JSDoc to stay compact; an exact `name` returns one service or event with its original method/declaration JSDoc. A name is invalid with other sections, unknown targets fail, and an API target must be live. The model-facing tool descriptions carry the operational rules the model needs at call time; [the generated tool catalog](../../../tool-catalog.md) is their exhaustive rendering. +`cordis_inspect` sections: `services` (every provided ctx service and the owning fiber, non-active owners flagged), `plugins` (a flat list of every loaded plugin with its lifecycle state, from `ctx.registry` — what capabilities are loaded, deliberately not the tree shape), `tools` (what the model can call), `dynamic` (the mount table: id, name, state, provided services, awaited services), `api` (live service signatures + the type shapes they reference, from the generated catalog), and `events` (harness events with dispatch mode and signature). Broad `api` and `events` reports omit full JSDoc to stay compact; an exact `name` returns one service or event with its original method/declaration JSDoc. A name is invalid with other sections, unknown targets fail, and an API target must be live. The model-facing tool descriptions carry the operational rules the model needs at call time; [the generated tool catalog](../../../../docs/tool-catalog.md) is their exhaustive rendering. ### Sandbox semantics @@ -50,7 +50,7 @@ Freshness is gated like every generated artifact: `pnpm run verify-cordis-api` ( ### Configuration, rendering, and observability -The plugin exposes one config field, validated by schemastery and documented in [the config catalog](../../../config-catalog.md): `vmTimeoutMs` (default 5000), the millisecond bound on the synchronous portion of mount-code evaluation. Tool names, the `cordis-dynamic` group name, and the `dyn-` id prefix are structural vocabulary and stay fixed. All three tools render as `generic` cards per [the tool cookbook](../../../cookbook/adding-a-tool.md) (`cordis_inspect` a `read`, `cordis_mount` an `execute` carrying the code as `rawInput`, `cordis_unmount` a `delete`), with no `presentResult` overrides. +The plugin exposes one config field, validated by schemastery and documented in [the config catalog](../../../../docs/config-catalog.md): `vmTimeoutMs` (default 5000), the millisecond bound on the synchronous portion of mount-code evaluation. Tool names, the `cordis-dynamic` group name, and the `dyn-` id prefix are structural vocabulary and stay fixed. All three tools render as `generic` cards per [the tool cookbook](../../../../docs/cookbook/adding-a-tool.md) (`cordis_inspect` a `read`, `cordis_mount` an `execute` carrying the code as `rawInput`, `cordis_unmount` a `delete`), with no `presentResult` overrides. Model-visible ⟺ logged holds with no new session event type: a mount or unmount is visible only through its own `tool/call` / `tool/result` pair, which the loop logs, and the changed tool set a mount induces is logged by the full changed request header the loop emits when schemas change between steps. There is deliberately no `cordis/mount` provenance event — it would duplicate what the tool-call pair records. Dynamic mounts are process-lifetime, not session state: resuming a persisted session rehydrates the conversation but does not re-mount plugins. @@ -77,4 +77,4 @@ The correctness investment therefore goes where it pays for every capability at ## Consequences -The toolset is a deliberate opt-in with a fully-privileged `ctx`, so a deployment adopts it as consciously as a bash tool. Several facts follow that the tool descriptions warn the model about directly: a waterfall listener (e.g. `tools/pre-execute`) that returns without calling `next()` vetoes the chain, so a mounted listener can lobotomize the agent's own tool dispatch ([waterfall semantics](../../../cordis-primer.md#cordis-waterfall-semantics)); mount code runs inside a tool call of the current turn, so awaiting anything that resolves only after the turn deadlocks; `vmTimeoutMs` bounds synchronous evaluation only; and mounts do not survive session resume. +The toolset is a deliberate opt-in with a fully-privileged `ctx`, so a deployment adopts it as consciously as a bash tool. Several facts follow that the tool descriptions warn the model about directly: a waterfall listener (e.g. `tools/pre-execute`) that returns without calling `next()` vetoes the chain, so a mounted listener can lobotomize the agent's own tool dispatch ([waterfall semantics](../../../../docs/cordis-primer.md#cordis-waterfall-semantics)); mount code runs inside a tool call of the current turn, so awaiting anything that resolves only after the turn deadlocks; `vmTimeoutMs` bounds synchronous evaluation only; and mounts do not survive session resume. diff --git a/docs/rfc/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md b/.agents/notes/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md similarity index 76% rename from docs/rfc/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md rename to .agents/notes/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md index cd4f3b078b..60a5e3e3a0 100644 --- a/docs/rfc/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md +++ b/.agents/notes/implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md @@ -1,4 +1,4 @@ -# RFC: Bash-backed grep and glob discovery tools +# Agent Note: Bash-backed grep and glob discovery tools Status: implemented @@ -10,13 +10,13 @@ Search output also has two distinct budgets. The tool needs enough raw `rg` outp ## Decision -`glob` and `grep` are model-facing tools in `@deepseek-ai/dsh-tool-fs-search`, backed by the bash seam, not by new `ctx.fs` provider methods. The package registers model-facing filesystem discovery tools, but execution uses `ctx.bash.resolve(request)` followed by `ctx.bash.run(spec)` with fixed `rg` command templates assembled by the tool. The tool layer owns schemas, argument validation, shell quoting, result parsing, result formatting, retention, formatted-result spill handoff, and timeout declaration. The bash executor owns request defaulting/capping, subprocess execution, process-group termination, environment scrubbing, raw output capture, and backend substitution across local, sandboxed, or remote bash implementations. +`glob` and `grep` are conditional model-facing tools in `@deepseek-ai/dsh-tool-fs-search`, backed by the bash seam, not by new `ctx.fs` provider methods. At plugin load, the package checks `command -v rg >/dev/null 2>&1` through `ctx.bash.resolve(request)` followed by `ctx.bash.run(spec)`; if the command exits nonzero, the package logs a warning and registers neither tools nor prompt sections. A probe that cannot start, times out, aborts, is killed, or produces no exit code fails plugin load loudly because that is a broken bash executor rather than an absent optional binary. When registered, execution uses the same `ctx.bash.resolve(request)` followed by `ctx.bash.run(spec)` flow with fixed `rg` command templates assembled by the tool. The tool layer owns schemas, argument validation, shell quoting, result parsing, result formatting, retention, formatted-result spill handoff, and timeout declaration. The bash executor owns request defaulting/capping, subprocess execution, process-group termination, environment scrubbing, raw output capture, and backend substitution across local, sandboxed, or remote bash implementations. The tools do not use `ctx.bash.start()` and do not create model-visible background tasks. They run as ordinary foreground tools from the agent loop's perspective: the tool call returns only after the `rg` command exits, times out, is aborted, or fails. `defineTool({ timeoutMs })` declares the cooperative tool-call budget, `@deepseek-ai/dsh-timeout-policy` enforces it through `exec.signal`, and the tool forwards that signal into the bash request before `resolve()` / `run()`. The bash backend's own timeout remains a second safety cap; whichever aborts first wins. The tools align `path` with Claude Code's search tools while binding resolution to the bash workdir, not to `ctx.fs`. The tool derives the bash request workdir from `exec.agent?.session.header.cwd`, mirroring `dsh-tool-bash` and `dsh-tool-fs`; when no session cwd exists, it omits `request.workdir` so the bash implementation applies its configured cwd or process cwd through `resolve()`. For `grep`, `path` is an optional ripgrep target and may be a file or directory; omitted means the resolved bash workdir. For `glob`, `path` is an optional directory search root; omitted means the resolved bash workdir. Relative `path` values resolve against that workdir. Returned paths are displayed relative to the resolved bash workdir when possible and are intended to be follow-up-readable only in co-located deployments where the bash workdir and filesystem `read` root are the same workspace. v1 documents that deployment requirement but does not perform runtime cross-service validation. Remote or virtual filesystem search is deferred until there is a shared workspace/root contract or a provider-specific search backend. -The package does not inject `fs`. It injects `tools`, `systemPrompt`, and `bash`; it deliberately reads `spillStore` with `ctx.get('spillStore')` instead of static inject because formatted-result spill is optional. Existing `@deepseek-ai/dsh-tool-fs` deployments that only want `read` / `write` / `edit` do not need to load bash. +The package does not inject `fs`. It injects `tools`, `systemPrompt`, and `bash`; it deliberately reads `spillStore` with `ctx.get('spillStore')` instead of static inject because formatted-result spill is optional. Existing `@deepseek-ai/dsh-tool-fs` deployments that only want `read` / `write` / `edit` do not need to load bash. Deployments that load search need `rg` available in the bash executor environment for the tools to enter the model-visible schema. ### Package shape @@ -63,9 +63,9 @@ Routine budgets stay out of the model-facing schema. `@deepseek-ai/dsh-tool-fs-s | `rawOutputMaxBytes` | `20000000` | Max complete raw `rg` stdout the tool will parse; matches Claude Code's ripgrep raw buffer. | | `timeoutMs` | `30000` | Tool-call timeout attached to both tool definitions and enforced by `@deepseek-ai/dsh-timeout-policy`. | -`globMaxResults` and `grepMaxMatches` use `ItemRetainer({ kind: 'head' })`. `grepMaxLineBytes` uses `TextRetainer({ kind: 'head', maxBytes: grepMaxLineBytes })` for each matched line so preview cuts preserve UTF-8 boundaries. This follows the [tool result retention library](../../implemented/architecture/2026-07-06-tool-result-retention-library.md) mapping for discovery items: collect the complete result, retain head items inline, and keep path mapping, grouping, and per-line preview outside the retainer. `grep` does not expose `case_insensitive`, `head_limit`, `offset`, `count`, multiline, context lines, output modes, or file type filters in v1. A model that needs surrounding context reads the matched file with `read`; a model that needs later results follows the returned spill locator's retrieval hint. +`globMaxResults` and `grepMaxMatches` use `ItemRetainer({ kind: 'head' })`. `grepMaxLineBytes` uses `TextRetainer({ kind: 'head', maxBytes: grepMaxLineBytes })` for each matched line so preview cuts preserve UTF-8 boundaries. This follows the [tool result retention library](../architecture/2026-07-06-tool-result-retention-library.md) mapping for discovery items: collect the complete result, retain head items inline, and keep path mapping, grouping, and per-line preview outside the retainer. `grep` does not expose `case_insensitive`, `head_limit`, `offset`, `count`, multiline, context lines, output modes, or file type filters in v1. A model that needs surrounding context reads the matched file with `read`; a model that needs later results follows the returned spill locator's retrieval hint. -The Claude Code values are reference points for the two-layer budget, not model-facing schema precedent. Its dedicated search tools buffer raw ripgrep output up to 20 MB for internal processing, use a 20-second ripgrep timeout on non-WSL platforms (60 seconds on WSL), then apply search-specific caps before the model sees a result: `GrepTool` defaults to `head_limit = 250` and persists formatted results above 20,000 characters, while `GlobTool` defaults to 100 paths and persists formatted results above 100,000 characters. This RFC mirrors the raw-buffer and inline-count defaults, chooses a 30-second default search timeout, and uses this harness's `ctx.spillStore.saveText()` path for formatted-result recovery. +The Claude Code values are reference points for the two-layer budget, not model-facing schema precedent. Its dedicated search tools buffer raw ripgrep output up to 20 MB for internal processing, use a 20-second ripgrep timeout on non-WSL platforms (60 seconds on WSL), then apply search-specific caps before the model sees a result: `GrepTool` defaults to `head_limit = 250` and persists formatted results above 20,000 characters, while `GlobTool` defaults to 100 paths and persists formatted results above 100,000 characters. This Agent Note mirrors the raw-buffer and inline-count defaults, chooses a 30-second default search timeout, and uses this harness's `ctx.spillStore.saveText()` path for formatted-result recovery. The `path` field follows the same split as Claude Code: `grep.path` is a file-or-directory ripgrep target, while `glob.path` is a directory search root. v1 does not expose a separate cwd/workdir argument on these tools. @@ -79,9 +79,9 @@ The `path` field follows the same split as Claude Code: `grep.path` is a file-or Raw `rg` stdout is an internal transport detail. The tool requests `stdoutMaxBytes: rawOutputMaxBytes` through `ctx.bash.resolve()` and parses `stdout.text` only when the executor returns untruncated stdout within that cap. If stdout is larger than `rawOutputMaxBytes`, or the executor still returns `stdout.truncated`, the tool fails with a clear search error telling the model to narrow `pattern`, `path`, or `include`. The tool never exposes raw `rg` output or bash raw spill paths to the model. -Only stdout is a parse source. Stderr is diagnostic text for invalid patterns, missing `rg`, and search failures; if bash truncates stderr, the tool uses the retained stderr tail with a truncation note and does not read `stderr.spillPath`. +Only stdout is a parse source. Stderr is diagnostic text for invalid patterns, runtime `rg` disappearance after registration, and search failures; if bash truncates stderr, the tool uses the retained stderr tail with a truncation note and does not read `stderr.spillPath`. -If `ctx.bash.run()` reports `aborted` because the tool timeout or caller cancellation fired, the tool returns a structured failure rather than pretending there were no matches. If bash reports its own timeout first, the tool likewise fails with a clear timeout message. Nonzero ripgrep exit semantics are tool-owned: exit 0 is success with matches, exit 1 is success with no matches, invalid pattern / missing `rg` / inaccessible search workdir are failures. +If `ctx.bash.run()` reports `aborted` because the tool timeout or caller cancellation fired, the tool returns a structured failure rather than pretending there were no matches. If bash reports its own timeout first, the tool likewise fails with a clear timeout message. Nonzero ripgrep exit semantics are tool-owned: exit 0 is success with matches, exit 1 is success with no matches, invalid pattern / runtime `rg` disappearance / inaccessible search workdir are failures. Search failures use a package-owned `HarnessError` subclass with `SEARCH_*` codes, not `FsErrorCode`, because these tools are not `ctx.fs` provider operations. The v1 vocabulary is `SEARCH_INVALID_PATTERN`, `SEARCH_FAILED`, `SEARCH_RAW_OUTPUT_OVERFLOW`, and `SEARCH_ABORTED`. Model argument validation failures such as missing required fields, blank strings, or unsupported negated/list `include` values remain ordinary tool argument errors. @@ -116,19 +116,19 @@ Line 12: ... (Full grep result stored at: /.../session-abc123/9f8e7d-grep-results.txt. Use read with offset/limit, or grep this path to search within it.) ``` -If the complete logical result fits under the inline cap, no formatted spill artifact is created. If the complete logical result is too large but formatted spill is unavailable, the footer says that the result was capped and the complete result could not be saved. The `truncated` / omitted count is a budget fact, not an incomplete-search fact; timeout, invalid regex, missing `rg`, inaccessible workdirs, raw-output overflow, binary skips, and parse failures stay in tool-domain error or incomplete fields. +If the complete logical result fits under the inline cap, no formatted spill artifact is created. If the complete logical result is too large but formatted spill is unavailable, the footer says that the result was capped and the complete result could not be saved. The `truncated` / omitted count is a budget fact, not an incomplete-search fact; timeout, invalid regex, runtime `rg` disappearance, inaccessible workdirs, raw-output overflow, binary skips, and parse failures stay in tool-domain error or incomplete fields. ## Alternatives considered **Put `glob` / `grep` on `ctx.fs`.** Rejected for v1: it forces every filesystem backend to grow a search API and makes local ripgrep behavior part of the provider seam. Search is useful product behavior, but it is not a universal text-storage primitive like `readText` or `writeText`. -**Directly spawn ripgrep from `dsh-fs-local`.** Rejected for this RFC's v1: direct spawn gives the cleanest argv boundary, stdout/stderr control, and early-stop control, but it duplicates process execution concerns that the bash seam already owns: environment scrubbing, process-group kill, timeout propagation, sandbox/remote executor substitution, and bounded output capture. It remains a reasonable optimization if bash-backed search proves too shell-string-sensitive or if foreground streaming becomes necessary. +**Directly spawn ripgrep from `dsh-fs-local`.** Rejected for this Agent Note's v1: direct spawn gives the cleanest argv boundary, stdout/stderr control, and early-stop control, but it duplicates process execution concerns that the bash seam already owns: environment scrubbing, process-group kill, timeout propagation, sandbox/remote executor substitution, and bounded output capture. It remains a reasonable optimization if bash-backed search proves too shell-string-sensitive or if foreground streaming becomes necessary. **Use `ctx.bash.start()` for streaming early stop.** Rejected: `start()` creates model-visible background task semantics: task ids, owner tokens, `bash_output`, `bash_kill`, completion notifications, and no built-in timeout. `grep` needs a foreground tool result, not a background bash workflow. If streaming search becomes necessary, the right abstraction is a foreground streaming process handle on the bash/process seam, not borrowing the public background-task API. **Expose bash raw spill paths to the model.** Rejected: a bash raw spill path contains raw `rg` stdout (`rg --json` records for grep), not the stable formatted search result. Search parses raw stdout only as an internal transport; model recovery uses a formatted result saved through `ctx.spillStore.saveText()`. -**Add `spillStore.saveFile()` for bash output normalization first.** Rejected for this RFC's v1: `saveFile()` would help a future bash normalization pass move existing executor spill files into session-scoped spill storage, but search only needs bounded in-memory raw `rg` stdout before producing the model-facing artifact. `saveText()` is sufficient for the formatted search result. +**Add `spillStore.saveFile()` for bash output normalization first.** Rejected for this Agent Note's v1: `saveFile()` would help a future bash normalization pass move existing executor spill files into session-scoped spill storage, but search only needs bounded in-memory raw `rg` stdout before producing the model-facing artifact. `saveText()` is sufficient for the formatted search result. **Rely on the generic `dsh-spill-policy`.** Rejected: generic post-execute spill sees only the final tool result. If `grep` / `glob` return the first page inline, the generic policy cannot recover omitted results. The search tools must save the complete formatted result themselves before returning the bounded model-facing text. @@ -138,22 +138,24 @@ If the complete logical result fits under the inline cap, no formatted spill art **Expand the bash seam with a raw-output reader first.** Rejected: a portable `readRawOutput(ref, maxBytes)` API would add reference lifetime, permission, and backend storage semantics. A per-run `stdoutMaxBytes` request is the narrower seam: search either receives complete stdout within `rawOutputMaxBytes` or fails clearly. +**Always register and report missing `rg` only at execution time.** Rejected: a model-visible tool schema is a promise that the deployment can attempt that capability. If the bash executor cannot find ripgrep at load, the safer surface is no `glob` / `grep` tools or prompt guidance. Execution-time missing-`rg` classification remains as a defensive fallback for environments that change after registration. + ## Testing -- Tests prove an aborted `exec.signal` reaches the bash backend (same-reference spec assertion plus the `SEARCH_ABORTED` result), and cover command construction/quoting (malicious patterns, paths with spaces, leading-dash values, quotes, newlines, glob metacharacters — unit assertions plus a real `bash -c` round-trip for every hostile value), `grep.path` as file and directory targets, `glob.path` as a directory search root, invalid pattern handling, no matches, malformed `rg --json` output, matched-line preview truncation, raw-output overflow, timeout/abort, formatted spill success/failure, the package-owned `SEARCH_*` error codes, and the no-background-task invariant. +- Tests cover registration-time `rg` probing (probe success registers both tools and prompt sections, nonzero probe skips both tools and prompt sections with a warning, infrastructure probe failures reject plugin load), prove an aborted `exec.signal` reaches the bash backend (same-reference spec assertion plus the `SEARCH_ABORTED` result), and cover command construction/quoting (malicious patterns, paths with spaces, leading-dash values, quotes, newlines, glob metacharacters — unit assertions plus a real `bash -c` round-trip for every hostile value), `grep.path` as file and directory targets, `glob.path` as a directory search root, invalid pattern handling, no matches, malformed `rg --json` output, matched-line preview truncation, raw-output overflow, timeout/abort, formatted spill success/failure, the package-owned `SEARCH_*` error codes, and the no-background-task invariant. - The first-party tool-owned spill precedent is covered directly: spill backend present, spill backend absent, `saveText()` failure, and missing spill owner. - The package has real Loader-path coverage for the namespace plugin export shape (`name`, `inject`, `Config`, and `apply`, with no default export). -- A real-executor integration suite (`dsh-bash-local` + a real `rg`) verifies the world: hostile patterns stay inert, per-session cwd resolution, VCS-metadata exclusion, modification-time ordering, and real ripgrep stderr classification. It self-skips where `rg` is not on PATH (a CI accommodation mirroring the keyless e2e skip); the fake-executor suite alone carries the per-file 100% coverage gate. +- A real-executor integration suite (`dsh-bash-local` + a real `rg`) verifies the world: hostile patterns stay inert, per-session cwd resolution, VCS-metadata exclusion, modification-time ordering, and real ripgrep stderr classification. It self-skips where `rg` is not on the test process PATH (a CI accommodation mirroring the keyless e2e skip); the fake-executor suite carries registration and execution coverage for missing `rg`, plus the per-file 100% coverage gate. - Snapshot gap note for the transcript-visible spill notice: this landed with the gap note, not a snapshot. The snapshot tier replays the acp-agent tree, and adding the search plugin there changes the assembled system prompt — every expected output would need re-recording with a real key, which the implementing environment did not hold. The spill notice's exact transcript text is pinned by unit tests (`formatGlobOutput`/`formatGrepOutput` and the through-the-registry spill tests); wiring the plugin into the acp-agent tree plus a `test:snapshot:record` pass is the follow-up for the next key-holding session. ## Consequences -- `glob` and `grep` are model-facing tools in `@deepseek-ai/dsh-tool-fs-search`, not `ctx.fs` provider methods and not part of the existing `@deepseek-ai/dsh-tool-fs` root plugin. The package injects `tools`, `systemPrompt`, and `bash`; it does not inject `fs`, and `ctx.spillStore` stays optional via `ctx.get('spillStore')`. +- `glob` and `grep` are conditional model-facing tools in `@deepseek-ai/dsh-tool-fs-search`, not `ctx.fs` provider methods and not part of the existing `@deepseek-ai/dsh-tool-fs` root plugin. They register only when the bash executor can find `rg`; the package injects `tools`, `systemPrompt`, and `bash`, does not inject `fs`, and keeps `ctx.spillStore` optional via `ctx.get('spillStore')`. - The schemas are exactly `glob(pattern, path?)` and `grep(pattern, path?, include?)`; search caps and timeout are defaulted, validated Config fields (`globMaxResults`, `grepMaxMatches`, `grepMaxLineBytes`, `rawOutputMaxBytes`, `timeoutMs`). - The tools execute through `ctx.bash.resolve(request)` → `ctx.bash.run(spec)`, forward `exec.signal`, never call `ctx.bash.start()`, and never expose a bash task id. The bash request workdir comes from `exec.agent?.session.header.cwd` when available; the resolved `spec.workdir` drives execution and relative-path display. - The tools request `stdoutMaxBytes: rawOutputMaxBytes` from the bash seam, parse only untruncated stdout within that cap, and treat over-cap or still-truncated raw output as a clear search failure; raw `rg` output is never exposed to the model. - Oversized complete formatted results are saved through `ctx.spillStore.saveText()` when available while inline results stay bounded; spill failure, a missing backend, or a missing owner preserves the inline result and reports the unsaved remainder — never an `isError`. -- The package README, the generated config catalog, and exported JSDoc document the Config fields and `SEARCH_*` codes; the repl-agent example ships the tools (the acp-agent tree waits on the snapshot re-record above); the fs group README records the co-located bash/filesystem deployment requirement. +- The package README, the generated config catalog, and exported JSDoc document the Config fields and `SEARCH_*` codes; the repl-agent example ships the conditional tool plugin (the acp-agent tree waits on the snapshot re-record above); the fs group README records the `rg` availability and co-located bash/filesystem deployment requirements. ## Risks diff --git a/docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md b/.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md similarity index 94% rename from docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md rename to .agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md index 118009dedb..988052fb2a 100644 --- a/docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md +++ b/.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md @@ -1,4 +1,4 @@ -# RFC: Expose agent session identity and JSONL location to tools and hooks +# Agent Note: Expose agent session identity and JSONL location to tools and hooks Status: implemented @@ -10,7 +10,7 @@ The boundary must preserve two properties: the owner of a fact decides how to re ## Decision -Extend the [`SessionPersistence`](../../implemented/architecture/2026-06-14-session-persistence.md) seam with a synchronous, side-effect-free location query: +Extend the [`SessionPersistence`](../architecture/2026-06-14-session-persistence.md) seam with a synchronous, side-effect-free location query: ```ts import type { SessionHeader } from '@deepseek-ai/dsh-session' @@ -42,7 +42,7 @@ The bash seam exports `DSH_ENV_PREFIX` as the single namespace source and derive The bash tool description teaches only the durable convention: current harness environment facts are available through managed `$DSH_*` variables and may be inspected when needed. It does not enumerate persistence-specific keys or add a permanent system-prompt section. Tool schemas are already logged in request headers and tool output is logged as `tool/result`, so no new session event is required. -The [Claude Code and Codex hook bridges](../../implemented/feature/2026-06-30-hook-bridges.md) resolve transcript location from the same persistence seam when constructing payloads. Codex uses `transcript_path: string | null`; Claude Code preserves its string field and falls back to `''`. Hook lookup neither materializes nor flushes a session. +The [Claude Code and Codex hook bridges](2026-06-30-hook-bridges.md) resolve transcript location from the same persistence seam when constructing payloads. Codex uses `transcript_path: string | null`; Claude Code preserves its string field and falls back to `''`. Hook lookup neither materializes nor flushes a session. ## Peer product findings diff --git a/docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md similarity index 97% rename from docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md rename to .agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md index 90fbc1cfb4..4904da5bd5 100644 --- a/docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md +++ b/.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md @@ -1,4 +1,4 @@ -# RFC: Parallel tool-call execution by per-call safety +# Agent Note: Parallel tool-call execution by per-call safety Status: implemented @@ -12,7 +12,7 @@ The session log remains authoritative: every started call has an audit event, ev ## Decision -Each tool may provide an optional `isConcurrencySafe(args)` classifier. It is synchronous and pure: it examines only the current call's parsed arguments and performs no I/O or mutation. Only an explicit `true` opts in; a missing classifier, invalid arguments, a thrown classifier, or any other return value makes the call exclusive. The canonical type contract lives in the [tool data structures](../../../core-data-structures/tools.md). +Each tool may provide an optional `isConcurrencySafe(args)` classifier. It is synchronous and pure: it examines only the current call's parsed arguments and performs no I/O or mutation. Only an explicit `true` opts in; a missing classifier, invalid arguments, a thrown classifier, or any other return value makes the call exclusive. The canonical type contract lives in the [tool data structures](../../../../docs/core-data-structures/tools.md). The classifier is deliberately unary. Returning `true` is the tool's promise that this call may overlap with any sibling call that also returns `true`; the scheduler does not compare calls or prove that their resource accesses are compatible. @@ -56,7 +56,7 @@ Any shared state touched during execution must be concurrency-safe. This include ## Configuration and declarations -`maxParallelToolCalls` is a positive AgentLoop deployment cap shared by every agent the factory creates. It defaults to `10`; `1` preserves serial execution. Exact fields and defaults live in the generated [configuration catalog](../../../config-catalog.md). +`maxParallelToolCalls` is a positive AgentLoop deployment cap shared by every agent the factory creates. It defaults to `10`; `1` preserves serial execution. Exact fields and defaults live in the generated [configuration catalog](../../../../docs/config-catalog.md). The shipped declarations are conservative. Web search, web fetch, and filesystem read opt in. Filesystem writes and edits, bash tools, subagent delegation, workflow, user interaction, todo mutation, Code Mode, and Cordis mutation tools remain exclusive. A subagent may share its parent's workspace or external resources, and the unary classifier cannot prove that sibling delegations have disjoint effects. Bash has no proven input-sensitive classifier and remains exclusive. diff --git a/docs/rfc/implemented/feature/2026-07-10-session-query-service.md b/.agents/notes/implemented/feature/2026-07-10-session-query-service.md similarity index 99% rename from docs/rfc/implemented/feature/2026-07-10-session-query-service.md rename to .agents/notes/implemented/feature/2026-07-10-session-query-service.md index ee2acc62e1..a34d699601 100644 --- a/docs/rfc/implemented/feature/2026-07-10-session-query-service.md +++ b/.agents/notes/implemented/feature/2026-07-10-session-query-service.md @@ -1,4 +1,4 @@ -# RFC: Exact session query service +# Agent Note: Exact session query service Status: implemented diff --git a/docs/rfc/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 similarity index 99% rename from docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md rename to .agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md index 3c2e17aebd..29f77364fa 100644 --- a/docs/rfc/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 @@ -1,4 +1,4 @@ -# RFC: Configure subagent persona, tool visibility, and depth +# Agent Note: Configure subagent persona, tool visibility, and depth Status: implemented diff --git a/docs/rfc/implemented/feature/2026-07-13-session-query-tracing.md b/.agents/notes/implemented/feature/2026-07-13-session-query-tracing.md similarity index 98% rename from docs/rfc/implemented/feature/2026-07-13-session-query-tracing.md rename to .agents/notes/implemented/feature/2026-07-13-session-query-tracing.md index f16f543ac5..08f856863e 100644 --- a/docs/rfc/implemented/feature/2026-07-13-session-query-tracing.md +++ b/.agents/notes/implemented/feature/2026-07-13-session-query-tracing.md @@ -1,4 +1,4 @@ -# RFC: Session query relationship tracing +# Agent Note: Session query relationship tracing Status: implemented 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> 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> 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/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml similarity index 65% rename from docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml rename to .agents/notes/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml index f1d38d7d95..28ecd2a765 100644 --- a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.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-14-time-context-plugin.md: b8b54156e08aa1212866d46500ad1ca65b4f4f14 -2026-07-14-time-context-plugin.zh.md: 0af261c66a9b52cdf250294b4bfdc240176c8434 +2026-07-14-time-context-plugin.md: 189f75fc12fe12e9dec56fc71ea901ec2eaa8b19 +2026-07-14-time-context-plugin.zh.md: 12671cb891531627fffabb7bd91a1532bc3de6b9 diff --git a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.md similarity index 97% rename from docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md rename to .agents/notes/implemented/feature/2026-07-14-time-context-plugin.md index b8b54156e0..189f75fc12 100644 --- a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.md +++ b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.md @@ -1,4 +1,4 @@ -# RFC: Optional time-context plugin +# Agent Note: Optional time-context plugin Status: implemented @@ -32,7 +32,7 @@ When `timeZone` is omitted, `Intl.DateTimeFormat` resolves the Node process's sy ### Logging and token shape -The loop records the temporal block in full `request/header` snapshots before transmission, satisfying the [reconstructable-requests contract](../architecture/2026-07-05-reconstructable-requests.md). Each request carries one current block; earlier readings do not remain in conversation history. The plugin owns the fact and contributes it through the prompt registry, following the [prompt-variables RFC](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md) without a loop special case. +The loop records the temporal block in full `request/header` snapshots before transmission, satisfying the [reconstructable-requests contract](../architecture/2026-07-05-reconstructable-requests.md). Each request carries one current block; earlier readings do not remain in conversation history. The plugin owns the fact and contributes it through the prompt registry, following the [prompt-variables Agent Note](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md) without a loop special case. ## Testing diff --git a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.zh.md similarity index 95% rename from docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md rename to .agents/notes/implemented/feature/2026-07-14-time-context-plugin.zh.md index 0af261c66a..12671cb891 100644 --- a/docs/rfc/implemented/feature/2026-07-14-time-context-plugin.zh.md +++ b/.agents/notes/implemented/feature/2026-07-14-time-context-plugin.zh.md @@ -1,4 +1,4 @@ -# RFC:可选时间上下文插件 +# Agent Note:可选时间上下文插件 Status: implemented @@ -6,7 +6,7 @@ Status: implemented ## 问题 -本记录中的动态系统提示词存储和刷新决策已由[持久的逐步骤时间上下文](2026-07-16-durable-per-step-time-context.md)取代。需要显式启用的包(package)、分区时间格式和校验仍然保留;后续 RFC 负责当前的模型可见与持久性契约。 +本记录中的动态系统提示词存储和刷新决策已由[持久的逐步骤时间上下文](2026-07-16-durable-per-step-time-context.md)取代。需要显式启用的包(package)、分区时间格式和校验仍然保留;后续 Agent Note 负责当前的模型可见与持久性契约。 如果部署方既未在提示词中提供时钟,也未给模型提供查询工具,agent(智能体)请求就无法获得实时准确的时间。静态文本会变得陈旧,而对于日期、截止时间或闲置时长等常规推理,调用工具会增加开销。缺少已经过去的时长时,模型无法区分紧接着发送的消息与上一条消息几小时后才发送的消息。 @@ -32,7 +32,7 @@ Status: implemented ### 日志与 token 形态 -agent loop(智能体循环)会在发送前通过完整的 `request/header` 快照记录时间区块,从而满足[可重建请求契约](../architecture/2026-07-05-reconstructable-requests.md)。每个请求只携带一个当前区块;先前的读数不会保留在会话历史中。该插件拥有时间信息,并按照[提示词变量 RFC](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)通过提示词注册表贡献该信息,无需为循环添加特殊分支。 +agent loop(智能体循环)会在发送前通过完整的 `request/header` 快照记录时间区块,从而满足[可重建请求契约](../architecture/2026-07-05-reconstructable-requests.md)。每个请求只携带一个当前区块;先前的读数不会保留在会话历史中。该插件拥有时间信息,并按照[提示词变量 Agent Note](../architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)通过提示词注册表贡献该信息,无需为循环添加特殊分支。 ## 测试 diff --git a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml similarity index 62% rename from docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml rename to .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml index ce964f71da..f037660761 100644 --- a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.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-16-durable-per-step-time-context.md: 4a0828111faf9f787c2d338024c42680a4a697e2 -2026-07-16-durable-per-step-time-context.zh.md: f745cf7da7c38f9682abf9d8f210bcba328c8a51 +2026-07-16-durable-per-step-time-context.md: 2d7076d51dbe1a64e5042230bddc6844141ff265 +2026-07-16-durable-per-step-time-context.zh.md: 432e0305cf44dcce1053c6580c9f0039309a7af4 diff --git a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.md b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md similarity index 99% rename from docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.md rename to .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md index 4a0828111f..2d7076d51d 100644 --- a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.md +++ b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md @@ -1,4 +1,4 @@ -# RFC: Durable per-step time context +# Agent Note: Durable per-step time context Status: implemented diff --git a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md similarity index 99% rename from docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md rename to .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md index f745cf7da7..432e0305cf 100644 --- a/docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md +++ b/.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.zh.md @@ -1,4 +1,4 @@ -# RFC: 持久的逐步骤时间上下文 +# Agent Note: 持久的逐步骤时间上下文 Status: implemented diff --git a/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml similarity index 60% rename from docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml rename to .agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml index 8618fd884f..34c342ffd3 100644 --- a/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-17-dedicated-full-screen-tui-front-door.md: c834594b3af1e1f348aec2cf67324d5123a1ed2d -2026-07-17-dedicated-full-screen-tui-front-door.zh.md: 8cb8eb6d2812e6ecf9d98d20c64da24c2943363c +2026-07-17-dedicated-full-screen-tui-front-door.md: 178b5ea44be67f820a8ea7fed8acb987dffb3f80 +2026-07-17-dedicated-full-screen-tui-front-door.zh.md: ac055bad1b7a692c7a980430fdbd1e34737a9994 diff --git a/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md similarity index 92% rename from docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md rename to .agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md index c834594b3a..178b5ea44b 100644 --- a/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md +++ b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md @@ -1,4 +1,4 @@ -# RFC: Dedicated full-screen TUI front door +# Agent Note: Dedicated full-screen TUI front door Status: implemented @@ -32,7 +32,7 @@ The built-in palette uses standard 16-color ANSI foregrounds and SGR attributes, ## Verification -The implemented [TUI terminal-state snapshot RFC](../testing/2026-07-18-tui-terminal-state-snapshots.md) owns the four-layer verification contract: direct behavior tests, transient semantic terminal snapshots, recorded JSONL journeys through production tools, and Loader/PTY smoke tests. The package README owns configuration, commands, model-visible effects, and current limitations. +The implemented [TUI terminal-state snapshot Agent Note](../testing/2026-07-18-tui-terminal-state-snapshots.md) owns the four-layer verification contract: direct behavior tests, transient semantic terminal snapshots, recorded JSONL journeys through production tools, and Loader/PTY smoke tests. The package README owns configuration, commands, model-visible effects, and current limitations. ## Alternatives considered diff --git a/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md similarity index 92% rename from docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md rename to .agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md index 8cb8eb6d28..ac055bad1b 100644 --- a/docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md +++ b/.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.zh.md @@ -1,4 +1,4 @@ -# RFC: 独立的全屏 TUI 入口 +# Agent Note: 独立的全屏 TUI 入口 Status: implemented @@ -32,7 +32,7 @@ agent 空闲时,编辑器输入调用 `agent.send()`;轮次运行中则调 ## 验证 -已实现的 [TUI 终端状态快照 RFC](../testing/2026-07-18-tui-terminal-state-snapshots.md) 规定四层验证契约:直接行为测试、瞬态语义终端快照、通过生产工具执行的已录制 JSONL 流程,以及 Loader/PTY 冒烟测试。包(package)README 负责记录配置、命令、模型可见效果和当前限制。 +已实现的 [TUI 终端状态快照 Agent Note](../testing/2026-07-18-tui-terminal-state-snapshots.md) 规定四层验证契约:直接行为测试、瞬态语义终端快照、通过生产工具执行的已录制 JSONL 流程,以及 Loader/PTY 冒烟测试。包(package)README 负责记录配置、命令、模型可见效果和当前限制。 ## 曾考虑的替代方案 diff --git a/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.md b/.agents/notes/implemented/process/2026-06-11-doc-sync-enforcement.md similarity index 99% rename from docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.md rename to .agents/notes/implemented/process/2026-06-11-doc-sync-enforcement.md index 8fd37a1656..8f32202c8f 100644 --- a/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.md +++ b/.agents/notes/implemented/process/2026-06-11-doc-sync-enforcement.md @@ -1,4 +1,4 @@ -# RFC: Doc-sync enforcement +# Agent Note: Doc-sync enforcement Status: implemented diff --git a/docs/rfc/implemented/process/2026-06-11-quality-gates.md b/.agents/notes/implemented/process/2026-06-11-quality-gates.md similarity index 93% rename from docs/rfc/implemented/process/2026-06-11-quality-gates.md rename to .agents/notes/implemented/process/2026-06-11-quality-gates.md index 9f92791f4a..1a1cfe5b54 100644 --- a/docs/rfc/implemented/process/2026-06-11-quality-gates.md +++ b/.agents/notes/implemented/process/2026-06-11-quality-gates.md @@ -1,4 +1,4 @@ -# RFC: Mechanical quality gates over prose guidelines +# Agent Note: Mechanical quality gates over prose guidelines Status: implemented @@ -23,4 +23,4 @@ Every AGENTS.md promise gets a command that exits non-zero, wired into git hooks - The gates themselves are code to maintain; config changes are reviewed like any change. - 100%-coverage pressure can produce assertion-free tests — mutation testing is the planned counterweight (see [the mutation-testing proposal](../../proposed/testing/2026-06-11-mutation-testing.md)). -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.md b/.agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md similarity index 78% rename from docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.md rename to .agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md index 1e63cef940..4075d4738a 100644 --- a/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.md +++ b/.agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md @@ -1,4 +1,4 @@ -# RFC: tsdown for JS bundling instead of dumble +# Agent Note: tsdown for JS bundling instead of dumble Status: implemented @@ -13,7 +13,7 @@ Build output currently matters only for `pnpm run build` + publint (nothing publ Replace dumble with **tsdown** (rolldown-based, ~2.5M downloads/week, VoidZero-backed, actively released): - Root `tsdown.config.ts` with `workspace: ['vendor/*', 'packages/*/*']` (explicit globs keep bundling to vendored Cordis and the TypeScript package tree; `workspace: true` would also discover example manifests and non-bundled workspace members). -- Shared shape: entry `lib/types/index.js`, `outDir: 'lib'`, ESM, `platform: node`, `target: es2024`, `fixedExtension: false` (keeps `.js` for `"type": "module"` packages), `dts: false` (tsc -b owns declarations), `clean: false` (lib/ also holds TSC's `lib/types` intermediate tree). The entry was originally `src/index.ts`; the [TSC-first build RFC](2026-06-17-ts-build-config.md) later moved tsdown to bundling TSC-emitted JS so TypeScript transform behavior comes from one compiler. +- Shared shape: entry `lib/types/index.js`, `outDir: 'lib'`, ESM, `platform: node`, `target: es2024`, `fixedExtension: false` (keeps `.js` for `"type": "module"` packages), `dts: false` (tsc -b owns declarations), `clean: false` (lib/ also holds TSC's `lib/types` intermediate tree). The entry was originally `src/index.ts`; the [TSC-first build Agent Note](2026-06-17-ts-build-config.md) later moved tsdown to bundling TSC-emitted JS so TypeScript transform behavior comes from one compiler. - Two per-package overrides in vendor/ (ours, like the regenerated tsconfigs; logged in vendor/README.md): schemastery (dual `.mjs`/`.cjs` via `outExtensions`), logger-console (two single-entry passes so the shared base class is inlined into each entry instead of a hash-named chunk, matching upstream's published shape). - `scripts/build.ts` deleted; `pnpm run build` = `tsc -b tsconfig.build.json && tsdown`. @@ -25,4 +25,4 @@ Replace dumble with **tsdown** (rolldown-based, ~2.5M downloads/week, VoidZero-b ## Consequences -Runtime bundle outputs still follow the dumble-era public entry shape (`lib/index.js`, plus package-specific variants such as `schemastery`'s `lib/index.mjs`/`lib/index.cjs` and `logger-console`'s `lib/browser.js`); declarations now live under `lib/types` per the [TSC-first build RFC](2026-06-17-ts-build-config.md). Externals still come from each package's dependencies/peerDependencies. We give up dumble's exports-field inference — new packages with non-default shapes need a per-package `tsdown.config.ts` instead of just package.json fields. Future option: tsdown could also absorb declaration bundling (isolatedDeclarations) if `tsc -b` ever becomes the bottleneck; that would be a new RFC. +Runtime bundle outputs still follow the dumble-era public entry shape (`lib/index.js`, plus package-specific variants such as `schemastery`'s `lib/index.mjs`/`lib/index.cjs` and `logger-console`'s `lib/browser.js`); declarations now live under `lib/types` per the [TSC-first build Agent Note](2026-06-17-ts-build-config.md). Externals still come from each package's dependencies/peerDependencies. We give up dumble's exports-field inference — new packages with non-default shapes need a per-package `tsdown.config.ts` instead of just package.json fields. Future option: tsdown could also absorb declaration bundling (isolatedDeclarations) if `tsc -b` ever becomes the bottleneck; that would be a new Agent Note. diff --git a/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.md b/.agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md similarity index 97% rename from docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.md rename to .agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md index 2aa24907d5..a8895ba5e8 100644 --- a/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.md +++ b/.agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md @@ -1,4 +1,4 @@ -# RFC: Vendor Cordis as source, not npm dependencies +# Agent Note: Vendor Cordis as source, not npm dependencies Status: implemented diff --git a/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md b/.agents/notes/implemented/process/2026-06-16-pnpm-over-yarn.md similarity index 99% rename from docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md rename to .agents/notes/implemented/process/2026-06-16-pnpm-over-yarn.md index 7a574a3388..f4a5d43f96 100644 --- a/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.md +++ b/.agents/notes/implemented/process/2026-06-16-pnpm-over-yarn.md @@ -1,4 +1,4 @@ -# RFC: pnpm as the package manager instead of Yarn 4 +# Agent Note: pnpm as the package manager instead of Yarn 4 Status: implemented diff --git a/docs/rfc/implemented/process/2026-06-17-ts-build-config.md b/.agents/notes/implemented/process/2026-06-17-ts-build-config.md similarity index 99% rename from docs/rfc/implemented/process/2026-06-17-ts-build-config.md rename to .agents/notes/implemented/process/2026-06-17-ts-build-config.md index 0687df250c..8f67b6fc2f 100644 --- a/docs/rfc/implemented/process/2026-06-17-ts-build-config.md +++ b/.agents/notes/implemented/process/2026-06-17-ts-build-config.md @@ -1,4 +1,4 @@ -# RFC: TSC-first build and one tsconfig +# Agent Note: TSC-first build and one tsconfig Status: implemented diff --git a/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.md b/.agents/notes/implemented/process/2026-06-18-markdown-cross-link-lint.md similarity index 81% rename from docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.md rename to .agents/notes/implemented/process/2026-06-18-markdown-cross-link-lint.md index db98c2fb7d..05dfed2453 100644 --- a/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.md +++ b/.agents/notes/implemented/process/2026-06-18-markdown-cross-link-lint.md @@ -1,4 +1,4 @@ -# RFC: Markdown cross-link validity linting +# Agent Note: Markdown cross-link validity linting Status: implemented @@ -6,7 +6,7 @@ Status: implemented Docs in this repo link to each other by relative path — `[topic](../implemented/2026-…-….md)`, `[the cookbook](adding-a-tool.md)`, `[architecture.md](../../architecture.md)`. Nothing verified those targets exist. A rename or a move silently breaks every inbound link, and the break is invisible until a reader clicks it. [Doc-sync enforcement](2026-06-11-doc-sync-enforcement.md) already mechanized two classes of doc drift (uncompilable code blocks, a stale event-taxonomy table) and [verify-md-wrap](2026-06-11-doc-sync-enforcement.md) a third (hard-wrapped prose) — but a dead cross-link is a fourth, equally mechanical class that was still verified by eyeball. -The motivating case is the RFC tree reorganization that introduced this gate: unifying `docs/adr/` + `docs/rfc/` into one `docs/rfc/` with `proposed/`/`implemented/`/`rejected/` subfolders renamed roughly forty inter-doc links by hand. A single fat-fingered path would have shipped a broken link with nothing to catch it. +The motivating case is the Agent Note tree reorganization that introduced this gate: unifying `docs/adr/` + `.agents/notes/` into one `.agents/notes/` with `proposed/`/`implemented/`/`rejected/` subfolders renamed roughly forty inter-doc links by hand. A single fat-fingered path would have shipped a broken link with nothing to catch it. ## Decision @@ -26,6 +26,6 @@ This gate checks *existence*, not anchor validity: a link to a real file with a ## Consequences -- Renames and moves that orphan a cross-link now fail the pre-push hook and CI instead of waiting for a reader to click a dead link. This made the RFC reorganization that introduced the gate self-verifying: the same PR that rewrote forty links also added the check that proves none dangle. +- Renames and moves that orphan a cross-link now fail the pre-push hook and CI instead of waiting for a reader to click a dead link. This made the Agent Note reorganization that introduced the gate self-verifying: the same PR that rewrote forty links also added the check that proves none dangle. - One more fast tsx script in the `doc-sync` chain; no new dependency (the mdast/GFM stack is already in devDependencies for `verify-md-wrap`). -- The convention this enforces — cross-reference docs by machine-checkable relative link, never by bare prose or a number — is documented in [docs/AGENTS.md](../../../AGENTS.md) so authors know the gate exists and why. +- The convention this enforces — cross-reference docs by machine-checkable relative link, never by bare prose or a number — is documented in [docs/AGENTS.md](../../../../docs/AGENTS.md) so authors know the gate exists and why. diff --git a/.agents/notes/implemented/process/2026-06-20-agent-note-classification.md b/.agents/notes/implemented/process/2026-06-20-agent-note-classification.md new file mode 100644 index 0000000000..42c4523e0c --- /dev/null +++ b/.agents/notes/implemented/process/2026-06-20-agent-note-classification.md @@ -0,0 +1,46 @@ +# Agent Note: Classify Agent Notes by kind via path-encoded subdirectories + +Status: implemented + +## Problem + +A lifecycle-only Agent Note tree — `proposed/` / `implemented/` / `rejected/` — does not record what *kind* of decision each file contains. A reader browsing one lifecycle cannot distinguish a new capability from a removal or a tooling-policy change without opening each file. + +The repo's standing bias is [mechanical quality gates over prose guidelines](2026-06-11-quality-gates.md): a convention that isn't machine-checked rots. So a classification scheme here had to be enforceable, not an honor-system header. + +## Decision + +Add a second axis — the Agent Note's **class** — and encode it in the path: `{lifecycle}/{class}/yyyy-mm-dd-topic.md`. The folder *is* the label. A file's location declares its class, the closed set is "these folders and no others," and the existing [verify-md-links](2026-06-18-markdown-cross-link-lint.md) gate already protects the path rewrites the move required. + +### The closed set of six classes + +| Class | Covers | +|---|---| +| `feature` | A new user- or model-facing capability. | +| `bug-fix` | Corrects a defect or closes a gap a postmortem surfaced. | +| `simplification` | Removes code, behavior, or surface area without adding a capability. | +| `architecture` | A structural decision about the **shipped source** — how packages relate, what the runtime vocabulary is. | +| `process` | Tooling, policy, or workflow **around** the code, not runtime behavior. | +| `testing` | Test infrastructure and strategy. | + +The `architecture` / `process` line: **architecture** is about the source we ship; **process** is the surrounding tooling and workflow. This Agent Note is itself a `process` decision — it changes how the repo is organized and gated, not what the harness does at runtime — so it lives under `implemented/process/`. + +### Two gates + +Both are `doc-sync` members, in the `verify-md-wrap` style (tsx ESM, verify-don't-generate, exit non-zero on the first violation): + +- **`scripts/verify-agent-note-classification.ts`** — the closed lifecycle and class sets. It asserts every file under a lifecycle folder lives in a class folder from the canonical set (a loose `.md` at a lifecycle root, or an unknown class folder, fails) and rejects a centralized `INDEX.md`. The canonical sets live in `scripts/agent-note-tree.ts`, and [the README](../../README.md) documents each class in prose. +- **`scripts/verify-doc-refs.ts`** — source comments that cite docs. Agent Note paths are referenced not only from Markdown but from TypeScript doc comments (root-relative prose like `.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md`). `verify-md-links` does not see those, so a reorganization could silently orphan them. This gate scans repo-authored `.ts` under `packages/**` and `examples/**` (excluding built `lib/` and `vendor/`) for `docs/….md` and `.agents/notes/….md` tokens, resolves each root-relative path, and asserts it exists. It requires the `.md` extension so extensionless prose is left alone. + +## Alternatives considered + +- **A `Classification:` prose line** in each file (next to `Status:`), parsed by the gate. Workable, but it duplicates into the file a fact the path can already carry, and a line can disagree with its folder. Path-encoding makes the label and its storage the same thing — there is nothing to keep in sync. +- **A `refactor` class.** It overlaps `simplification` almost entirely; the only discriminator anyone reached for was "does observable behavior change?", which `simplification` already encodes (it does not). One class, not two. +- **A generated or hand-maintained corpus index.** Rejected because the lifecycle/class tree is authoritative, while a centralized inventory creates a merge hotspot without providing discovery that tree navigation or repository search cannot provide. The separate [index proposal](../../rejected/process/2026-07-04-generate-agent-note-index-tables.md) records the discarded generated shape. + +## Consequences + +- Every Agent Note sits under a class folder. A reader can browse one folder to see all simplifications or all testing decisions within a lifecycle. +- Two more fast tsx scripts in the `doc-sync` chain; no new dependency (the mdast/GFM stack was already present for `verify-md-wrap`/`verify-md-links`). +- Adding a class is a deliberate act: amend the `const` in `scripts/agent-note-tree.ts` and the [Classification section](../../README.md#classification), not just `mkdir` a folder. The gate rejects an unknown folder, so an ad-hoc class can't slip in. +- Source-comment doc references are now gated too — a moved or renamed doc that a `.ts` comment cites fails the pre-push hook, closing a drift class `verify-md-links` structurally could not see. diff --git a/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.md b/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md similarity index 85% rename from docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.md rename to .agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md index 47dc370d22..21b86b1812 100644 --- a/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.md +++ b/.agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md @@ -1,12 +1,12 @@ -# RFC: Core-data-structures catalog and the `ts type-equiv` drift gate +# Agent Note: Core-data-structures catalog and the `ts type-equiv` drift gate Status: implemented ## Problem -A reader trying to understand the harness could find its *behavior* in [architecture.md](../../../architecture.md) (the service map, the session/turn/step lifecycle, the event taxonomy) but had no single place describing its *vocabulary* — the data structures that behavior moves around. The type shapes lived only in source, scattered across `packages/*/src/types.ts`, so understanding "what is a `Message`, a `SessionEvent`, a `StreamChunk`" meant reading the declarations directly. A prose catalog would help, but a catalog that paraphrases or paste-copies type definitions rots the instant a field changes — and an out-of-sync type doc is worse than none, because a reader trusts it. +A reader trying to understand the harness could find its *behavior* in [architecture.md](../../../../docs/architecture.md) (the service map, the session/turn/step lifecycle, the event taxonomy) but had no single place describing its *vocabulary* — the data structures that behavior moves around. The type shapes lived only in source, scattered across `packages/*/src/types.ts`, so understanding "what is a `Message`, a `SessionEvent`, a `StreamChunk`" meant reading the declarations directly. A prose catalog would help, but a catalog that paraphrases or paste-copies type definitions rots the instant a field changes — and an out-of-sync type doc is worse than none, because a reader trusts it. -So the work had two intertwined questions: **what belongs in such a catalog** (the scoping problem — a harness has dozens of cross-package types and dumping all of them helps no one), and **how to keep pasted type definitions from drifting** (the durability problem). This RFC records both decisions. Its sibling, [the generated cordis events + services catalog](2026-06-20-generated-cordis-catalog.md), is the *wiring*-axis complement: this one catalogs the data structures, that one the events and services that move them. +So the work had two intertwined questions: **what belongs in such a catalog** (the scoping problem — a harness has dozens of cross-package types and dumping all of them helps no one), and **how to keep pasted type definitions from drifting** (the durability problem). This Agent Note records both decisions. Its sibling, [the generated cordis events + services catalog](2026-06-20-generated-cordis-catalog.md), is the *wiring*-axis complement: this one catalogs the data structures, that one the events and services that move them. ## Decision @@ -48,7 +48,7 @@ The durability requirement was specific: the doc shows the **literal** current t The spine-vs-seam rule was tested against `BashExecRequest`, tool schemas and definitions, the schema DSL, presentation types, and the session/persistence split before adoption. -`verify-type-equiv` must scan the complete Markdown scope, not only manifest-named documents. Otherwise an unmanifested `type-equiv` block escapes the claimed one-to-one check. The gate therefore reports such blocks as orphans. This RFC records that fail-closed scan rule together with the spine-vs-seam and verbatim-match decisions; the generated Cordis catalog has the symmetric design record in [its RFC](2026-06-20-generated-cordis-catalog.md). +`verify-type-equiv` must scan the complete Markdown scope, not only manifest-named documents. Otherwise an unmanifested `type-equiv` block escapes the claimed one-to-one check. The gate therefore reports such blocks as orphans. This Agent Note records that fail-closed scan rule together with the spine-vs-seam and verbatim-match decisions; the generated Cordis catalog has the symmetric design record in [its Agent Note](2026-06-20-generated-cordis-catalog.md). ## Consequences diff --git a/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.md b/.agents/notes/implemented/process/2026-06-20-generated-cordis-catalog.md similarity index 95% rename from docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.md rename to .agents/notes/implemented/process/2026-06-20-generated-cordis-catalog.md index 879f8de701..7de7056b33 100644 --- a/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.md +++ b/.agents/notes/implemented/process/2026-06-20-generated-cordis-catalog.md @@ -1,4 +1,4 @@ -# RFC: Generated cordis events + services catalog +# Agent Note: Generated cordis events + services catalog Status: implemented @@ -6,7 +6,7 @@ Status: implemented A plugin author needs two reference surfaces that no single document gave them: every cordis **event** they can listen to (with its exact signature and dispatch mode) and every `ctx.<key>` **service** they can call (with its exact interface). The pieces existed but were scattered — a hand-maintained event-taxonomy *table* in `docs/architecture.md` (names + prose Mode/Purpose, name-set-checked by `verify-event-taxonomy`), a Service-map table (8 rows of role prose), and the `interface Events` / `interface Context` declarations themselves. The taxonomy table also could not catch a brand-new *undocumented* event: a name-set verifier only checks the names that are already in the table on both sides. -This is the wiring-axis complement to the [core-data-structures catalog](../../../core-data-structures/core.md) ([its RFC](2026-06-20-core-data-structures-catalog.md)): that one catalogs the *data structures* the loop moves around (verified hand-pastes); this one catalogs the *events and services* that move them. +This is the wiring-axis complement to the [core-data-structures catalog](../../../../docs/core-data-structures/core.md) ([its Agent Note](2026-06-20-core-data-structures-catalog.md)): that one catalogs the *data structures* the loop moves around (verified hand-pastes); this one catalogs the *events and services* that move them. ## Decision diff --git a/docs/rfc/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 similarity index 61% rename from docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml rename to .agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml index 5f83acc3aa..2e152a0072 100644 --- a/docs/rfc/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: 68c0f3bbc0472b0c96f9d64fc6b1b24ac7008795 -2026-07-02-bilingual-docs-and-pairing-gate.zh.md: 2cf8f9b9c17d8a521d8674833e909b34f315cfe0 +2026-07-02-bilingual-docs-and-pairing-gate.md: 45c6edff41a7bc21c76aeeaf14d16af824c601de +2026-07-02-bilingual-docs-and-pairing-gate.zh.md: 91ba7523705d1500150efe0eac9085ea980e80d6 diff --git a/docs/rfc/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 similarity index 83% rename from docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md rename to .agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md index 68c0f3bbc0..45c6edff41 100644 --- a/docs/rfc/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 @@ -1,4 +1,4 @@ -# RFC: Bilingual documentation via paired sibling files and a pairing gate +# Agent Note: Bilingual documentation via paired sibling files and a pairing gate Status: implemented @@ -10,14 +10,14 @@ This repo's README and docs tree are read by people and agents inside and outsid ## Decision -- **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](../../../i18n/README.md); translation rules: [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md); terminology source of truth: [docs/i18n/terminology.md](../../../i18n/terminology.md). +- **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](../../../../.agents/skills/dsh-translate-docs/SKILL.md), following the same pattern as [dsh-code-review](../../../../.agents/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. ## Alternatives considered -- **English as the canonical source with a fingerprint inside the translation** — the design first proposed for this RFC: `.zh.md` files carried an HTML comment recording the English source's blob hash, and translation flowed EN → ZH only. Revised in review: the team wants Chinese-first authoring (write and review a Chinese RFC, then translate to English) with the two languages holding equal authority, which a one-directional canonical model cannot express. The sidecar record covering BOTH sides replaced the in-file one-directional fingerprint; the blob-hash mechanics survived unchanged. +- **English as the canonical source with a fingerprint inside the translation** — the design first proposed for this Agent Note: `.zh.md` files carried an HTML comment recording the English source's blob hash, and translation flowed EN → ZH only. Revised in review: the team wants Chinese-first authoring (write and review a Chinese Agent Note, then translate to English) with the two languages holding equal authority, which a one-directional canonical model cannot express. The sidecar record covering BOTH sides replaced the in-file one-directional fingerprint; the blob-hash mechanics survived unchanged. - **Locale directories (`docs/en/` + `docs/zh/`, the Kubernetes/ECharts model)** — rejected: this repo has no docs-site framework to map locales to routes, moving every English file would churn every existing cross-reference, and `verify-md-links`/`verify-doc-refs` would need path-mapping logic instead of working unchanged. - **A separate translation repo (the PingCAP `docs`/`docs-cn` model)** — rejected: right for a docs product with independent release trains, overkill for a monorepo's own documentation; it also puts the translation outside the reach of this repo's gates. - **Interleaved bilingual files (single file, both languages)** — rejected: doubles every diff, breaks the one-line-per-paragraph convention's diff ergonomics, and makes partial inconsistency invisible. @@ -34,5 +34,5 @@ Paired sibling files with locale suffixes are the dominant Chinese big-tech conv - Every pair adds a third file to the tree. The record is machine-written (`--write`), so the cost is directory noise, not maintenance effort; in exchange, "who confirmed these consistent, and when" is answerable from git blame on the yaml. - When the two sides disagree, no mechanical rule picks a winner — the PR review does. That is the price of equal authority, accepted deliberately: the alternative (a canonical language) forbids Chinese-first authoring. - Generated docs (`cordis-catalog/`, `tool-catalog/`, `module-graph.md`) are excluded for now; the planned follow-up is to teach their generators to emit Chinese alongside English, at which point they leave the exclusion list. -- Rollout is incremental by design: documents outside `required` are visible backlog (`--list`), not red CI, so pairs land in reviewable batches without a big-bang PR. A date-named document dated on or after the manifest's `requiredSince` cutoff merges bilingual or not at all, so new date-named RFCs do not enlarge that backlog. +- Rollout is incremental by design: documents outside `required` are visible backlog (`--list`), not red CI, so pairs land in reviewable batches without a big-bang PR. A date-named document dated on or after the manifest's `requiredSince` cutoff merges bilingual or not at all, so new date-named Agent Notes do not enlarge that backlog. - The recorded hashes double as the update tool (`git cat-file -p <hash>` recovers either side's last-confirmed text for a minimal diff-based update), so re-translation of whole files is never forced by the mechanism. diff --git a/docs/rfc/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 similarity index 84% rename from docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md rename to .agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.zh.md index 2cf8f9b9c1..91ba752370 100644 --- a/docs/rfc/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 @@ -1,4 +1,4 @@ -# RFC:通过配对兄弟文件与配对门禁实现双语文档 +# Agent Note:通过配对兄弟文件与配对门禁实现双语文档 Status: implemented @@ -10,14 +10,14 @@ Status: implemented ## 决策 -- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `foo.md`、中文 `foo.zh.md`,以及一份一致性记录 `foo.i18n.yaml`。没有哪种语言是正典:一篇文档可以先用中文撰写和评审、之后再译成英文,反之亦可;约束配对的是:两侧必须表达相同的内容,且配对整体合并(两种语言加记录,绝不单独落一侧)。政策见 [docs/i18n/README.md](../../../i18n/README.md);翻译规则见 [docs/i18n/translation-rules.md](../../../i18n/translation-rules.md);术语真源见 [docs/i18n/terminology.md](../../../i18n/terminology.md)。 +- **配对兄弟文件,两种语言同权。** 一对文档由三个兄弟文件组成:英文 `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](../../../../.agents/skills/dsh-translate-docs/SKILL.md),与 [dsh-code-review](../../../../.agents/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(技能)承载工作流,并将文档作为真源。 ## 曾考虑的替代方案 -- **英文为正典源、指纹放在译文内**:本 RFC 最初提出的设计:`.zh.md` 文件携带一条 HTML 注释记录英文源的 blob hash,翻译只沿 EN → ZH 单向流动。评审中修订:团队需要中文先行的撰写方式(先写、先审中文 RFC,再译英文),两种语言同权,而单向正典模型无法表达这一点。覆盖**两侧**的伴随记录取代了文件内的单向指纹;blob hash 的机制本身保持不变。 +- **英文为正典源、指纹放在译文内**:本 Agent Note 最初提出的设计:`.zh.md` 文件携带一条 HTML 注释记录英文源的 blob hash,翻译只沿 EN → ZH 单向流动。评审中修订:团队需要中文先行的撰写方式(先写、先审中文 Agent Note,再译英文),两种语言同权,而单向正典模型无法表达这一点。覆盖**两侧**的伴随记录取代了文件内的单向指纹;blob hash 的机制本身保持不变。 - **语言目录(`docs/en/` + `docs/zh/`,Kubernetes/ECharts 模式)**:否决。本仓库没有将 locale 映射到路由的文档站框架;如果移动所有英文文件,所有既有交叉引用都要随之修改;且 `verify-md-links`/`verify-doc-refs` 将需要路径映射逻辑,而非原样工作。 - **独立翻译仓库(PingCAP `docs`/`docs-cn` 模式)**:否决。适合有独立发布节奏的文档产品,对 monorepo 自身的文档而言过重;还会把译文置于本仓库门禁触及不到的地方。 - **中英混排单文件(一个文件、两种语言)**:否决。每个 diff 都翻倍,破坏一段一行约定的 diff 易读性,且局部不一致不可见。 @@ -34,5 +34,5 @@ Status: implemented - 每个配对给目录树多添一个文件。记录由机器写入(`--write`),代价是目录噪音而非维护负担;换来的是「谁在何时确认过这对文档一致」可以从 yaml 的 git blame 直接回答。 - 两侧说法冲突时,没有机械规则裁决谁赢,由 PR 评审裁决。这是同权的代价,且是有意接受的:另一个选项(正典语言)会禁止中文先行撰写。 - 生成文档(`cordis-catalog/`、`tool-catalog/`、`module-graph.md`)暂被排除;计划中的后续工作是让生成器在输出英文的同时输出中文,届时将这些文件移出排除清单。 -- 推进天然是渐进的:`required` 之外的文档是可见的 backlog(待翻清单,`--list`),而非红色的 CI;因此配对按可评审的批次落地,无需一个巨型 PR。凡文件名以日期开头且日期不早于 manifest 中 `requiredSince` 分界日期的文档,都必须配齐双语文件,因此新建的日期命名 RFC 不会增加这份 backlog。 +- 推进天然是渐进的:`required` 之外的文档是可见的 backlog(待翻清单,`--list`),而非红色的 CI;因此配对按可评审的批次落地,无需一个巨型 PR。凡文件名以日期开头且日期不早于 manifest 中 `requiredSince` 分界日期的文档,都必须配齐双语文件,因此新建的日期命名 Agent Note 不会增加这份 backlog。 - 记录的 hash 兼作更新工具(`git cat-file -p <hash>` 能还原任一侧上次确认的文本,用于基于 diff 的最小更新),因此这套机制从不强迫整篇重译。 diff --git a/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md b/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md similarity index 96% rename from docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md rename to .agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md index 7da1a05a3a..06ab61732c 100644 --- a/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md +++ b/.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md @@ -1,4 +1,4 @@ -# RFC: Generated tool-schema catalog (boot-and-harvest) +# Agent Note: Generated tool-schema catalog (boot-and-harvest) Status: implemented @@ -19,7 +19,7 @@ The cordis catalog is a pure TypeScript-AST pass because every event/service nam - `tool-subagent`'s tool name is `config.toolName ?? 'subagent'` — chosen at load, not a literal. - An MCP plugin can register **raw JSON Schema** directly via `ctx.tools.register()` without `defineTool` at all, so enumerating `defineTool(` call sites structurally under-counts. -The only faithful source of truth is the schema the registry actually holds after the plugin loads. Booting is the [testing-policy discipline](../../../testing.md) "verify the world, not the self-report" applied to a doc generator: read the shipped artifact, not a re-derivation of it. +The only faithful source of truth is the schema the registry actually holds after the plugin loads. Booting is the [testing-policy discipline](../../../../docs/testing.md) "verify the world, not the self-report" applied to a doc generator: read the shipped artifact, not a re-derivation of it. ### Restoring "nothing silently omitted" diff --git a/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md b/.agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md similarity index 74% rename from docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md rename to .agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md index 4e3148ead6..4f3fafe59d 100644 --- a/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md +++ b/.agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md @@ -1,10 +1,10 @@ -# RFC: Documentation graph index for maintainers and SDK users +# Agent Note: Documentation graph index for maintainers and SDK users Status: implemented ## Problem -The repo already had several high-trust documentation surfaces, each on a different axis: [module-graph.md](../../../module-graph.md) is generated from package `peerDependencies`, the generated [Cordis events](../../../cordis-catalog/events.md) and [services](../../../cordis-catalog/services.md) catalogs are generated from Cordis `Events` and `Context` declarations, [tool-catalog.md](../../../tool-catalog.md) is generated by booting shipped tool plugins, and [core-data-structures/](../../../core-data-structures/core.md) uses `ts type-equiv` blocks to keep pasted type definitions synchronized with source. +The repo already had several high-trust documentation surfaces, each on a different axis: [module-graph.md](../../../../docs/module-graph.md) is generated from package `peerDependencies`, the generated [Cordis events](../../../../docs/cordis-catalog/events.md) and [services](../../../../docs/cordis-catalog/services.md) catalogs are generated from Cordis `Events` and `Context` declarations, [tool-catalog.md](../../../../docs/tool-catalog.md) is generated by booting shipped tool plugins, and [core-data-structures/](../../../../docs/core-data-structures/core.md) uses `ts type-equiv` blocks to keep pasted type definitions synchronized with source. Those references are accurate, but they are mostly catalogs. A maintainer still has to synthesize the relationships: which packages form a capability seam, which app bundles a concrete spine, which event is durable vs live, where a hook or policy plugin can intercept work, and which model-facing tool depends on which service. An SDK user has the same problem from another angle: "Which package do I install or load for the behavior I want, and which event/service/tool do I extend?" @@ -12,7 +12,7 @@ The hooks subsystem makes event producer/consumer topology and interception poin ## Decision -Add generated relationship graph docs, indexed at [docs/graph-atlas.md](../../../graph-atlas.md), produced by focused generators and verified by `pnpm run verify-doc-graphs` / existing catalog freshness checks as part of `doc-sync`. +Add generated relationship graph docs, indexed at [docs/graph-atlas.md](../../../../docs/graph-atlas.md), produced by focused generators and verified by `pnpm run verify-doc-graphs` / existing catalog freshness checks as part of `doc-sync`. The index is a relationship layer above the existing catalogs. It does not replace exact references; instead, it links to them and explains how their pieces fit together. @@ -30,15 +30,15 @@ The first index links ten relationship surfaces. Package topology and tool-packa | Graph | Maintenance mode | Source of truth | |---|---|---| -| [module dependency graph](../../../module-graph.md) | generated | `packages/*/*/package.json` peer dependencies plus package group paths | -| [tool schema catalog and package map](../../../tool-catalog.md) | generated | boot-harvested tool schemas plus tool-package service/effect metadata | -| [capability seams and core services](../../../capability-seams.md) | hybrid generated | Cordis service declarations plus a role manifest in `gen-doc-graphs.ts` | +| [module dependency graph](../../../../docs/module-graph.md) | generated | `packages/*/*/package.json` peer dependencies plus package group paths | +| [tool schema catalog and package map](../../../../docs/tool-catalog.md) | generated | boot-harvested tool schemas plus tool-package service/effect metadata | +| [capability seams and core services](../../../../docs/capability-seams.md) | hybrid generated | Cordis service declarations plus a role manifest in `gen-doc-graphs.ts` | | [echo-agent app composition](../../../../examples/echo-agent/composition.md) | hybrid generated | `examples/echo-agent/cordis.yml` plugin list plus curated app/bundle expansion | | [repl-agent app composition](../../../../examples/repl-agent/composition.md) | hybrid generated | `examples/repl-agent/cordis.yml` plugin list plus curated app/bundle expansion | | [acp-agent app composition](../../../../examples/acp-agent/composition.md) | hybrid generated | `examples/acp-agent/cordis.yml` plugin list plus curated app/bundle expansion | -| [event producer/consumer matrix](../../../event-producer-consumer.md) | hybrid generated | Cordis event declarations, AST-scanned `ctx.on/emit/parallel/serial/waterfall` sites, and explicit dynamic dispatch overrides | -| [agent turn and step lifecycle](../../../agent-lifecycle.md) | curated | architecture.md loop lifecycle, Cordis catalog links, and session event semantics | -| [tool execution pipeline](../../../tool-execution-pipeline.md) | curated | tool pipeline semantics and the `tools/execute` waterfall | +| [event producer/consumer matrix](../../../../docs/event-producer-consumer.md) | hybrid generated | Cordis event declarations, AST-scanned `ctx.on/emit/parallel/serial/waterfall` sites, and explicit dynamic dispatch overrides | +| [agent turn and step lifecycle](../../../../docs/agent-lifecycle.md) | curated | architecture.md loop lifecycle, Cordis catalog links, and session event semantics | +| [tool execution pipeline](../../../../docs/tool-execution-pipeline.md) | curated | tool pipeline semantics and the `tools/execute` waterfall | | [ACP snapshot replay](../../../../packages/ui/acp/snapshot-replay.md) | curated | snapshot harness behavior | ### Why generators own the docs diff --git a/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md b/.agents/notes/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md similarity index 98% rename from docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md rename to .agents/notes/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md index 1c7e515c4f..882ab8cf82 100644 --- a/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md +++ b/.agents/notes/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md @@ -1,4 +1,4 @@ -# RFC: JSDoc completeness gate for the cordis surface +# Agent Note: JSDoc completeness gate for the cordis surface Status: implemented diff --git a/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.md b/.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md similarity index 74% rename from docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.md rename to .agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md index 3f88e43c1d..68e055f0a2 100644 --- a/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.md +++ b/.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md @@ -1,17 +1,17 @@ -# RFC: Documentation tiers, budgets, and the ceiling gate +# Agent Note: Documentation tiers, budgets, and the ceiling gate Status: implemented ## Problem -Standing docs accumulated repeated rules, retold incidents, duplicated package maps, and stale RFC summaries despite existing writing guidance. Because review alone did not prevent that growth, the repository needed a mechanical budget alongside its documentation taxonomy. +Standing docs accumulated repeated rules, retold incidents, duplicated package maps, and stale Agent Note summaries despite existing writing guidance. Because review alone did not prevent that growth, the repository needed a mechanical budget alongside its documentation taxonomy. ## Decision -- **A tier taxonomy with one home per fact.** [docs/AGENTS.md](../../../AGENTS.md) is the documentation standard: it assigns every Markdown tier a single job (standing orders, system map, type catalog, decision records, incident stories, how-tos, per-package contracts, generated catalogs, workflows), forbids restating a fact outside its home tier (link instead), and carries the slop checklist used when writing or reviewing any doc. -- **A narrow, hard budget gate.** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) joins `doc-sync`: every doc listed in [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) must stay under its word ceiling (`wc -w` semantics, whole file), and a budgeted file that is missing fails the gate so a rename cannot silently orphan its budget. Scope is deliberately only the accretion-prone standing docs — the root and subtree `AGENTS.md` files, `architecture.md`, `packages/README.md`, and the standing policy docs they evict content into (`docs/testing.md`, `docs/defensive-patterns.md`). Reference docs, RFCs, and package READMEs are unbudgeted: length is legitimate there when every row is a fact, and review plus the slop checklist govern them. +- **A tier taxonomy with one home per fact.** [docs/AGENTS.md](../../../../docs/AGENTS.md) is the documentation standard: it assigns every Markdown tier a single job (standing orders, system map, type catalog, decision records, incident stories, how-tos, per-package contracts, generated catalogs, workflows), forbids restating a fact outside its home tier (link instead), and carries the slop checklist used when writing or reviewing any doc. +- **A narrow, hard budget gate.** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) joins `doc-sync`: every doc listed in [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) must stay under its word ceiling (`wc -w` semantics, whole file), and a budgeted file that is missing fails the gate so a rename cannot silently orphan its budget. Scope is deliberately only the accretion-prone standing docs — the root and subtree `AGENTS.md` files, `architecture.md`, `packages/README.md`, and the standing policy docs they evict content into (`docs/testing.md`, `docs/defensive-patterns.md`). Reference docs, Agent Notes, and package READMEs are unbudgeted: length is legitimate there when every row is a fact, and review plus the slop checklist govern them. - **Ceilings are an enforcement frontier that ratchets.** A ceiling sits at least 5% above the doc's current size — working headroom, so routine wording edits pass while real growth still trips the gate — and ratchets down, keeping that margin, as the doc is brought to its target budget (root `AGENTS.md` ≤ 1,500 words; `architecture.md` ≤ 1,800; subtree `AGENTS.md` ≤ 600; `packages/README.md` ≤ 600) — the same rollout mechanism as the [translation-pairing `required` list](2026-07-02-bilingual-docs-and-pairing-gate.md). When the gate goes red the fix is to relocate or condense per the taxonomy; raising a ceiling is permitted only with explicit justification in the PR description, the manifest diff being the reviewable act. -- **A thin workflow skill, contracts in docs.** [.agents/skills/dsh-doc-standards](../../../../.agents/skills/dsh-doc-standards/SKILL.md) carries the placement/audit/red-gate workflow and defers to the standard as its source of truth, the same split as [dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md) over the i18n contract. +- **A thin workflow skill, contracts in docs.** [.agents/skills/dsh-doc-standards](../../../skills/dsh-doc-standards/SKILL.md) carries the placement/audit/red-gate workflow and defers to the standard as its source of truth, the same split as [dsh-translate-docs](../../../skills/dsh-translate-docs/SKILL.md) over the i18n contract. ## Alternatives considered diff --git a/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.md b/.agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md similarity index 98% rename from docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.md rename to .agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md index 70a6d60c1a..bd16baf8f6 100644 --- a/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.md +++ b/.agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md @@ -1,4 +1,4 @@ -# RFC: Generated persistence log event catalog +# Agent Note: Generated persistence log event catalog Status: implemented diff --git a/.agents/notes/implemented/process/2026-07-05-uniform-agent-note-format.md b/.agents/notes/implemented/process/2026-07-05-uniform-agent-note-format.md new file mode 100644 index 0000000000..1a6aa40477 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-05-uniform-agent-note-format.md @@ -0,0 +1,28 @@ +# Agent Note: One gated in-file format for Agent Notes + +Status: implemented + +## Problem + +Agent Note paths encoded lifecycle and class, but file contents still mixed headings, status formats, ADR and proposal templates, and proposal-era sections in implemented records. Authors copied whichever neighbor they found, and lifecycle moves could skip the required rewrite because no gate enforced an in-file contract. + +## Decision + +[README.md § The file format](../../README.md#the-file-format) is the in-file contract — the header block (`# Agent Note: <title>` plus a dateless, folder-agreeing `Status:` enum whose only content is the rejection reason), the per-lifecycle body skeleton (`Problem` opener everywhere; `Proposal`/`Acceptance criteria`/`Risks` in `proposed/`; present-tense `Decision`/`Consequences` with proposal-era headings banned in `implemented/`; frozen proposal shape in `rejected/`), a mandatory `Alternatives considered` section, and the canonical section vocabulary between which bespoke technical sections stay free-form. `pnpm run verify-agent-note-format` ([scripts/verify-agent-note-format.ts](../../../../scripts/verify-agent-note-format.ts)) enforces every mechanical clause as part of `doc-sync`, so a lifecycle move that skips its rewrite now fails CI instead of review memory. + +The whole corpus was normalized in the same change that defined the format — the pre-release stance: no transition period, no dual-format tolerance. The one grandfather is content, not format: alternatives are recorded, never invented, so a pre-format Agent Note whose alternatives are not reconstructible from the record carries the exact `agent-note-format: alternatives-not-recorded` comment, which the gate accepts only for files dated before this Agent Note. + +## Alternatives considered + +- **A full rigid template** (one fixed section sequence per lifecycle, every Agent Note restructured to fit) — rejected: the big design Agent Notes carry eight to fifteen bespoke technical sections (package topology, wire contracts, schemas) that are load-bearing content, not drift; a rigid sequence would force destructive rewrites now and template-fighting forever. +- **Header-only normalization** (H1 and Status, bodies untouched) — rejected: the debt markers flagged the *body* genre split, and leaving `Context`/`Decision` beside `Problem`/`Proposal` indefinitely resolves nothing. +- **No Status line** (the folder already is the status; the three newest pre-format Agent Notes (and the zh counterpart of one) omitted the line) — rejected in favor of keeping a self-describing file: the drift risk that motivated dropping it is neutralized by gating the line against the folder instead. +- **Dated status** (`Status: implemented (accepted YYYY-MM-DD)`) — rejected: the acceptance date is narrated history the writing rules keep out of docs; the filename carries first-proposed, git carries the rest, and the gate could check a date's format but never its truth. +- **A bare `# <title>` H1** — rejected: the `Agent Note: ` prefix self-describes the genre when a file is read outside its tree, and the format gate prevents it from drifting. +- **`## What we give up` as the implemented closer** (the README's own phrase for what an Agent Note records) — rejected: it names only costs, and an honest consequences section records what the trade-off bought as well. +- **Convention without a gate** (write the contract down, enforce by review) — rejected: the slop checklist already outlawed spec-speak in `implemented/` by convention, and nineteen files show what convention alone achieves here. +- **A standalone `FORMAT.md` contract file** — rejected because one front door carrying layout, classification, and format is easier to discover and maintain than two contract files. + +## Consequences + +Every Agent Note now costs slightly more structure, and the mandatory `Alternatives considered` section is deliberate friction: a decision recorded without what it beat invites the re-litigation Agent Notes exist to prevent. Pre-format Agent Notes whose alternatives were not reconstructible carry the grandfather comment permanently — an honest gap on the record rather than fabricated rationale. `doc-sync` gains one gate, and moving an Agent Note between lifecycle folders is now real work at move time (the body rewrite the move always owed) instead of deferred cleanup nothing tracked. The thirty-nine debt markers are gone, resolved by the template they were waiting for. diff --git a/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md b/.agents/notes/implemented/process/2026-07-06-export-surface-jsdoc-gate.md similarity index 99% rename from docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md rename to .agents/notes/implemented/process/2026-07-06-export-surface-jsdoc-gate.md index 6ebb477dea..452c877182 100644 --- a/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md +++ b/.agents/notes/implemented/process/2026-07-06-export-surface-jsdoc-gate.md @@ -1,4 +1,4 @@ -# RFC: Export-surface JSDoc gate +# Agent Note: Export-surface JSDoc gate Status: implemented diff --git a/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.md b/.agents/notes/implemented/process/2026-07-06-generated-config-catalog.md similarity index 93% rename from docs/rfc/implemented/process/2026-07-06-generated-config-catalog.md rename to .agents/notes/implemented/process/2026-07-06-generated-config-catalog.md index 999ebd8503..82355ab511 100644 --- a/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.md +++ b/.agents/notes/implemented/process/2026-07-06-generated-config-catalog.md @@ -1,4 +1,4 @@ -# RFC: Generated plugin config catalog +# Agent Note: Generated plugin config catalog Status: implemented @@ -8,7 +8,7 @@ The repository had no source-backed reference for plugin configuration. Package ## Decision -`scripts/gen-config-catalog.ts` emits [docs/config-catalog.md](../../../config-catalog.md) from each plugin's declared config type and JSDoc, with injection requirements, referenced-type links, and a source pointer. Package-local types are included transitively; workspace and external types are linked or named. Deterministic `--write` and `--check` modes make the committed page a generated artifact. +`scripts/gen-config-catalog.ts` emits [docs/config-catalog.md](../../../../docs/config-catalog.md) from each plugin's declared config type and JSDoc, with injection requirements, referenced-type links, and a source pointer. Package-local types are included transitively; workspace and external types are linked or named. Deterministic `--write` and `--check` modes make the committed page a generated artifact. Pure AST generation is correct here for the same reason it is for the events/services catalog and NOT for the tool catalog: a config type is a static declaration and every schemastery schema in the repo is a static `z.object`/`z.intersect` literal, so the source is the whole truth — nothing about the config surface is runtime-composed. diff --git a/docs/rfc/implemented/process/2026-07-06-node-engine-floor.md b/.agents/notes/implemented/process/2026-07-06-node-engine-floor.md similarity index 96% rename from docs/rfc/implemented/process/2026-07-06-node-engine-floor.md rename to .agents/notes/implemented/process/2026-07-06-node-engine-floor.md index af08c96be3..59f4c347eb 100644 --- a/docs/rfc/implemented/process/2026-07-06-node-engine-floor.md +++ b/.agents/notes/implemented/process/2026-07-06-node-engine-floor.md @@ -1,4 +1,4 @@ -# RFC: Raise the Node LTS engine floor to 22.19 +# Agent Note: Raise the Node LTS engine floor to 22.19 Status: implemented @@ -24,7 +24,7 @@ Those source features clear on the 22.x line at **22.18**, but the installed Pi - The advertised LTS branch no longer undercuts the Pi adapter dependency floor. - CI proves the Node 22 LTS floor directly with Node 22.19, keeps the Node 24 branch on `node: 24`, and keeps Node 26 for the next even line; each leg typechecks the source graph and launches the unbuilt workflow worker for real. - The built-bin smoke needs no version-conditional flag: at 22.19 type-stripping is already the default, so the test stays the plain `node lib/bin.js` path it documents. -- A future dependency or source API that raises the runtime floor must move `engines.node`, the compatibility matrix, and this RFC in the same change. +- A future dependency or source API that raises the runtime floor must move `engines.node`, the compatibility matrix, and this Agent Note in the same change. ## Alternatives considered diff --git a/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.md b/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md similarity index 99% rename from docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.md rename to .agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md index be31a439c1..65ad437ca9 100644 --- a/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.md +++ b/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md @@ -1,4 +1,4 @@ -# RFC: Parallel GitHub CI gates +# Agent Note: Parallel GitHub CI gates Status: implemented diff --git a/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.md b/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md similarity index 94% rename from docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.md rename to .agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md index 4e27b111d6..60472e51ec 100644 --- a/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.md +++ b/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md @@ -1,4 +1,4 @@ -# RFC: Parallel pre-push gates +# Agent Note: Parallel pre-push gates Status: implemented @@ -14,7 +14,7 @@ Flattening those members directly into `lefthook.yml` solves the local hook only [lefthook.yml](../../../../lefthook.yml) keeps one pre-push job named `full check` and runs `pnpm run check:pre-push`. That package script delegates to [scripts/run-gates.ts](../../../../scripts/run-gates.ts), the same bounded scheduler CI uses. -The `pre-push` mode expands into leaf gates for the unit suite, snapshot suite, build, `hygiene` members, `doc-sync` members, and module-graph freshness. The leaf list keeps the same gate vocabulary as the package scripts, including RFC classification and RFC format, while the runner schedules independent checks with four active top-level workers by default; `DSH_GATE_CONCURRENCY` overrides that bound. +The `pre-push` mode expands into leaf gates for the unit suite, snapshot suite, build, `hygiene` members, `doc-sync` members, and module-graph freshness. The leaf list keeps the same gate vocabulary as the package scripts, including Agent Note classification and Agent Note format, while the runner schedules independent checks with four active top-level workers by default; `DSH_GATE_CONCURRENCY` overrides that bound. The build gate makes the hook self-contained from a clean worktree. `publint`, `verify-node-next-types`, and the pre-push form of `doc-typecheck` wait for that build output, while source-only gates continue in parallel. diff --git a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md b/.agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md similarity index 57% rename from docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md rename to .agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md index 6cd8851149..0d294feb0f 100644 --- a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md +++ b/.agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md @@ -1,18 +1,18 @@ -# RFC: A gated Known-Limitations section in every package README +# Agent Note: A gated Known-Limitations section in every package README Status: implemented ## Problem -The [documentation standard](../../../AGENTS.md) assigns limitations to package READMEs. Without a shared shape, an omitted section cannot distinguish an audited absence from forgotten documentation, and variant headings prevent a repository-wide search. +The [documentation standard](../../../../docs/AGENTS.md) assigns limitations to package READMEs. Without a shared shape, an omitted section cannot distinguish an audited absence from forgotten documentation, and variant headings prevent a repository-wide search. ## Decision -Every package manifest under `packages/<group>/<pkg>/package.json` has a sibling README with the canonical `## Known Limitations and Deferred Work` section. Its bullets record durable consumer gaps and non-obvious maintainer constraints owned by that package; ordinary cleanup remains in its source TODO or owning RFC. The [`verify-package-readme-limitations` gate](../../../../scripts/verify-package-readme-limitations.ts) derives the package set from manifests, rejects missing READMEs, and requires exactly one canonical h2 with at least one top-level bullet. Near-miss headings such as “Limitations,” “Deferred,” “What is NOT here,” or “Non-goals” fail. +Every package manifest under `packages/<group>/<pkg>/package.json` has a sibling README with the canonical `## Known Limitations and Deferred Work` section. Its bullets record durable consumer gaps and non-obvious maintainer constraints owned by that package; ordinary cleanup remains in its source TODO or owning Agent Note. The [`verify-package-readme-limitations` gate](../../../../scripts/verify-package-readme-limitations.ts) derives the package set from manifests, rejects missing READMEs, and requires exactly one canonical h2 with at least one top-level bullet. Near-miss headings such as “Limitations,” “Deferred,” “What is NOT here,” or “Non-goals” fail. A package with nothing to declare is listed in `NO_LIMITATIONS` and omits the section. Adding a limitation requires removing the entry; renames and removals fail because every entry must name a scanned package. -The gate checks presence, shape, and the allowlist. Review under the documentation and [prose](../../../../.agents/skills/dsh-prose-standard/SKILL.md) standards owns coverage and accuracy. The standing rule lives in [packages/AGENTS.md](../../../../packages/AGENTS.md). +The gate checks presence, shape, and the allowlist. Review under the documentation and [prose](../../../skills/dsh-prose-standard/SKILL.md) standards owns coverage and accuracy. The standing rule lives in [packages/AGENTS.md](../../../../packages/AGENTS.md). ## Alternatives considered diff --git a/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md b/.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md similarity index 88% rename from docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md rename to .agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md index d0844f4f3b..dd986f3661 100644 --- a/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md +++ b/.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md @@ -1,4 +1,4 @@ -# RFC: Package Model Experience contract +# Agent Note: Package Model Experience contract Status: implemented @@ -8,9 +8,9 @@ A package README can explain APIs and runtime mechanics without answering the qu ## Decision -Every workspace package README with a model-facing or model-adjacent contract ends with the canonical [Model Experience section](../../../cookbook/adding-a-package.md#4-write-the-package-readme), immediately before `## Known Limitations and Deferred Work`; a package on the no-limitations allowlist ends with Model Experience itself. An audited model-agnostic generic package omits the section through `NO_MODEL_EXPERIENCE_SECTION`. +Every workspace package README with a model-facing or model-adjacent contract ends with the canonical [Model Experience section](../../../../docs/cookbook/adding-a-package.md#4-write-the-package-readme), immediately before `## Known Limitations and Deferred Work`; a package on the no-limitations allowlist ends with Model Experience itself. An audited model-agnostic generic package omits the section through `NO_MODEL_EXPERIENCE_SECTION`. -Packages with direct, conditional, capped, lifetime, multi-surface, or auxiliary-model effects use one H3 per context surface. Each surface contains three ordered H4 fields—`What the model sees`, `Token effect`, and `KV Cache effect`—and each field starts with one prose paragraph. The cache field distinguishes append-only growth, a stable repeated prefix, replacement of earlier tokens, and an independent model request; it names every package-owned configuration, scope, lifecycle, compaction, or routing change that can alter the request before newly appended content. “Does not invalidate” means the package preserves an already-reusable prefix, not that a provider promises a cache hit or retention period. Stable package-owned text is quoted exactly: system-prompt prose and other long literals use a titled H5 plus `markdown` fence under the field that introduces them, normally `What the model sees`, while short literals stay inline with named interpolation placeholders. Tool-schema surfaces link their anchored section in the generated [tool catalog](../../../tool-catalog.md) and state only composition or configuration deltas; runtime-only definitions explain why the catalog omits them. Data-dependent and provider-owned text is summarized. Agent-scoped visibility is explicit, and prompt and schema surfaces remain separate when scoping can hide one without the other. +Packages with direct, conditional, capped, lifetime, multi-surface, or auxiliary-model effects use one H3 per context surface. Each surface contains three ordered H4 fields—`What the model sees`, `Token effect`, and `KV Cache effect`—and each field starts with one prose paragraph. The cache field distinguishes append-only growth, a stable repeated prefix, replacement of earlier tokens, and an independent model request; it names every package-owned configuration, scope, lifecycle, compaction, or routing change that can alter the request before newly appended content. “Does not invalidate” means the package preserves an already-reusable prefix, not that a provider promises a cache hit or retention period. Stable package-owned text is quoted exactly: system-prompt prose and other long literals use a titled H5 plus `markdown` fence under the field that introduces them, normally `What the model sees`, while short literals stay inline with named interpolation placeholders. Tool-schema surfaces link their anchored section in the generated [tool catalog](../../../../docs/tool-catalog.md) and state only composition or configuration deltas; runtime-only definitions explain why the catalog omits them. Data-dependent and provider-owned text is summarized. Agent-scoped visibility is explicit, and prompt and schema surfaces remain separate when scoping can hide one without the other. A package with no model-context effect, or one path rendered entirely by another package, uses the verifier's audited short form: one sentence beginning `None, as ` or `Indirectly, through ` followed by a `KV Cache effect` H4 and one prose paragraph. Pure transport and keyless test-support packages use the none form when they create no model-bound content. Provider backends use the indirect form even when they cap or filter data, and wiring bundles use it when named children own every effect. These sections locate the contribution and disclaim direct cache invalidation without restating the consumer. Structured sections likewise document only package-owned inputs, transformations, and deltas. 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/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml similarity index 59% rename from docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml rename to .agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml index eb411f1158..366faaf6a3 100644 --- a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.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-14-typescript-program-backed-semantic-gates.md: 3e7a76e86d83080ae1a4f91ca97cc9749c90ef29 -2026-07-14-typescript-program-backed-semantic-gates.zh.md: 0a13452012e7f6cbd3ad7994845ba1985355c089 +2026-07-14-typescript-program-backed-semantic-gates.md: f9c00a4b6a5e9f08c11902e9267e4c1a954cebf8 +2026-07-14-typescript-program-backed-semantic-gates.zh.md: ce1f1edc765f621ca9f650720aa2db43f636e330 diff --git a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md similarity index 99% rename from docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md rename to .agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md index 3e7a76e86d..f9c00a4b6a 100644 --- a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md +++ b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md @@ -1,4 +1,4 @@ -# RFC: TypeScript Program-backed semantic gates +# Agent Note: TypeScript Program-backed semantic gates Status: implemented diff --git a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md similarity index 99% rename from docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md rename to .agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md index 0a13452012..ce1f1edc76 100644 --- a/docs/rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md +++ b/.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.zh.md @@ -1,4 +1,4 @@ -# RFC: 基于 TypeScript Program 的语义门禁 +# Agent Note: 基于 TypeScript Program 的语义门禁 Status: implemented diff --git a/docs/rfc/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml similarity index 62% rename from docs/rfc/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml rename to .agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml index 9424f2aa24..31ec0bdb07 100644 --- a/docs/rfc/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-17-run-ci-examples-from-built-lib.md: aae88ee965b4e2211f3a53aeb9c0ee4944d95c2a -2026-07-17-run-ci-examples-from-built-lib.zh.md: 0cd3822a71b8f7399be93beaac74b2177fcbe7ca +2026-07-17-run-ci-examples-from-built-lib.md: 22f69ed56bfc479281648bfb40df5acbd129ebc0 +2026-07-17-run-ci-examples-from-built-lib.zh.md: 74b985f578dd25f785e556c0cd493a7a9292fc43 diff --git a/docs/rfc/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md similarity index 98% rename from docs/rfc/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md rename to .agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md index aae88ee965..22f69ed56b 100644 --- a/docs/rfc/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md +++ b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.md @@ -1,4 +1,4 @@ -# RFC: Run CI examples from built lib +# Agent Note: Run CI examples from built lib Status: implemented diff --git a/docs/rfc/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md similarity index 98% rename from docs/rfc/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md rename to .agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md index 0cd3822a71..74b985f578 100644 --- a/docs/rfc/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md +++ b/.agents/notes/implemented/process/2026-07-17-run-ci-examples-from-built-lib.zh.md @@ -1,4 +1,4 @@ -# RFC: 在 CI 中从构建后的 lib 运行示例 +# Agent Note: 在 CI 中从构建后的 lib 运行示例 Status: implemented diff --git a/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.i18n.yaml b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.i18n.yaml new file mode 100644 index 0000000000..2b1dc53f6b --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-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 +2026-07-19-remove-generated-agent-note-index.md: 27c1591b29a1ca64370de6ffadfb9c524a804ced +2026-07-19-remove-generated-agent-note-index.zh.md: 868955bc10900f784bd88066042abe24454e27b5 diff --git a/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.md b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.md new file mode 100644 index 0000000000..27c1591b29 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.md @@ -0,0 +1,33 @@ +# Agent Note: Keep Agent Notes discoverable without a generated index + +Status: implemented + +English | [中文](2026-07-19-remove-generated-agent-note-index.zh.md) + +## Problem + +A committed Agent Note index duplicates facts already encoded by each file's lifecycle/class path, filename date, and H1. Every branch that adds, moves, or renames an otherwise unrelated Agent Note rewrites the same generated file, making that artifact a predictable merge hotspot. + +The centralized chronological list adds little discovery value beyond browsing the lifecycle/class tree or searching the repository, while its generator, renderer, command, and freshness check remain maintenance surface. + +## Decision + +The lifecycle/class filesystem tree is the Agent Note inventory. [README.md](../../README.md) remains the curated front door and contract, while ordinary tree navigation and repository search provide discovery. + +`scripts/agent-note-tree.ts` owns the closed lifecycle/class sets and structural walker. `verify-agent-note-classification` validates that tree and rejects the legacy homes and a root `INDEX.md`; it does not render or freshness-check a centralized list. + +This decision supersedes the rejected [generated-index proposal](../../rejected/process/2026-07-04-generate-agent-note-index-tables.md). + +## Alternatives considered + +**Keep the committed generated index and resolve conflicts by regenerating it.** Regeneration makes conflict resolution mechanical but does not prevent unrelated branches from modifying the same artifact or reduce the review noise it creates. + +**Offer an uncommitted on-demand index command.** It avoids committed conflicts but preserves a renderer and command for a discovery path already served by tree navigation and repository search. + +**Restore a hand-maintained index.** It has the same shared-file contention and adds completeness/order mistakes that generation avoided. + +## Consequences + +- Adding, moving, or renaming an Agent Note no longer changes a corpus-wide generated file. +- The classification gate performs less work and the documentation gate topology gains no process or stage. +- Readers give up a single chronological page and use the lifecycle/class tree or repository search instead. diff --git a/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md new file mode 100644 index 0000000000..868955bc10 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-19-remove-generated-agent-note-index.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 无需生成索引即可发现 Agent Note + +Status: implemented + +[English](2026-07-19-remove-generated-agent-note-index.md) | 中文 + +## 问题 + +提交到仓库的 Agent Note 索引,会重复记录每个文件的生命周期/类别路径、文件名日期和 H1 已经编码的事实。任何分支只要添加、移动或重命名彼此无关的 Agent Note,都会重写同一个生成文件,因此该产物会成为可预见的合并冲突热点。 + +与浏览生命周期/类别目录树或搜索仓库相比,这份集中式时间顺序清单提供的发现价值有限;但其生成器、渲染器、命令和新鲜度检查仍然构成维护负担。 + +## 决策 + +生命周期/类别文件系统目录树就是 Agent Note 清单。[README.md](../../README.md) 继续作为人工维护的入口和契约,普通的目录树浏览与仓库搜索负责内容发现。 + +`scripts/agent-note-tree.ts` 持有封闭的生命周期/类别集合与结构遍历器。`verify-agent-note-classification` 校验该目录树,并拒绝旧目录和根目录中的 `INDEX.md`,但不会渲染集中式清单或检查其新鲜度。 + +本决策取代已拒绝的[生成索引提案](../../rejected/process/2026-07-04-generate-agent-note-index-tables.md)。 + +## 备选方案 + +**保留提交到仓库的生成索引,并通过重新生成解决冲突。** 重新生成能让冲突解决过程机械化,但无法阻止无关分支修改同一产物,也不会减少由此产生的评审噪音。 + +**提供不提交到仓库的按需索引命令。** 这可以避免已提交文件的冲突,但仍需维护渲染器和命令,而目录树浏览与仓库搜索已经覆盖该发现路径。 + +**恢复人工维护的索引。** 它具有相同的共享文件争用问题,还会重新引入生成机制已经避免的完整性和排序错误。 + +## 影响 + +- 添加、移动或重命名 Agent Note 时,不再改动覆盖整个语料库的生成文件。 +- 分类门禁执行的工作更少,文档门禁拓扑也不会增加进程或阶段。 +- 读者不再获得单一的时间顺序页面,改用生命周期/类别目录树或仓库搜索。 diff --git a/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.i18n.yaml b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.i18n.yaml new file mode 100644 index 0000000000..ae5ed9b11e --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.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-19-require-agent-notes-for-non-trivial-changes.md: f2645832ebcdd0b81cbff5415c7eb6f60b6fa8cf +2026-07-19-require-agent-notes-for-non-trivial-changes.zh.md: 659aa7cad0823fa0082be1827f8c083037376a4c diff --git a/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.md b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.md new file mode 100644 index 0000000000..f2645832eb --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.md @@ -0,0 +1,31 @@ +# Agent Note: Require an Agent Note for every non-trivial change + +Status: implemented + +English | [中文](2026-07-19-require-agent-notes-for-non-trivial-changes.zh.md) + +## Problem + +A selective threshold based on whether a decision seems durable, contested, and surprising lets substantial changes land without preserving their rationale. Code and tests show what changed, but they cannot consistently preserve why an approach won, which alternatives lost, or what costs maintainers accepted. + +## Decision + +Every non-trivial change adds or updates at least one Agent Note in the same PR. Non-trivial changes include behavior, architecture, cross-file or cross-package contracts, process or tooling, testing strategy, on-disk, wire, or configuration formats, and other decisions a maintainer may reasonably revisit. + +Updating the note that already owns a decision satisfies the rule; a new note is required only when no note owns it. Purely mechanical or local edits with no behavioral, contractual, structural, process, or rationale change are exempt. The [Agent Notes README](../../README.md#when-to-write-one) owns this boundary, while root `AGENTS.md` carries the standing order. + +Review enforces the semantic boundary. No automated gate attempts to classify a diff as trivial or non-trivial, so this policy adds no gate stage or runtime. + +## Alternatives considered + +**Require notes only for decisions judged durable, contested, and surprising.** The threshold is subjective enough that a substantial change can be treated as obvious or local, losing the rationale Agent Notes exist to preserve. + +**Require a new note for every change.** This duplicates an existing note when it already owns the decision and adds empty ceremony to purely mechanical edits. + +**Add a CI diff-classification gate.** A mechanical check cannot reliably determine whether a semantic change is trivial, while another gate adds runtime and invites false positives or superficial compliance. + +## Consequences + +- Every substantial change preserves its rationale and rejected alternatives beside the implementation. +- Contributors maintain an existing owning note instead of creating duplicate records. +- Mechanical edits remain lightweight, and the gate topology and runtime remain unchanged. diff --git a/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.zh.md b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.zh.md new file mode 100644 index 0000000000..659aa7cad0 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-19-require-agent-notes-for-non-trivial-changes.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 每项实质性变更都必须附带 Agent Note + +Status: implemented + +[English](2026-07-19-require-agent-notes-for-non-trivial-changes.md) | 中文 + +## 问题 + +如果只在决策被认为持久、有争议且出人意料时才记录 Agent Note,实质性变更就可能在没有保存决策依据的情况下落地。代码和测试能展示改动内容,却无法稳定保留某种方案胜出的原因、被放弃的备选方案,以及维护者接受的成本。 + +## 决策 + +每项实质性变更都在同一个 PR 中新增或更新至少一份 Agent Note。实质性变更包括行为、架构、跨文件或跨包契约、流程或工具、测试策略、磁盘格式、线协议或配置格式,以及维护者可能合理重审的其他决策。 + +更新已经持有该决策的 Agent Note 即满足规则;仅当没有 Agent Note 持有该决策时才新增记录。完全机械或局部、且不改变行为、契约、结构、流程或决策依据的编辑可豁免。[Agent Notes README](../../README.md#when-to-write-one) 持有这条边界,根目录 `AGENTS.md` 则携带常驻指令。 + +评审负责执行这条语义边界。自动化门禁不尝试把差异分类为平凡或实质性变更,因此这项政策不会增加门禁阶段或运行时间。 + +## 备选方案 + +**只为被判断为持久、有争议且出人意料的决策要求 Agent Note。** 这条门槛过于主观,实质性变更可能被视为显而易见或局部改动,从而丢失 Agent Note 本应保存的决策依据。 + +**每项变更都必须新增 Agent Note。** 当现有 Agent Note 已经持有该决策时,这会产生重复记录,也会让纯机械编辑承担空洞的流程负担。 + +**添加 CI 差异分类门禁。** 机械检查无法可靠判断语义变更是否平凡,额外门禁还会增加运行时间,并引入误报或表面合规。 + +## 影响 + +- 每项实质性变更都会在实现旁保留其决策依据和被放弃的备选方案。 +- 贡献者维护现有的决策持有记录,而不是创建重复记录。 +- 机械编辑仍保持轻量,门禁拓扑和运行时间也保持不变。 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/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.md b/.agents/notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md similarity index 88% rename from docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.md rename to .agents/notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md index 54f8397fc9..f1a9c9aa3c 100644 --- a/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.md +++ b/.agents/notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md @@ -1,4 +1,4 @@ -# RFC: Drop the mutable session summary +# Agent Note: Drop the mutable session summary Status: implemented @@ -20,7 +20,7 @@ Delete the mutable session summary entirely. `SessionSummary` and the `SessionMe Anything the summary was meant to provide is **derivable from the append-only log** when a consumer actually needs it (`firstPrompt` = first `user/message`; recency = the last event's `time` or the file mtime) or already lives in the immutable header (`createdAt`, `cwd`). The one thing *not* derivable — a user-*edited* title — had no implementation and is pure YAGNI; it can return as its own log event or header field if a real feature ever needs it. -This is recorded as a decision because it is **durable** (it narrows a public service contract and an on-disk format across two backends), **contested** (the summary was a deliberate forward-looking design, not an accident), and **surprising** (a future reader finding `SessionHeader` where the original RFC describes `SessionMeta` would otherwise ask why the summary vanished). It also unblocks the [shared persistence write coordinator](../architecture/2026-06-18-shared-persistence-write-coordinator.md): with no mutable summary, the coordinator's hook interface needs no `updateSummary` hook and the JSONL-sidecar-vs-SQLite-column durability divergence disappears, so the two backends' write paths converge. +This is recorded as a decision because it is **durable** (it narrows a public service contract and an on-disk format across two backends), **contested** (the summary was a deliberate forward-looking design, not an accident), and **surprising** (a future reader finding `SessionHeader` where the original Agent Note describes `SessionMeta` would otherwise ask why the summary vanished). It also unblocks the [shared persistence write coordinator](../architecture/2026-06-18-shared-persistence-write-coordinator.md): with no mutable summary, the coordinator's hook interface needs no `updateSummary` hook and the JSONL-sidecar-vs-SQLite-column durability divergence disappears, so the two backends' write paths converge. ## No migration @@ -30,4 +30,4 @@ This is unreleased software (see [root AGENTS.md](../../../../AGENTS.md) § "Pre A future session picker now has to derive its preview/ordering from the log (or reintroduce a typed field) rather than reading a ready-made summary row. That is the correct cost: a cache for a feature that does not exist is dead weight that every backend pays to maintain and every contract test pays to assert. The principle — **a passing test pins current behavior, not necessarily correct behavior; behavior can be an artifact of a past compromise** — is now recorded as a standalone convention in [root AGENTS.md](../../../../AGENTS.md), with this change as its worked example. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md b/.agents/notes/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md similarity index 96% rename from docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md rename to .agents/notes/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md index a93e3d3196..4e8a092989 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md +++ b/.agents/notes/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md @@ -1,4 +1,4 @@ -# RFC: Fold trace-only session facts into load-bearing events +# Agent Note: Fold trace-only session facts into load-bearing events Status: implemented @@ -33,7 +33,7 @@ A consumer can no longer filter the canonical log for standalone `usage` or step ## Implementation note -Shipped as proposed, with one scope refinement (per AGENTS.md "RFCs are proposals, not golden truth"): +Shipped as proposed, with one scope refinement (per AGENTS.md "Agent Notes are proposals, not golden truth"): - **Empty-content `assistant/message` hosts usage with no data loss.** The proof the proposal demanded (no persisted usage chunk becomes unrepresented) lands on the max-tokens path: a step cut off with usage but empty content (e.g. only a dropped tool call) previously emitted a standalone `usage`. It now records an empty-content `assistant/message { content: [], usage }`. To keep that from injecting a spurious content-less assistant turn into the provider transcript, `deriveMessages()` skips empty-content `assistant/message` events. A regression test asserts usage stays represented AND derived history is uncorrupted. diff --git a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md b/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md similarity index 85% rename from docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md rename to .agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md index 9ce20ff7d4..ecea052387 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md +++ b/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md @@ -1,4 +1,4 @@ -# RFC: Drop the unconsumed `llm/adapter-change` event +# Agent Note: Drop the unconsumed `llm/adapter-change` event Status: implemented @@ -6,19 +6,19 @@ Status: implemented `LlmService.registerAdapter()` emits `llm/adapter-change` on registration and disposal ([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts)). Grepping `llm/adapter-change` across `packages/*/src` and `examples/*/src` finds only the declaration, emit sites, docs, and tests; no production listener subscribes to it. -This differs from `tools/change` and `system-prompt/change`. Those two events are also unconsumed today, but they are plausible registry-change signals for future live tool/prompt UIs. LLM adapter registration is more of a boot-time implementation detail: adapters are not a user-visible palette and the real model-call interception seam is `llm/stream`. Keeping an adapter-change event with no listener repeats the [drop-the-dead-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) pattern at a smaller scale. +This differs from `tools/change` and `system-prompt/change`. Those two events are also unconsumed today, but they are plausible registry-change signals for future live tool/prompt UIs. LLM adapter registration is more of a boot-time implementation detail: adapters are not a user-visible palette and the real model-call interception seam is `llm/stream`. Keeping an adapter-change event with no listener repeats the [drop-the-dead-summary](2026-06-19-drop-mutable-session-summary.md) pattern at a smaller scale. The event is not free. `registerAdapter()` yields its rollback disposer before emitting `llm/adapter-change` so a throwing listener unwinds the mutation instead of leaking an adapter entry, and the package carries tests for that listener-throw path. That defensive ordering protects a failure mode only tests can trigger. ## Decision -Only `llm/adapter-change` is removed: the declaration in `dsh-llm`'s `interface Events`, the `ctx.emit('llm/adapter-change')` calls, and the "Emits `llm/adapter-change` on registration and disposal" sentence in `LlmService.registerAdapter`'s JSDoc. `registerAdapter()`'s effect generator keeps the mutation and rollback disposer for HMR/disposal but sheds the listener-throw rollback ordering that existed only for the removed event. The adapter-disposer test asserts the returned disposer removes the adapter without subscribing to the event; the listener-throw rollback test is gone with its subject. The event taxonomy in [docs/architecture.md](../../../architecture.md) and [packages/llm/llm/README.md](../../../../packages/llm/llm/README.md) is updated in the same change. +Only `llm/adapter-change` is removed: the declaration in `dsh-llm`'s `interface Events`, the `ctx.emit('llm/adapter-change')` calls, and the "Emits `llm/adapter-change` on registration and disposal" sentence in `LlmService.registerAdapter`'s JSDoc. `registerAdapter()`'s effect generator keeps the mutation and rollback disposer for HMR/disposal but sheds the listener-throw rollback ordering that existed only for the removed event. The adapter-disposer test asserts the returned disposer removes the adapter without subscribing to the event; the listener-throw rollback test is gone with its subject. The event taxonomy in [docs/architecture.md](../../../../docs/architecture.md) and [packages/llm/llm/README.md](../../../../packages/llm/llm/README.md) is updated in the same change. ## Alternatives considered ### Why not remove every registry change event? -A microkernel where registries announce mutations is a coherent convention. `tools/change` and `system-prompt/change` may become useful when a UI can live-refresh available tools or prompt sections. This RFC leaves that convention intact where it has a plausible user-facing consumer and cuts only the adapter-change event whose current and likely future consumer is unclear. +A microkernel where registries announce mutations is a coherent convention. `tools/change` and `system-prompt/change` may become useful when a UI can live-refresh available tools or prompt sections. This Agent Note leaves that convention intact where it has a plausible user-facing consumer and cuts only the adapter-change event whose current and likely future consumer is unclear. If an LLM adapter browser or dynamic model-picker needs this signal later, reintroduce it with that consumer and a clearer payload than "something changed." diff --git a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md b/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md similarity index 89% rename from docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md rename to .agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md index 3a5dfcda26..b482a444b5 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md +++ b/.agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md @@ -1,4 +1,4 @@ -# RFC: Drop unconsumed assembled LLM convenience surfaces +# Agent Note: Drop unconsumed assembled LLM convenience surfaces Status: implemented @@ -12,7 +12,7 @@ Status: implemented The only production consumer of the LLM service is the agent loop, and it uses `stream()` exclusively — feeding raw chunks through its own `BlockAssembler` so it can log chunks for replay fidelity while assembling in parallel ([packages/core/agent-loop/src/loop.ts](../../../../packages/core/agent-loop/src/loop.ts), the `ctx.llm.stream(req)` step). Grepping `streamBlocks` and `ctx.llm.generate` across `packages/*/src` and `examples/*/src` finds no production callers. The references are the service methods, docs, and tests; adapter tests use `generate()` as a convenient driver, but they can hand-drain `stream()` through the same assembler helper without preserving a public production API. -This is the [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) pattern: assembled-view APIs with tested contracts, consumed by tests rather than production. They were built speculatively for consumers that do not care about token-level deltas, but the one real consumer cares about deltas precisely so it can persist high-fidelity replay data. +This is the [drop-mutable-session-summary](2026-06-19-drop-mutable-session-summary.md) pattern: assembled-view APIs with tested contracts, consumed by tests rather than production. They were built speculatively for consumers that do not care about token-level deltas, but the one real consumer cares about deltas precisely so it can persist high-fidelity replay data. `streamBlocks()` drags a dedicated slice of `BlockAssembler` behind it: `flushReady()` and `flushRemaining()` ([packages/llm/llm/src/assembler.ts:138-168](../../../../packages/llm/llm/src/assembler.ts)) plus the `flushed` cursor field exist only to support incremental in-order yield. `generate()` drags `GenerateResult`, `BlockAssembler.result()`, and the `llm/generate` waterfall as a second interception surface over the same underlying stream. The loop's assembler usage is `push()` / `message()` / `usage` / `finish` — not streaming flush or one-shot service assembly. diff --git a/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md b/.agents/notes/implemented/simplification/2026-06-20-prune-dead-seam-methods.md similarity index 77% rename from docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md rename to .agents/notes/implemented/simplification/2026-06-20-prune-dead-seam-methods.md index ce16ff2ee2..782ffe891e 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md +++ b/.agents/notes/implemented/simplification/2026-06-20-prune-dead-seam-methods.md @@ -1,25 +1,25 @@ -# RFC: Prune dead methods from the persistence seam +# Agent Note: Prune dead methods from the persistence seam Status: implemented -> **Implementation note:** Only `SessionPersistence.has()` and `.delete()` were removed. `BashExecutor.get()` and `.list()` remain because removing their one-line lookup surface required substantially more completion-tracking machinery in consumers. Their id branding is covered by the [branded-ids RFC](../architecture/2026-06-20-branded-ids.md). +> **Implementation note:** Only `SessionPersistence.has()` and `.delete()` were removed. `BashExecutor.get()` and `.list()` remain because removing their one-line lookup surface required substantially more completion-tracking machinery in consumers. Their id branding is covered by the [branded-ids Agent Note](../architecture/2026-06-20-branded-ids.md). ## Problem -A capability seam ([interface / implementation / consumer](../../implemented/architecture/2026-06-13-capability-seams.md)) carries abstract methods that no consumer calls. The seam exists to let implementations and consumers evolve independently — but a method no consumer programs against is not a seam, it is speculative surface every implementation must still implement and test. +A capability seam ([interface / implementation / consumer](../architecture/2026-06-13-capability-seams.md)) carries abstract methods that no consumer calls. The seam exists to let implementations and consumers evolve independently — but a method no consumer programs against is not a seam, it is speculative surface every implementation must still implement and test. ### `SessionPersistence.has()` and `.delete()` The abstract service declared its operations beyond create/append: `load`, `list`, `has`, `delete`. Production consumers of `ctx.sessionPersistence` use only two: the agent-loop resume path calls `load()` ([packages/core/agent-loop/src/index.ts:176](../../../../packages/core/agent-loop/src/index.ts)), and the ACP bridge calls `list()` for `session/list` ([packages/ui/acp/src/index.ts:494](../../../../packages/ui/acp/src/index.ts)). Grepping every `sessionPersistence.*` / `persistence.*` use across `packages/*/src` and `examples/` finds no `has(` and no `delete(` on the service. The `.has(`/`.delete(` calls in `packages/ui/acp/src/index.ts` are on the in-memory `SessionStore` and a local `Set` of loading ids, not persistence. The only callers of `has`/`delete` were the contract suites and per-backend specs. -`has()` was not just unused — it was the most intricate branch in the shared coordinator: a tracked-vs-untracked dual-probe (`loadLive(id, cwd)` for a live-tracked session vs `loadStored(id)` for an untracked one) with a multi-line rationale. `delete()` dragged the `deleteStored` backend hook that every backend had to implement. This is the [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) pattern: a contract test exercised both, but no shipping code asks "is this session persisted?" or removes one. +`has()` was not just unused — it was the most intricate branch in the shared coordinator: a tracked-vs-untracked dual-probe (`loadLive(id, cwd)` for a live-tracked session vs `loadStored(id)` for an untracked one) with a multi-line rationale. `delete()` dragged the `deleteStored` backend hook that every backend had to implement. This is the [drop-mutable-session-summary](2026-06-19-drop-mutable-session-summary.md) pattern: a contract test exercised both, but no shipping code asks "is this session persisted?" or removes one. ## Decision The methods nothing consumes are removed — from the abstract seam, the implementation, and the contract/spec suites that existed only to exercise them: -- `SessionPersistence.has()` / `.delete()` are gone: the abstract declarations, the coordinator's `has`/`delete`/`deleteCore`, and the `PersistenceBackend.deleteStored` hook (jsonl + sqlite each implemented `deleteStored` only to satisfy the hook — those implementations went too). The backends are the [dual-backend](../../implemented/architecture/2026-06-14-session-persistence.md) design and otherwise out of scope; removing a hook they implemented for no consumer is part of removing the hook, not a backend redesign. -- Every doc and source-comment reference is updated to the surviving four-method, `list()`-only contract — not only literal `has(`/`delete(`/`deleteStored` spellings but `{@link has}`/`{@link delete}` JSDoc links and "six public methods" counts — across the seam and backend READMEs, [docs/architecture.md](../../../architecture.md), the [session-persistence](../../implemented/architecture/2026-06-14-session-persistence.md) and [write-coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) RFCs, and the coordinator/backends JSDoc. +- `SessionPersistence.has()` / `.delete()` are gone: the abstract declarations, the coordinator's `has`/`delete`/`deleteCore`, and the `PersistenceBackend.deleteStored` hook (jsonl + sqlite each implemented `deleteStored` only to satisfy the hook — those implementations went too). The backends are the [dual-backend](../architecture/2026-06-14-session-persistence.md) design and otherwise out of scope; removing a hook they implemented for no consumer is part of removing the hook, not a backend redesign. +- Every doc and source-comment reference is updated to the surviving four-method, `list()`-only contract — not only literal `has(`/`delete(`/`deleteStored` spellings but `{@link has}`/`{@link delete}` JSDoc links and "six public methods" counts — across the seam and backend READMEs, [docs/architecture.md](../../../../docs/architecture.md), the [session-persistence](../architecture/2026-06-14-session-persistence.md) and [write-coordinator](../architecture/2026-06-18-shared-persistence-write-coordinator.md) Agent Notes, and the coordinator/backends JSDoc. ## Alternatives considered diff --git a/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md b/.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md similarity index 92% rename from docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md rename to .agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md index 7ed7d10211..c85f644853 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md +++ b/.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md @@ -1,4 +1,4 @@ -# RFC: Keep one public stop primitive +# Agent Note: Keep one public stop primitive Status: implemented @@ -34,4 +34,4 @@ A future plugin cannot abort only the current model/tool step while preserving q ## Related -This RFC only removes the redundant stop verb. Mid-turn steering remains an intentional message path; quiescence observation remains via `whenIdle()`. The resulting public surface is `send()`, `steer()`, `inject()`, `cancel()`, `whenIdle()`, status, options, session, and identity. +This Agent Note only removes the redundant stop verb. Mid-turn steering remains an intentional message path; quiescence observation remains via `whenIdle()`. The resulting public surface is `send()`, `steer()`, `inject()`, `cancel()`, `whenIdle()`, status, options, session, and identity. diff --git a/docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md b/.agents/notes/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md similarity index 82% rename from docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md rename to .agents/notes/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md index f4b49281ea..910eb46e92 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md +++ b/.agents/notes/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md @@ -1,4 +1,4 @@ -# RFC: Stop mirroring durable boundaries as agent events +# Agent Note: Stop mirroring durable boundaries as agent events Status: implemented @@ -6,7 +6,7 @@ Status: implemented removed; `agent/steering` and `agent/stream-chunk` were RETAINED here (they are not durable-boundary mirrors — see "Scope: what is and isn't removed"). The original proposal bundled `agent/steering` into the removal; keeping it - out kept this RFC's scope to boundaries. Each retained event was later + out kept this Agent Note's scope to boundaries. Each retained event was later removed by its own decision — see [Stop mirroring the token stream as an agent event](2026-07-02-remove-stream-chunk-mirror.md) and [Remove the `agent/steering` mirror emit](2026-07-04-remove-agent-steering-mirror.md). --> @@ -23,7 +23,7 @@ Make `session/event` the single live boundary/transcript stream. Consumers that The four durable-boundary mirrors — `agent/turn-start`, `agent/turn-end`, `agent/step-start`, `agent/step-end` — are removed from the agent event taxonomy. A UI that wants the agent handle at a boundary retains the live target object from `agent/created`/`agent/disposed` and compares its session directly; `dsh-ui-stdio` uses this to label the app-owned agent's `[main turn N]` header while other sessions render their durable id. The canonical record remains the event-sourced session log. -The step mirrors (which had no consumer at all) were removed first, in [the event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md); that RFC KEPT the turn mirrors on the stated justification that the stdio UI needed the `Agent` handle at the turn boundary. This RFC finishes the job: `dsh-ui-stdio` is a disposable test REPL whose rendering can change freely, so "ui-stdio needs it" is not a reason to keep a mirror — it reads `session/event` and retains only its live target object. +The step mirrors (which had no consumer at all) were removed first, in [the event-domain-semantics Agent Note](../architecture/2026-06-30-event-domain-semantics.md); that Agent Note KEPT the turn mirrors on the stated justification that the stdio UI needed the `Agent` handle at the turn boundary. This Agent Note finishes the job: `dsh-ui-stdio` is a disposable test REPL whose rendering can change freely, so "ui-stdio needs it" is not a reason to keep a mirror — it reads `session/event` and retains only its live target object. ## Scope: what is and isn't removed @@ -37,8 +37,8 @@ RETAINED — NOT durable-boundary mirrors, so out of scope for this decision: ## Alternatives considered -- **Bundling `agent/steering` into the removal** — the original proposal's shape; narrowed out as scope creep: it mirrors the durable `steering/message` control record, not a boundary, and was removed by [its own later decision](2026-07-04-remove-agent-steering-mirror.md) (as was `agent/stream-chunk`, by [the stream-chunk-mirror RFC](2026-07-02-remove-stream-chunk-mirror.md)). -- **Keeping the turn mirrors for the stdio UI** — [the event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md)'s original stance; rejected here because `dsh-ui-stdio` is a disposable test REPL, not a load-bearing consumer, and it renders boundaries from `session/event` plus its live target object instead. +- **Bundling `agent/steering` into the removal** — the original proposal's shape; narrowed out as scope creep: it mirrors the durable `steering/message` control record, not a boundary, and was removed by [its own later decision](2026-07-04-remove-agent-steering-mirror.md) (as was `agent/stream-chunk`, by [the stream-chunk-mirror Agent Note](2026-07-02-remove-stream-chunk-mirror.md)). +- **Keeping the turn mirrors for the stdio UI** — [the event-domain-semantics Agent Note](../architecture/2026-06-30-event-domain-semantics.md)'s original stance; rejected here because `dsh-ui-stdio` is a disposable test REPL, not a load-bearing consumer, and it renders boundaries from `session/event` plus its live target object instead. ## Consequences diff --git a/docs/rfc/implemented/simplification/2026-06-20-unify-agent-and-session-id.md b/.agents/notes/implemented/simplification/2026-06-20-unify-agent-and-session-id.md similarity index 90% rename from docs/rfc/implemented/simplification/2026-06-20-unify-agent-and-session-id.md rename to .agents/notes/implemented/simplification/2026-06-20-unify-agent-and-session-id.md index 9084befad5..a8a2c375b5 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-unify-agent-and-session-id.md +++ b/.agents/notes/implemented/simplification/2026-06-20-unify-agent-and-session-id.md @@ -1,4 +1,4 @@ -# RFC: Unify the agent id and the session id +# Agent Note: Unify the agent id and the session id Status: implemented @@ -8,7 +8,7 @@ A live agent/session pair needs one identity for registry routing, event sourcin ACP uses the same value for both identities. Stdio and hooks also operate on the session event stream and need the corresponding live agent directly; no production path reattaches one live agent object to several sessions or drives one session through several agent ids. -The [agent-scope runtime](../../implemented/architecture/2026-07-12-agent-scope-runtime-design.md) uses one `AgentCreationTransaction` for create and resume, and agent/session entries share the same final-entry collision rule. A second identity would not represent separate liveness, rollback, or quiescence; it would only add API and translation state around the same transaction. +The [agent-scope runtime](../architecture/2026-07-12-agent-scope-runtime-design.md) uses one `AgentCreationTransaction` for create and resume, and agent/session entries share the same final-entry collision rule. A second identity would not represent separate liveness, rollback, or quiescence; it would only add API and translation state around the same transaction. Session identity likewise has one home in `Session.header.id`; `Session.id` is a derived accessor rather than independent state that needs duplicate validation. diff --git a/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md b/.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md similarity index 86% rename from docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md rename to .agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md index d2b4c6dc4f..3753fb803a 100644 --- a/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md +++ b/.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md @@ -1,10 +1,10 @@ -# RFC: Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin +# Agent Note: Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin Status: implemented ## Problem -The filesystem capability from [filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) currently makes one abstract `FileSystem` service own two different jobs: +The filesystem capability from [filesystem-capability-seam](../architecture/2026-06-17-filesystem-capability-seam.md) currently makes one abstract `FileSystem` service own two different jobs: 1. **Provider operations** — resolving targets, stat/version metadata, text reads/streams, atomic writes, and guarded literal edits. 2. **Agent-facing policy** — line windows, literal edit semantics, and read-before-write/edit observed-state. @@ -13,7 +13,7 @@ That makes every future backend reimplement model-facing read semantics and obse This also creates a real UX dead-end: a windowed read records `view: partial`, and partial views cannot authorize `edit`. A model that reads lines 100-150 of a large file therefore cannot edit line 120 unless it first gets a `full` read, which may be impossible for a file past the read cap. Literal edit only needs freshness: the bytes being matched must still be from the version the model read. -The old RFC already deferred a separate `@deepseek-ai/dsh-fs-policy` package. This RFC builds that layer and keeps `ctx.fs` close to fsspec-style storage primitives (`info`/`cat`/`open`), without turning it into full fsspec. +The old Agent Note already deferred a separate `@deepseek-ai/dsh-fs-policy` package. This Agent Note builds that layer and keeps `ctx.fs` close to fsspec-style storage primitives (`info`/`cat`/`open`), without turning it into full fsspec. ## Decision @@ -28,7 +28,7 @@ provider dsh-fs-local local implementation of ctx.fs `dsh-tool-fs` keeps the same model-facing `read`/`write`/`edit` schemas. It is the executor: it injects `fs` (not a policy service) and reaches `ctx.fs` directly, owns read windowing, and dispatches the `fs/*` events so `dsh-fs-policy` can gate and record. -This RFC decided the four-layer split, the provider contract, and the freshness policy. The tool↔policy COUPLING was then refined by [the event-gate RFC](../architecture/2026-06-26-file-context-as-event-gate.md): `dsh-fs-policy` is a gate PLUGIN that participates through the `fs/*` events rather than a `ctx.fileContext` method service, so the tool is not method-coupled to it and read windowing + the fs I/O live in `dsh-tool-fs`. This document describes that landed event-gate shape; the provider's version guard is optional (omit = unconditional bare provider). +This Agent Note decided the four-layer split, the provider contract, and the freshness policy. The tool↔policy COUPLING was then refined by [the event-gate Agent Note](../architecture/2026-06-26-file-context-as-event-gate.md): `dsh-fs-policy` is a gate PLUGIN that participates through the `fs/*` events rather than a `ctx.fileContext` method service, so the tool is not method-coupled to it and read windowing + the fs I/O live in `dsh-tool-fs`. This document describes that landed event-gate shape; the provider's version guard is optional (omit = unconditional bare provider). ## Provider Contract @@ -63,11 +63,11 @@ type FsWriteIntent = This is a *text-storage* seam, deliberately half a level above byte-level fsspec (`cat`/`open` hand back raw bytes). UTF-8 decoding, binary/NUL rejection, guarded full-file writes, and guarded literal text edits live in the provider so the policy layer never touches raw bytes, reimplements cross-chunk decoding, or separates stale checks from the mutation critical section. Model-facing concepts still stay out of the provider: no line windows, numbered lines, rendered footers, or observed-state store leak down. -Deleted from `dsh-fs`: `readPage`, `FsExpectation`, `FsView`, `FsStateSource`, `FsReadRequest`, `FsTextLine`, line/window constants, `formatReadBody`, and the observed-state `WeakMap`. `applyEdit` is replaced by the narrower provider primitive `editText`, whose contract is version-guarded literal text mutation rather than policy-layer read authorization. The `FS_PARTIAL_OBSERVATION` code also leaves the `FsErrorCode` taxonomy: freshness authorization has no partial/full distinction, so nothing can raise it. `FsTargetKey` and `FsVersion` become branded opaque ids under the existing [branded-ids RFC](../../implemented/architecture/2026-06-20-branded-ids.md). +Deleted from `dsh-fs`: `readPage`, `FsExpectation`, `FsView`, `FsStateSource`, `FsReadRequest`, `FsTextLine`, line/window constants, `formatReadBody`, and the observed-state `WeakMap`. `applyEdit` is replaced by the narrower provider primitive `editText`, whose contract is version-guarded literal text mutation rather than policy-layer read authorization. The `FS_PARTIAL_OBSERVATION` code also leaves the `FsErrorCode` taxonomy: freshness authorization has no partial/full distinction, so nothing can raise it. `FsTargetKey` and `FsVersion` become branded opaque ids under the existing [branded-ids Agent Note](../architecture/2026-06-20-branded-ids.md). ## Policy Contract -`@deepseek-ai/dsh-fs-policy` is a plugin, not a service: it registers no `ctx.*` key and injects nothing. It owns the write/edit freshness policy and observed-state that do not belong on the `FileSystem` provider base class (where a sandboxed/remote backend would otherwise inherit model-facing observation policy it has no business carrying). It contributes that policy through the `fs/*` event gate the executor dispatches. (This RFC originally proposed a concrete `ctx.fileContext` service with `read`/`write`/`edit` methods; [the event-gate RFC](../architecture/2026-06-26-file-context-as-event-gate.md) refined it into the plugin described here so the tool is never method-coupled to the policy.) +`@deepseek-ai/dsh-fs-policy` is a plugin, not a service: it registers no `ctx.*` key and injects nothing. It owns the write/edit freshness policy and observed-state that do not belong on the `FileSystem` provider base class (where a sandboxed/remote backend would otherwise inherit model-facing observation policy it has no business carrying). It contributes that policy through the `fs/*` event gate the executor dispatches. (This Agent Note originally proposed a concrete `ctx.fileContext` service with `read`/`write`/`edit` methods; [the event-gate Agent Note](../architecture/2026-06-26-file-context-as-event-gate.md) refined it into the plugin described here so the tool is never method-coupled to the policy.) Observed state lives here as `WeakMap<owner, Map<targetKey, FsVersion>>`. An entry exists iff the owner has read, written, OR edited that target (every success emits `fs/observed`), so its presence *is* the prior-observation record — there is no separate `hasRead` flag. The owner is derived structurally from the opaque event actor (`{ agent?: { session? } }`), a shape that lives in `dsh-fs-policy`, not `dsh-fs`. @@ -97,7 +97,7 @@ Cross-process writes are best-effort freshness plus atomic replacement: `mtime:s ## Supersedes -This RFC reverses two decisions from [filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) and narrows a third: +This Agent Note reverses two decisions from [filesystem-capability-seam](../architecture/2026-06-17-filesystem-capability-seam.md) and narrows a third: - Read-before-write/edit policy moves out of `ctx.fs` and into the `dsh-fs-policy` plugin (on the `fs/*` event gate). - Text reads no longer return backend-numbered line records or `full`/`partial` views; authorization is based on version freshness, so a windowed read can authorize edit when the file is unchanged. @@ -111,12 +111,12 @@ It keeps the interface/implementation/consumer discipline, consumer-never-import ## Later extension -The seam was later extended with direct directory listing by [Add direct directory listing to the filesystem seam](../architecture/2026-07-03-filesystem-directory-listing-seam.md). That follow-up is tracked separately so this RFC's acceptance criteria continue to describe the fsspec-style refit that originally shipped. +The seam was later extended with direct directory listing by [Add direct directory listing to the filesystem seam](../architecture/2026-07-03-filesystem-directory-listing-seam.md). That follow-up is tracked separately so this Agent Note's acceptance criteria continue to describe the fsspec-style refit that originally shipped. ## Alternatives considered - **Byte-level fsspec (`cat`/`open` handing back raw bytes)** — rejected: the seam is deliberately text-storage, half a level up, so UTF-8 decoding, binary/NUL rejection, and guarded text mutations live once in the provider and the policy layer never touches raw bytes or separates stale checks from the mutation critical section. -- **A concrete `ctx.fileContext` method service** — this RFC's original policy shape; reworked by [the event-gate RFC](../architecture/2026-06-26-file-context-as-event-gate.md) into the gate plugin, so the tool is never method-coupled to the policy. +- **A concrete `ctx.fileContext` method service** — this Agent Note's original policy shape; reworked by [the event-gate Agent Note](../architecture/2026-06-26-file-context-as-event-gate.md) into the gate plugin, so the tool is never method-coupled to the policy. - **Keeping `readPage` and `full`/`partial` view authorization on the provider** — the pre-refit shape the Supersedes section reverses: view completeness is not what edit safety needs, version freshness is, and the view rule made large files past the read cap impossible to edit. ## Consequences diff --git a/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md b/.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md similarity index 88% rename from docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md rename to .agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md index 6ca85b3d16..c79202b95a 100644 --- a/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md +++ b/.agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md @@ -1,4 +1,4 @@ -# RFC: Stop mirroring the token stream as an agent event +# Agent Note: Stop mirroring the token stream as an agent event Status: implemented @@ -17,7 +17,7 @@ ctx.emit('agent/stream-chunk', agent, turn, step, chunk) // ← the mirror The only thing the emit added over the session event was the live `Agent` handle, and the sole consumer discarded it (its handler signature was `(_agent, _turn, _step, chunk)`). -This is the same duplication the [boundary-mirror removal](2026-06-20-remove-agent-boundary-mirror-events.md) eliminated for turn/step boundaries: a consumer had two sources of truth for one durable fact, and every change had to touch both. That RFC deferred the chunk stream ("`assistant/chunk` persistence remains load-bearing, so the chunk stream could later be evaluated as a mirror, but that is a separate decision") rather than bundling it in. This RFC is that separate decision. +This is the same duplication the [boundary-mirror removal](2026-06-20-remove-agent-boundary-mirror-events.md) eliminated for turn/step boundaries: a consumer had two sources of truth for one durable fact, and every change had to touch both. That Agent Note deferred the chunk stream ("`assistant/chunk` persistence remains load-bearing, so the chunk stream could later be evaluated as a mirror, but that is a separate decision") rather than bundling it in. This Agent Note is that separate decision. The premise the deferral hinged on is settled: chunk persistence is authoritative and staying. The proposal to stop persisting chunks and keep only a transient live stream event was [rejected](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md) — high-fidelity replay, partial failed streams, and snapshot replay all depend on the persisted `assistant/chunk` feed. So `assistant/chunk` on `session/event` is the durable, load-bearing token stream, and `agent/stream-chunk` is a pure redundant mirror of it. @@ -32,7 +32,7 @@ Remove `agent/stream-chunk` from the agent event taxonomy. The token stream is r Removed: `agent/stream-chunk`. Not touched: -- `assistant/chunk` (the durable session event) — the authoritative token stream, kept exactly as-is. This RFC removes the LIVE MIRROR, not the persistence (the persistence-removal proposal was separately rejected — see above). +- `assistant/chunk` (the durable session event) — the authoritative token stream, kept exactly as-is. This Agent Note removes the LIVE MIRROR, not the persistence (the persistence-removal proposal was separately rejected — see above). - `agent/steering` — not touched by THIS decision (a control signal, not the token stream). Its durable twin is `steering/message`, and the mirror emit was removed by its own follow-up: [Remove the `agent/steering` mirror emit](2026-07-04-remove-agent-steering-mirror.md). - `agent/status`, `agent/error`, `agent/created`/`agent/disposed`, `agent/queued`, `agent/session-start` — lifecycle/control events that are not transcript data and have no durable duplicate. diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.md b/.agents/notes/implemented/simplification/2026-07-04-drop-image-content-block.md similarity index 87% rename from docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.md rename to .agents/notes/implemented/simplification/2026-07-04-drop-image-content-block.md index 73645e8fda..df63f2a78f 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.md +++ b/.agents/notes/implemented/simplification/2026-07-04-drop-image-content-block.md @@ -1,4 +1,4 @@ -# RFC: Drop the `image` content block until a path can honor it +# Agent Note: Drop the `image` content block until a path can honor it Status: implemented @@ -20,7 +20,7 @@ The recorded fallback, had review landed on keeping the slot: keep `ImageBlock` ## Verification -No harness `ImageBlock` is constructed outside RFC records. ACP's independent inbound-image rejection remains tested, while adapter, codec, and compaction default branches are covered with plugin-defined block types. +No harness `ImageBlock` is constructed outside Agent Note records. ACP's independent inbound-image rejection remains tested, while adapter, codec, and compaction default branches are covered with plugin-defined block types. ## Consequences diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md b/.agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md similarity index 71% rename from docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md rename to .agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md index 95a5a487b5..dadab43f76 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md +++ b/.agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md @@ -1,4 +1,4 @@ -# RFC: Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path +# Agent Note: Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path Status: implemented @@ -13,10 +13,10 @@ Both knobs were adapter-symmetric, so removal shed them from both twins together ## Decision -- `prefill` is removed from `GenerateOptions`, along with both adapters' UNSUPPORTED guards, the tests pinning the throws, the paste line in [core.md](../../../core-data-structures/core.md), and the adapter README rows documenting the rejection. The cookbook's UNSUPPORTED guidance ([adding-an-llm-adapter.md](../../../cookbook/adding-an-llm-adapter.md)) states the rule generically — a `GenerateOptions` field your provider cannot honor throws `LlmError(..., 'UNSUPPORTED')` — instead of using prefill as the example. The [content-block vocabulary RFC](../architecture/2026-06-11-content-block-vocabulary.md)'s consequences record prefill as producer-gated rather than as having a home, per [implemented/AGENTS.md](../AGENTS.md). +- `prefill` is removed from `GenerateOptions`, along with both adapters' UNSUPPORTED guards, the tests pinning the throws, the paste line in [core.md](../../../../docs/core-data-structures/core.md), and the adapter README rows documenting the rejection. The cookbook's UNSUPPORTED guidance ([adding-an-llm-adapter.md](../../../../docs/cookbook/adding-an-llm-adapter.md)) states the rule generically — a `GenerateOptions` field your provider cannot honor throws `LlmError(..., 'UNSUPPORTED')` — instead of using prefill as the example. The [content-block vocabulary Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)'s consequences record prefill as producer-gated rather than as having a home, per [implemented/AGENTS.md](../AGENTS.md). - `strict` is removed from `ToolSchema`, `DefineToolOptions`, `defineTool`, the `schemas()` allowlist, the deepseek serializer branch and its wire-type field, and the tool-catalog renderer's `Strict:` row. The pi-ai payload fixup is simplified to the unconditional scrub of pi-ai's own per-tool strict default (pi-ai stamps `strict: false` on every serialized tool; the hand-rolled twin sends no such field, so the scrub survives for wire parity, pinned by its serializer test). The setter tests and the core.md paste line are gone; both `GenerateOptions` and `ToolSchema` keep their rows in `scripts/type-equiv.manifest.json`, since each type survives minus a field. -This RFC deliberately does NOT touch `temperature`, `stop`, or `maxTokens`: those are honored end-to-end by both adapters and are the natural first targets of a request-mutating hook plugin on `agent/request`. +This Agent Note deliberately does NOT touch `temperature`, `stop`, or `maxTokens`: those are honored end-to-end by both adapters and are the natural first targets of a request-mutating hook plugin on `agent/request`. ## Alternatives considered @@ -26,7 +26,7 @@ This RFC deliberately does NOT touch `temperature`, `stop`, or `maxTokens`: thos ## Verification -`rg prefill` returns only RFC records (this one and the [content-block vocabulary RFC](../architecture/2026-06-11-content-block-vocabulary.md)'s producer-gated consequence); a tool-schema-scoped `rg strict` returns only this RFC, the surviving pi-ai scrub, and unrelated prose such as `strictEqual`. Both adapters' contract tests pass without the guards, and the pi-ai fixup still scrubs the library's strict default — wire parity pinned by its serializer tests. +`rg prefill` returns only Agent Note records (this one and the [content-block vocabulary Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)'s producer-gated consequence); a tool-schema-scoped `rg strict` returns only this Agent Note, the surviving pi-ai scrub, and unrelated prose such as `strictEqual`. Both adapters' contract tests pass without the guards, and the pi-ai fixup still scrubs the library's strict default — wire parity pinned by its serializer tests. ## Consequences diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md b/.agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md similarity index 52% rename from docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md rename to .agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md index 35868be2fd..80faa2ac07 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md +++ b/.agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md @@ -1,4 +1,4 @@ -# RFC: Drop the unconsumed web observation surface — the `providers-change` event and the status methods +# Agent Note: Drop the unconsumed web observation surface — the `providers-change` event and the status methods Status: implemented @@ -7,11 +7,11 @@ Status: implemented `WebService` exposes an observation surface no production code observes: - **`web/providers-change`** (`packages/web/web/src/index.ts`) is declared and emitted on every provider registration and disposal, and each registration effect's rollback yield is ordered BEFORE the emit solely so a throwing change listener unwinds the registration. No listener exists outside the package's own two unit tests (one of which exists to pin that rollback ordering). -- **`searchStatus()` / `fetchStatus()` and the `WebCapabilityStatus` union** (same package) have zero production callers: `dsh-tool-web` executes directly through `ctx.web.search()`/`fetch()` and surfaces unavailability as the structured `WebError` codes the seam throws at execution time (`packages/web/tool-web/src/search.ts`, `packages/web/tool-web/src/fetch.ts`); the only status callers are the web packages' own tests. The prose in `packages/web/tool-web/README.md` and [architecture.md](../../../architecture.md) claims the tool "reads only the aggregated `searchStatus()`/`fetchStatus()`" — drift that survives only because nothing checks prose against call sites. +- **`searchStatus()` / `fetchStatus()` and the `WebCapabilityStatus` union** (same package) have zero production callers: `dsh-tool-web` executes directly through `ctx.web.search()`/`fetch()` and surfaces unavailability as the structured `WebError` codes the seam throws at execution time (`packages/web/tool-web/src/search.ts`, `packages/web/tool-web/src/fetch.ts`); the only status callers are the web packages' own tests. The prose in `packages/web/tool-web/README.md` and [architecture.md](../../../../docs/architecture.md) claims the tool "reads only the aggregated `searchStatus()`/`fetchStatus()`" — drift that survives only because nothing checks prose against call sites. The seam's own design starves both surfaces of consumers: tool registration follows product ENABLEMENT, not provider availability (`packages/web/tool-web/src/index.ts`), and provider selection resolves at execution time, never cached — so there is no cache to invalidate, no registration set to recompute, and no caller that needs an availability probe distinct from executing and routing the structured error. HMR cleanup is carried by the effect disposers themselves. -This mirrors [drop the unconsumed `llm/adapter-change` event](../../implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md), which removed the same notification shape, the same rollback-before-emit machinery, and the same listener-throw test from `LlmService`. That RFC's keep/cut criterion — keep `tools/change` for its plausible user-facing tool-list consumer, cut the boot-time backend-registry signal — puts a web-provider registry squarely on the cut side; the status methods are the same judgment applied to a pull surface instead of a push one. +This mirrors [drop the unconsumed `llm/adapter-change` event](2026-06-20-drop-unconsumed-llm-adapter-change-event.md), which removed the same notification shape, the same rollback-before-emit machinery, and the same listener-throw test from `LlmService`. That Agent Note's keep/cut criterion — keep `tools/change` for its plausible user-facing tool-list consumer, cut the boot-time backend-registry signal — puts a web-provider registry squarely on the cut side; the status methods are the same judgment applied to a pull surface instead of a push one. ## Decision @@ -21,11 +21,11 @@ Remove the registry-change event, aggregated status methods and type, and their ### Why not keep it? -The web seam RFC specified both deliberately — the event as a minimal HMR-visibility signal, the status methods as the tool's aggregated diagnostics — and a future provider-status panel is imaginable. But the same RFC's other choices starved them: derived-on-call selection and enablement-based registration leave no consumer that CAN need either, the shipped tool demonstrates the real pattern (execute and route the structured error), and the drifted README sentence shows the promised consumer never materialized. Per AGENTS.md "RFCs are proposals, not golden truth", these are the parts of that proposal the code has since shown to over-reach; a future observer reintroduces the smallest signal or query it actually consumes, shaped by that consumer. +The web seam Agent Note specified both deliberately — the event as a minimal HMR-visibility signal, the status methods as the tool's aggregated diagnostics — and a future provider-status panel is imaginable. But the same Agent Note's other choices starved them: derived-on-call selection and enablement-based registration leave no consumer that CAN need either, the shipped tool demonstrates the real pattern (execute and route the structured error), and the drifted README sentence shows the promised consumer never materialized. Per AGENTS.md "Agent Notes are proposals, not golden truth", these are the parts of that proposal the code has since shown to over-reach; a future observer reintroduces the smallest signal or query it actually consumes, shaped by that consumer. ## Verification -No `providers-change`, `searchStatus`, `fetchStatus`, or `WebCapabilityStatus` spelling survives outside RFC history; the catalog is fresh (`verify-cordis-catalog` green); registration/disposal HMR-safety tests prove cleanup through execution behavior; and the tool-web README plus the architecture paragraph describe the execution-time error-routing contract the tool actually has. +No `providers-change`, `searchStatus`, `fetchStatus`, or `WebCapabilityStatus` spelling survives outside Agent Note history; the catalog is fresh (`verify-cordis-catalog` green); registration/disposal HMR-safety tests prove cleanup through execution behavior; and the tool-web README plus the architecture paragraph describe the execution-time error-routing contract the tool actually has. ## Consequences diff --git a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md b/.agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md similarity index 98% rename from docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md rename to .agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md index 43f35ee770..bde3efcc75 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md +++ b/.agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md @@ -1,4 +1,4 @@ -# RFC: Fold the stdio UI helper into the stdio app +# Agent Note: Fold the stdio UI helper into the stdio app Status: implemented diff --git a/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md b/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md new file mode 100644 index 0000000000..aa61e859d2 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md @@ -0,0 +1,31 @@ +# Agent Note: Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger) + +Status: implemented + +## Problem + +The merge-extensible vocabulary maps are designed to grow by declaration merging, and the codebase already states the admission policy on `TurnEndReasonMap` (`packages/core/session/src/types.ts`): a variant like `refusal` is "deliberately omitted until" an adapter or loop first emits it. Three declared vocabulary items violated that policy — each had no producer and no consumer, and two had not even a test: + +- **`CacheHint` and its `cache?: CacheHint` block fields** on `TextBlock`/`ToolResultBlock` (`packages/llm/llm/src/types.ts`; the image block carried a third such field, which left with it — see [the drop-image Agent Note](2026-07-04-drop-image-content-block.md)). Nothing constructed a block with `cache:` anywhere — src, tests, and doc pastes all came up empty — and neither adapter read `.cache`: DeepSeek prompt caching is automatic, so the adapters map `prompt_cache_hit_tokens` OUT of responses without ever sending a hint IN. This was Anthropic-style `cache_control` surface with no provider that could honor it. +- **`MessageSourceMap.agent`** (`{ kind: 'agent'; agentId: string }`, same file). Zero constructors, tests included. Its intended producer shipped without it: the subagent backends send the parent's prompt to the child with no `source`, so it logs as `{ kind: 'user' }`, and the generic envelope renderer interpolates `source.kind` without ever routing on it. +- **`TurnTriggerMap.continuation`** (`packages/core/session/src/types.ts`). The loop structurally cannot emit it — continuation happens *within* a turn as further steps, never as a new turn — and it constructs only `message` and `injection` triggers. The only writer was one hand-built test fixture needing an arbitrary non-message trigger (`packages/support/llm-replay/tests/llm-replay.spec.ts`), which an `injection` trigger serves equally; the only production trigger reader, the ACP bridge, filters on `kind === 'message'`. + +## Decision + +`CacheHint`, its `cache?` block fields, the `agent` message-source variant, and the `continuation` turn-trigger variant are deleted: the shipped vocabulary carries none of them. The llm-replay fixture uses an `injection` trigger (any non-`message` trigger serves its purpose). The type-equiv pastes in [core.md](../../../../docs/core-data-structures/core.md) and [session.md](../../../../docs/core-data-structures/session.md) match the pruned maps — both symbols keep their rows in `scripts/type-equiv.manifest.json`, since each map survives minus a member — and the [content-block vocabulary Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)'s consequences record cache hints as producer-gated rather than as having a home, per [implemented/AGENTS.md](../AGENTS.md). + +Each variant returns the day it gains a real producer, exactly as the maps are designed to grow: a caching feature re-adds `cache` together with the adapter that transmits it; subagent attribution re-adds `agent` together with the backend that stamps it and a consumer that routes on it; an auto-continue feature that genuinely starts new turns re-adds `continuation` with the plugin that emits it. + +## Alternatives considered + +### Why not keep them? + +The [content-block vocabulary Agent Note](../architecture/2026-06-11-content-block-vocabulary.md) listed "cache hints … have a home" as a design consequence, and reserved slots do advertise intent. But an empty slot is contract surface every implementation and consumer must consider (must my adapter honor `cache`? must my renderer route `agent` sources?), and the sibling map's own JSDoc already rejects reservation-without-emitter — `refusal` and `max_turn_requests` are named as variants to add *when something first emits them*, not declared in advance. Holding already-declared dead variants to the same standard makes the vocabulary mean something: if it is in the map, something produces it. + +## Verification + +`rg` for `CacheHint`, the `agent` message-source spelling, and the `continuation` trigger spelling returns only Agent Note records (this one, and [the drop-image Agent Note](2026-07-04-drop-image-content-block.md)'s account of the image block's own `cache` field); the llm-replay fixture asserts the same replay behavior with an `injection` trigger; the core-data-structures pastes and the type-equiv manifest are in sync. + +## Consequences + +Nothing operational changed — nothing could construct these values. The mirror-event removals ([the boundary-mirror Agent Note](2026-06-20-remove-agent-boundary-mirror-events.md), [the stream-chunk Agent Note](2026-07-02-remove-stream-chunk-mirror.md)) touch only transient `agent/*` events, never the durable vocabulary, so there is no collision. Elsewhere the admission policy already holds: `rejected`, `prompt/blocked`, and `hook/invoked`/`hook/result` each have live producers — this Agent Note extends the same bar to the three variants that lacked one. The image block's own `cache?` field belongs to [the drop-image Agent Note](2026-07-04-drop-image-content-block.md), which removed it together with the block; this Agent Note covers the two fields on the block types that remain. diff --git a/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md b/.agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md similarity index 94% rename from docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md rename to .agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md index 0de8f5732c..97652ef50c 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md +++ b/.agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md @@ -1,4 +1,4 @@ -# RFC: Prune write-only fields and a dead routing knob from the fs seam +# Agent Note: Prune write-only fields and a dead routing knob from the fs seam Status: implemented @@ -13,7 +13,7 @@ The [fs seam split](2026-06-26-fsspec-style-fs-seam.md) moved read routing and p ## Decision -Delete the fs-local constant, its re-export, and the `streamMinSize` knob (the remaining `FsIoInternals` knobs are genuinely used by the atomic-write tests); drop `inputPath` from `FsTarget`; shrink `FsEditOutcome` to `{ version, before, after }` and pass `replaceAll` to `formatEditOutput` from the parsed args; drop `limit`/`version` from `FileReadOutcome`. The [filesystem.md](../../../core-data-structures/filesystem.md) pastes, `packages/fs/fs/README.md`, and the test fakes that had to fabricate the removed fields shrink with the types. +Delete the fs-local constant, its re-export, and the `streamMinSize` knob (the remaining `FsIoInternals` knobs are genuinely used by the atomic-write tests); drop `inputPath` from `FsTarget`; shrink `FsEditOutcome` to `{ version, before, after }` and pass `replaceAll` to `formatEditOutput` from the parsed args; drop `limit`/`version` from `FileReadOutcome`. The [filesystem.md](../../../../docs/core-data-structures/filesystem.md) pastes, `packages/fs/fs/README.md`, and the test fakes that had to fabricate the removed fields shrink with the types. ## Alternatives considered diff --git a/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md b/.agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md similarity index 62% rename from docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md rename to .agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md index 6591aab8f3..ebfc774792 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md +++ b/.agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md @@ -1,4 +1,4 @@ -# RFC: Remove the `agent/steering` mirror emit +# Agent Note: Remove the `agent/steering` mirror emit Status: implemented @@ -12,19 +12,19 @@ Steering carries real production traffic — the hook bridges' turn-continuation ## Decision -`agent/steering` is removed from the agent event taxonomy: the declaration in `packages/core/agent/src/types.ts` (and its mention in the live-events JSDoc list there), the emit in `drainSteering` (whose then-unused `ctx` parameter went with it), the row in `packages/core/agent/README.md`, and the emit line in the loop-pseudocode blocks (the `packages/core/agent-loop/src/loop.ts` module doc and [architecture.md](../../../architecture.md)); the cordis catalog is regenerated without it. The one regression test pins source preservation on the durable `steering/message` event — the fact it pins lives on the log. +`agent/steering` is removed from the agent event taxonomy: the declaration in `packages/core/agent/src/types.ts` (and its mention in the live-events JSDoc list there), the emit in `drainSteering` (whose then-unused `ctx` parameter went with it), the row in `packages/core/agent/README.md`, and the emit line in the loop-pseudocode blocks (the `packages/core/agent-loop/src/loop.ts` module doc and [architecture.md](../../../../docs/architecture.md)); the cordis catalog is regenerated without it. The one regression test pins source preservation on the durable `steering/message` event — the fact it pins lives on the log. -Three implemented RFCs stated the retention, and each is amended per [implemented/AGENTS.md](../AGENTS.md) to point here as the record of the removal: the [boundary RFC](2026-06-20-remove-agent-boundary-mirror-events.md)'s retained-list entry, the [stream-chunk RFC](2026-07-02-remove-stream-chunk-mirror.md)'s scope clause, and the [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md)'s transient-emit enumeration. +Three implemented Agent Notes stated the retention, and each is amended per [implemented/AGENTS.md](../AGENTS.md) to point here as the record of the removal: the [boundary Agent Note](2026-06-20-remove-agent-boundary-mirror-events.md)'s retained-list entry, the [stream-chunk Agent Note](2026-07-02-remove-stream-chunk-mirror.md)'s scope clause, and the [event-domain-semantics Agent Note](../architecture/2026-06-30-event-domain-semantics.md)'s transient-emit enumeration. ## Alternatives considered ### Why not keep it? -"It is a control signal, not a boundary" — but the taxonomy's operative distinction is mirrored-vs-live-only, not control-vs-boundary, and this event mirrored. A consumer that wants enqueue-time notification has `agent/queued` (with its steering flag); a consumer that wants drain-time notification is by definition asking for the moment `steering/message` is appended, which `session/event` delivers with the same payload plus durability. The rejected [retire-mid-turn-steering RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md) defended the steering *capability* — `steer()`, the durable event, continuation forcing — all of which this removal keeps untouched. +"It is a control signal, not a boundary" — but the taxonomy's operative distinction is mirrored-vs-live-only, not control-vs-boundary, and this event mirrored. A consumer that wants enqueue-time notification has `agent/queued` (with its steering flag); a consumer that wants drain-time notification is by definition asking for the moment `steering/message` is appended, which `session/event` delivers with the same payload plus durability. The rejected [retire-mid-turn-steering Agent Note](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md) defended the steering *capability* — `steer()`, the durable event, continuation forcing — all of which this removal keeps untouched. ## Verification -The `agent/steering` spelling survives only in RFC prose (this RFC, the three amended RFCs above, and the frozen [rejected steering-capability RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md), whose text records the proposal it declined); the catalog is regenerated; the retargeted test pins source preservation on `steering/message`. +The `agent/steering` spelling survives only in Agent Note prose (this Agent Note, the three amended Agent Notes above, and the frozen [rejected steering-capability Agent Note](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md), whose text records the proposal it declined); the catalog is regenerated; the retargeted test pins source preservation on `steering/message`. ## Consequences diff --git a/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md b/.agents/notes/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md similarity index 76% rename from docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md rename to .agents/notes/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md index 79abfab2a0..f054f168a2 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md +++ b/.agents/notes/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md @@ -1,4 +1,4 @@ -# RFC: Share the app bins' boot glue instead of maintaining twin copies +# Agent Note: Share the app bins' boot glue instead of maintaining twin copies Status: implemented @@ -10,13 +10,13 @@ The stdio and ACP bins duplicated environment loading, fail-loud handling, entry The helpers live once, in [`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot) (`packages/ui/app-boot`, in the `ui` group because the bins are published artifacts whose runtime dependency must itself be published, not `support/`): `resolveConfigPath` (snapshot-aware, the single path resolver for both bins), `loadEnv`, `installFailLoud`, `assertEntriesLoaded`, and `boot`, each parameterized by the bin's diagnostic prefix and injectable at its side-effect seams (the warn sink, the process slice) so the unit suite covers every branch — including `boot()` driven in-process against the real Loader with relative-specifier configs, both the settled-tree happy path and the fiber-less-entry rejection. The package carries the per-file 100% coverage gate; the loader-failure lore has one home. -Each `bin.ts` is a thin self-executing composition over the shared helpers plus its app-specific lifecycle (the ACP bin: replay-mode env skipping and the stdin-EOF dispose; the stdio bin: nothing extra). The bins stay coverage-excluded and export nothing; the published-artifact guards are unchanged — the built-bin smokes still run each bin under plain node in a node_modules-shaped temp dir (now symlinking `ui/app-boot` too) and still assert the missing-config non-zero exit, per the "real entry path means the published artifact" defensive pattern. The [extract-example-app-packages RFC](../architecture/2026-06-20-extract-example-app-packages.md)'s bin-ownership facts are amended accordingly. +Each `bin.ts` is a thin self-executing composition over the shared helpers plus its app-specific lifecycle (the ACP bin: replay-mode env skipping and the stdin-EOF dispose; the stdio bin: nothing extra). The bins stay coverage-excluded and export nothing; the published-artifact guards are unchanged — the built-bin smokes still run each bin under plain node in a node_modules-shaped temp dir (now symlinking `ui/app-boot` too) and still assert the missing-config non-zero exit, per the "real entry path means the published artifact" defensive pattern. The [extract-example-app-packages Agent Note](../architecture/2026-06-20-extract-example-app-packages.md)'s bin-ownership facts are amended accordingly. ## Alternatives considered ### Why not keep the duplication? -The bins were framed as independently-owned published artifacts, and a new package carries fixed overhead (manifest, README, tsconfig reference, publint surface) comparable to the deduplicated line count. But app-vs-app sharing was never weighed by the RFC that created the bins — it consolidated three example `start.ts` copies INTO the bins and stopped there; the drift was observed fact; and the coverage-gap argument is independent of the dedup argument: this was the only nontrivial runtime logic in the repo exempt from the per-file 100% gate. The recorded fallback (extracting only the pure logic into per-app modules) would have ended the exemption but kept two homes for the lore. +The bins were framed as independently-owned published artifacts, and a new package carries fixed overhead (manifest, README, tsconfig reference, publint surface) comparable to the deduplicated line count. But app-vs-app sharing was never weighed by the Agent Note that created the bins — it consolidated three example `start.ts` copies INTO the bins and stopped there; the drift was observed fact; and the coverage-gap argument is independent of the dedup argument: this was the only nontrivial runtime logic in the repo exempt from the per-file 100% gate. The recorded fallback (extracting only the pure logic into per-app modules) would have ended the exemption but kept two homes for the lore. ## Consequences diff --git a/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md b/.agents/notes/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md similarity index 85% rename from docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md rename to .agents/notes/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md index 7cbf67043f..82830b1366 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md +++ b/.agents/notes/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md @@ -1,12 +1,12 @@ -# RFC: Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics +# Agent Note: Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics Status: implemented ## Problem -Four pieces of the `dsh-hook-protocol`/bridge contract missed the discipline the [subagent-observe-enrich RFC](../feature/2026-06-30-subagent-observe-enrich.md) records — it dropped an `agentType` lifecycle field for lacking a consumer, and these failed the same test: +Four pieces of the `dsh-hook-protocol`/bridge contract missed the discipline the [subagent-observe-enrich Agent Note](../feature/2026-06-30-subagent-observe-enrich.md) records — it dropped an `agentType` lifecycle field for lacking a consumer, and these failed the same test: -1. **`HookDialect`'s `'native'` variant** (`packages/hooks/hook-protocol/src/types.ts`) had zero producers — the bridges stamp `'claude'` and `'codex'`; the only `'native'` constructor anywhere was the lib's own unit test. The field's own JSDoc defines `dialect` as "the bridge that ran it", and native is not a bridge: the [interception-seams RFC](../feature/2026-06-30-interception-seams.md) records that native hooks are not a package and that "a native plugin can already use the typed Decisions" without the durable hook log, and the flagship native-plugin worked example asserts exactly that (no `hook/*` events at all). +1. **`HookDialect`'s `'native'` variant** (`packages/hooks/hook-protocol/src/types.ts`) had zero producers — the bridges stamp `'claude'` and `'codex'`; the only `'native'` constructor anywhere was the lib's own unit test. The field's own JSDoc defines `dialect` as "the bridge that ran it", and native is not a bridge: the [interception-seams Agent Note](../feature/2026-06-30-interception-seams.md) records that native hooks are not a package and that "a native plugin can already use the typed Decisions" without the durable hook log, and the flagship native-plugin worked example asserts exactly that (no `hook/*` events at all). 2. **`HookOutput.suppressOutput`** (same file) was parsed by the codec and discarded on every path: no bridge branch, no merge fold, no warn, no deferred-list row — uniquely among its parsed-but-unhonored siblings, each of which carries a stated deferral (`updatedInput` → a logged warn plus the [pre-tool-input-rewrite proposal](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md); `systemMessage` → a logged warn plus a README deferred row; `continue`/`stopReason` → a `TODO(hook-continue-false)` anchor plus the `'stop'` decision record). Structurally there is nothing to suppress: hook stdout never enters any transcript (context flows only via `additionalContext`; the log records only `decision`/`stderrSummary`), so a hook author setting `suppressOutput: true` got silent nothing with no warn. 3. **`defaultTimeoutMs` was double-defaulted in both bridge configs with a floating literal** — a schema `.default(600_000)` AND a `?? 600_000` fallback (`packages/hooks/hooks-claude/src/index.ts`, `packages/hooks/hooks-codex/src/index.ts`), two homes per bridge for one protocol-level constant, so the bridges could silently drift apart on the shared default. *The proposal's original remedy — delete the knob outright — was overtaken by the no-hardcoded-tunables audit, which kept the knob as the explicit bridge-owned config (and added `stderrSummaryMaxChars` beside it); what remained to fix was the literal's home.* 4. **The `hook/result` semantics lived in the bridges, twice, not in the lib that owns the event.** `summarize()` — the stderr truncation rule — was byte-identical in `packages/hooks/hooks-claude/src/index.ts` and `packages/hooks/hooks-codex/src/index.ts`, and so was the decision-string rule `output.decision ?? (output.continue === false ? 'stop' : 'pass')`; yet `dsh-hook-protocol` declared `hook/result`, documented `stderrSummary` as "truncated" without owning the truncation, and documented the decision values without owning the mapping. If one bridge drifted (a different cap, a different fallback), the shared durable event's semantics would fork silently. diff --git a/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md b/.agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md similarity index 96% rename from docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md rename to .agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md index 39edd8fee9..7d1065d250 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md +++ b/.agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md @@ -1,4 +1,4 @@ -# RFC: Trim unreachable ACP bridge surface — the branding knobs and the kind-sniffing fallback +# Agent Note: Trim unreachable ACP bridge surface — the branding knobs and the kind-sniffing fallback Status: implemented diff --git a/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md b/.agents/notes/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md similarity index 91% rename from docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md rename to .agents/notes/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md index 0907a63417..a719e20f1e 100644 --- a/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md +++ b/.agents/notes/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md @@ -1,4 +1,4 @@ -# RFC: Drop unconsumed skill provider events +# Agent Note: Drop unconsumed skill provider events Status: implemented @@ -14,7 +14,7 @@ Skill discovery reads the current provider map on demand, provider registration The skill registry declares and emits no provider-membership events. Provider registration and disposal remain direct effect-owned state changes that synchronously invalidate completed catalogs; lookup and discovery read the current provider map on demand. Tests observe cleanup through provider lookup and collected output rather than lifecycle notifications. -The generated event catalog, API catalog, and producer/consumer matrix omit the deleted notifications. The skill-system RFC and package documentation describe registration through its direct effect-owned state and cache-invalidation contract. +The generated event catalog, API catalog, and producer/consumer matrix omit the deleted notifications. The skill-system Agent Note and package documentation describe registration through its direct effect-owned state and cache-invalidation contract. ## Alternatives considered diff --git a/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md b/.agents/notes/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md similarity index 98% rename from docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md rename to .agents/notes/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md index 8ece4214b7..68ced83876 100644 --- a/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md +++ b/.agents/notes/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md @@ -1,4 +1,4 @@ -# RFC: Prune unused web seam fields +# Agent Note: Prune unused web seam fields Status: implemented diff --git a/docs/rfc/implemented/simplification/2026-07-12-simplify-session-log-representation.md b/.agents/notes/implemented/simplification/2026-07-12-simplify-session-log-representation.md similarity index 98% rename from docs/rfc/implemented/simplification/2026-07-12-simplify-session-log-representation.md rename to .agents/notes/implemented/simplification/2026-07-12-simplify-session-log-representation.md index 7bf44c6c06..97a89e5be7 100644 --- a/docs/rfc/implemented/simplification/2026-07-12-simplify-session-log-representation.md +++ b/.agents/notes/implemented/simplification/2026-07-12-simplify-session-log-representation.md @@ -1,4 +1,4 @@ -# RFC: Simplify session-log representation +# Agent Note: Simplify session-log representation Status: implemented 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/docs/rfc/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml similarity index 62% rename from docs/rfc/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml rename to .agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml index b86d9405e0..2b0b5c067d 100644 --- a/docs/rfc/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-retire-subagent-mock-package.md: 47174d22eaf27c5a5509280793dd50d70c542d00 -2026-07-19-retire-subagent-mock-package.zh.md: bfad78e1912f9d5153196df3fea2494c88436a91 +2026-07-19-retire-subagent-mock-package.md: 4a7fa32fdb0d8e656d61c39491a49bbd85e0adf3 +2026-07-19-retire-subagent-mock-package.zh.md: 7de72abb18050fb737000a2013e514dde3dae521 diff --git a/docs/rfc/implemented/simplification/2026-07-19-retire-subagent-mock-package.md b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.md similarity index 98% rename from docs/rfc/implemented/simplification/2026-07-19-retire-subagent-mock-package.md rename to .agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.md index 47174d22ea..4a7fa32fdb 100644 --- a/docs/rfc/implemented/simplification/2026-07-19-retire-subagent-mock-package.md +++ b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.md @@ -1,4 +1,4 @@ -# RFC: Retire the standalone subagent mock package +# Agent Note: Retire the standalone subagent mock package Status: implemented diff --git a/docs/rfc/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md similarity index 98% rename from docs/rfc/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md rename to .agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md index bfad78e191..7de72abb18 100644 --- a/docs/rfc/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md +++ b/.agents/notes/implemented/simplification/2026-07-19-retire-subagent-mock-package.zh.md @@ -1,4 +1,4 @@ -# RFC: 撤销独立的 subagent mock 包 +# Agent Note: 撤销独立的 subagent mock 包 Status: implemented diff --git a/docs/rfc/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml similarity index 61% rename from docs/rfc/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml rename to .agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml index 55a113d22b..cd03e02285 100644 --- a/docs/rfc/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-use-one-session-surface-manager.md: 0c0fd70717ff4b49e8817e88592d0781d9ccbd09 -2026-07-19-use-one-session-surface-manager.zh.md: 6d11c9bb475b0ee5a568f96678d0251f1160dda3 +2026-07-19-use-one-session-surface-manager.md: dee1a2a1cb6642730c87035de071d77ad38bd238 +2026-07-19-use-one-session-surface-manager.zh.md: ce538f1569c91e317af347d2ac20db624215eac8 diff --git a/docs/rfc/implemented/simplification/2026-07-19-use-one-session-surface-manager.md b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.md similarity index 98% rename from docs/rfc/implemented/simplification/2026-07-19-use-one-session-surface-manager.md rename to .agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.md index 0c0fd70717..dee1a2a1cb 100644 --- a/docs/rfc/implemented/simplification/2026-07-19-use-one-session-surface-manager.md +++ b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.md @@ -1,4 +1,4 @@ -# RFC: Use one surface manager per session +# Agent Note: Use one surface manager per session Status: implemented diff --git a/docs/rfc/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md similarity index 97% rename from docs/rfc/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md rename to .agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md index 6d11c9bb47..ce538f1569 100644 --- a/docs/rfc/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md +++ b/.agents/notes/implemented/simplification/2026-07-19-use-one-session-surface-manager.zh.md @@ -1,4 +1,4 @@ -# RFC: 每个会话只使用一个表层管理器 +# Agent Note: 每个会话只使用一个表层管理器 Status: implemented 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 `<steering source="…">…</steering>` and `context/message` as `<context source="…">…</context>` (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.** `<steering>` and `<context>` 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 `<steering>` 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 `<system-reminder>` frame and opting out of the `<context>` 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 `<context>` 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 `<system-reminder>…</system-reminder>` 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 `<system-reminder>`) and surface-level wrapping (`<context>`/`<steering>` 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` 包成 `<steering source="…">…</steering>`,`context/message` 包成 `<context source="…">…</context>`(后者有一个 `'raw'` 退出选项可跳过封套)。这些封套意在告诉模型「这是注入内容,不是用户在说话」。 + +两个问题: + +- **没有模型在这些标签上训练过。** `<steering>` 和 `<context>` 是任何模型都未被教会去读的任意标记,因此这层框架只是徒增 token 而没有可靠效果,还可能起反作用——已录制的 transcript 显示,模型会把 `<steering>` 指令当成第三方元数据而拒绝服从,只回答原始提示。 +- **session 表层是承载框架的错误层次。** 表层的职责是把持久日志投影为模型 transcript;决定内容如何措辞并不是它的事。想要特定框架的调用方可以在注入前自行格式化内容——唯一的重度生产方(`workspace-context`)本就这样做,它自带完整的 `<system-reminder>` 框架,并用 `envelope: 'raw'` 退出 `<context>` 封套。剩下的标签机制(`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。 + +## 权衡的替代方案 + +- **保留 `<context>` 封套,只对 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` 已经自行为内容加框架:它把一个完整的 `<system-reminder>…</system-reminder>` 块作为消息内容发出,而不依赖表层封套。这种调用方自有的模式才是应保留的——表层逐字透传内容,任何框架都住在生产方自己的内容里。 + +曾经存在两条框架路径——调用方自行加框架(`workspace-context` 的 `<system-reminder>`),以及表层封套(`deriveEventMessage` 加上的 `<context>`/`<steering>`)。本次变更移除了后者,只留下调用方自有的框架。如果未来又需要带标签的框架,应由事件的 `meta` map(生产方附加、对模型隐藏的元数据字段)来统一它,交给专门的渲染器或适配器消费,而不是在 `deriveEventMessage` 中重新硬编码标签。生产方在 `meta` 中声明所需的框架,由一个渲染器统一施加;session 表层的投影始终保持逐字透传。 diff --git a/docs/rfc/implemented/testing/2026-06-11-property-based-testing.md b/.agents/notes/implemented/testing/2026-06-11-property-based-testing.md similarity index 95% rename from docs/rfc/implemented/testing/2026-06-11-property-based-testing.md rename to .agents/notes/implemented/testing/2026-06-11-property-based-testing.md index db7a9c8a79..06350753cd 100644 --- a/docs/rfc/implemented/testing/2026-06-11-property-based-testing.md +++ b/.agents/notes/implemented/testing/2026-06-11-property-based-testing.md @@ -1,4 +1,4 @@ -# RFC: Property-based testing for protocol-shaped code +# Agent Note: Property-based testing for protocol-shaped code Status: implemented @@ -24,4 +24,4 @@ Adopt `fast-check` (a root devDependency) with one `tests/properties.spec.ts` pe - A property flake from a timeout is a finding, not something to retry away. The loop properties are deterministic by construction (settle on `agent/status`), so a hang is a real defect. - Property tests supplement, not replace, the example tests that pin specific branches for the 100%-coverage gate. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md b/.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md similarity index 86% rename from docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md rename to .agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md index 2f7c0dbd8e..5fc7479eaf 100644 --- a/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md +++ b/.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md @@ -1,14 +1,14 @@ -# RFC: ACP snapshot tests — record-once / replay-deterministic +# Agent Note: ACP snapshot tests — record-once / replay-deterministic Status: implemented ## Problem -Unit tests do not exercise the complete ACP subprocess transcript, while real-API tests are nondeterministic and key-gated. Editor-facing `session/update` output can therefore regress despite green unit coverage, as the [default-export postmortem](../../../postmortem/0001-acp-default-export-drops-inject.md) demonstrated. +Unit tests do not exercise the complete ACP subprocess transcript, while real-API tests are nondeterministic and key-gated. Editor-facing `session/update` output can therefore regress despite green unit coverage, as the [default-export postmortem](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md) demonstrated. The blocker for a full-transcript test is the model: the agent's output is driven by a non-deterministic LLM, and a key-gated test that hits the real API on every run is neither deterministic nor CI-runnable. We want the fidelity of a real run with the determinism of a fixture. -This RFC records the decision to add a third test tier — **snapshot tests** — and the design choices that make it deterministic, keyless-in-CI, and cheap to maintain. +This Agent Note records the decision to add a third test tier — **snapshot tests** — and the design choices that make it deterministic, keyless-in-CI, and cheap to maintain. ## Decision @@ -42,14 +42,14 @@ Replay is positional and therefore permits only one in-flight model stream per s Recording runs the scenario with the real `llm-deepseek` adapter and the JSONL persistence backend, then copies the produced `.jsonl` into the scenario dir. Per-event appends are durable, but the harness shuts the subprocess down gracefully (close stdin → `await ctx.dispose()`) before harvesting so the final events are flushed. `llm-replay` itself does no recording — it is replay-only. -Replay uses a `cordis.snapshot.yml` overlay that replaces the real adapter with `llm-replay` while retaining the live composition. Recording uses the ordinary config and a harness-supplied persistence root. Replay mode skips `.env` loading, so a stray API key cannot trigger a live call. See the [single-source config RFC](2026-07-04-single-source-acp-replay-config.md). +Replay uses a `cordis.snapshot.yml` overlay that replaces the real adapter with `llm-replay` while retaining the live composition. Recording uses the ordinary config and a harness-supplied persistence root. Replay mode skips `.env` loading, so a stray API key cannot trigger a live call. See the [single-source config Agent Note](2026-07-04-single-source-acp-replay-config.md). ### Two surfaces: normalize, then compare A snapshot run asserts **two** normalized surfaces, because the harness's external surfaces are distinct: 1. The **stdout transcript** — the framed `session/update` JSON-RPC the editor sees. Catches regressions in the ACP bridge's event→update translation (`streamSessionEventUpdate`). Compared against a committed `stdout.expected.jsonl`. -2. The **re-persisted session JSONL**, normalized and compared with `session.jsonl`. The same fixture is both replay source and expected log. Prompt text is scrubbed; one scenario per header class pins readable prompt and tool content as described in the [header-pinning RFC](2026-07-06-pin-request-header-content-in-one-scenario.md). Override scenarios derive model behavior solely from their sidecar. +2. The **re-persisted session JSONL**, normalized and compared with `session.jsonl`. The same fixture is both replay source and expected log. Prompt text is scrubbed; one scenario per header class pins readable prompt and tool content as described in the [header-pinning Agent Note](2026-07-06-pin-request-header-content-in-one-scenario.md). Override scenarios derive model behavior solely from their sidecar. The surfaces are complementary: stdout covers bridge projection, while JSONL covers loop, tool, and boundary structure that the projection omits. @@ -77,4 +77,4 @@ Tool determinism comes from a temporary cwd, scrubbed environment, fresh non-log The new tier adds reviewed per-scenario input, session, stdout, optional override, and optional workspace fixtures. Workspace seeds are copied into the temporary cwd for both record and replay. In return the tier provides deterministic keyless transcript coverage through the real Loader and tool composition. The subprocess, input, workspace, normalization, and replay harness can support examples beyond ACP. -This RFC relates to but does not supersede the [proposed determinism RFC](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md): that proposal's "universal replay fixture" re-derives session *message history* after every test (an internal-consistency invariant), whereas snapshot tests pin the *external protocol output*. They are complementary — one guards the event-sourcing invariant, the other guards the editor-facing contract. +This Agent Note relates to but does not supersede the [proposed determinism Agent Note](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md): that proposal's "universal replay fixture" re-derives session *message history* after every test (an internal-consistency invariant), whereas snapshot tests pin the *external protocol output*. They are complementary — one guards the event-sourcing invariant, the other guards the editor-facing contract. diff --git a/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md b/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md similarity index 90% rename from docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md rename to .agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md index ea2eb6964a..36160de354 100644 --- a/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md +++ b/.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md @@ -1,14 +1,14 @@ -# RFC: Real-API e2e in CI against the external DeepSeek API +# Agent Note: Real-API e2e in CI against the external DeepSeek API Status: implemented ## Problem -The harness leans hard on real-API tests by policy: [docs/testing.md](../../../testing.md) argues that a no-key suite proves the plumbing but not the product, and the [ACP inject postmortem](../../../postmortem/0001-acp-default-export-drops-inject.md) is the standing proof — 178 keyless tests stayed green while a real editor session crashed instantly. The real-API e2e suite (`pnpm run test:e2e`, the `*.e2e.ts` files) exists precisely to close that gap: it drives the agent against the live DeepSeek API — real model calls, real bash tools, multi-turn, resume, ACP-over-stdio. +The harness leans hard on real-API tests by policy: [docs/testing.md](../../../../docs/testing.md) argues that a no-key suite proves the plumbing but not the product, and the [ACP inject postmortem](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md) is the standing proof — 178 keyless tests stayed green while a real editor session crashed instantly. The real-API e2e suite (`pnpm run test:e2e`, the `*.e2e.ts` files) exists precisely to close that gap: it drives the agent against the live DeepSeek API — real model calls, real bash tools, multi-turn, resume, ACP-over-stdio. The default gate ([.github/workflows/ci.yml](../../../../.github/workflows/ci.yml)) is deliberately keyless: it carries no secret and runs for forks. `test:e2e` self-skips without a key (`describe.skipIf(!process.env.DEEPSEEK_API_KEY)`), so adding it there would report green without exercising the real suite. A separate secret-bearing workflow is required to make real-API coverage a merge signal. -This RFC records the decision to add a **second, secret-consuming workflow** that runs the real-API suite in CI, and — because introducing the first CI secret into a repo that may later go public is a security/isolation decision — the threat model it relies on and what changes when the repo becomes public. +This Agent Note records the decision to add a **second, secret-consuming workflow** that runs the real-API suite in CI, and — because introducing the first CI secret into a repo that may later go public is a security/isolation decision — the threat model it relies on and what changes when the repo becomes public. ## Decision @@ -20,7 +20,7 @@ ci.yml's value is that it is keyless, forkable, and always-green: any contributo ### Cost is not the constraint; reliability is -Internal inference cost is not the limiting constraint, so the workflow optimizes for coverage and signal. It runs every matching `*.e2e.ts` file on multiple triggers and every trusted PR, implementing the [docs/testing.md](../../../testing.md) with-key policy. +Internal inference cost is not the limiting constraint, so the workflow optimizes for coverage and signal. It runs every matching `*.e2e.ts` file on multiple triggers and every trusted PR, implementing the [docs/testing.md](../../../../docs/testing.md) with-key policy. ### Triggers: trusted events only @@ -93,6 +93,6 @@ None of these require changing the workflow to go public; they are operational s A second CI workflow and the first repo secret to maintain. The real-API suite now gates merges (pre-merge on trusted PRs, post-merge on the main branch) and runs nightly, so a real break in the agent's interaction with the external API surfaces in CI rather than only in a developer's local run — at the cost of real (but internally free) API calls on every trusted PR and merge. The preflight makes secret misconfiguration self-announcing instead of silently disabling the net. -The design carries a documented constraint surface: the `pull_request` trigger's key-exposure tradeoff (drop it to harden), the `if:` gate's dependence on the author-based Dependabot test, and the hard prohibition on `pull_request_target`. The going-public checklist above is the operational companion — this RFC is the place a future maintainer should re-read before changing the trigger set or flipping repo visibility, rather than re-deriving the fork/secret model from scratch. +The design carries a documented constraint surface: the `pull_request` trigger's key-exposure tradeoff (drop it to harden), the `if:` gate's dependence on the author-based Dependabot test, and the hard prohibition on `pull_request_target`. The going-public checklist above is the operational companion — this Agent Note is the place a future maintainer should re-read before changing the trigger set or flipping repo visibility, rather than re-deriving the fork/secret model from scratch. The scheduled trigger auto-disables after 60 days of repo inactivity (a GitHub behavior); push/PR/dispatch are backstops, and an active monorepo will not hit it. Runner egress to `https://api.deepseek.com` is assumed — GitHub-hosted `ubuntu-latest` has it; an egress-restricted self-hosted runner would need connectivity confirmed before relying on the nightly. diff --git a/docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md b/.agents/notes/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md similarity index 94% rename from docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md rename to .agents/notes/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md index ef0d3bd4ab..b17ecad098 100644 --- a/docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md +++ b/.agents/notes/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md @@ -1,4 +1,4 @@ -# RFC: Use `session.jsonl` as the only snapshot session-log artifact +# Agent Note: Use `session.jsonl` as the only snapshot session-log artifact Status: implemented @@ -24,7 +24,7 @@ Stdout expected outputs remain unchanged; they are the editor-facing projection ## Verification -`session.expected.jsonl` appears nowhere in the snapshot harness, fixtures, orphan guards, or docs; the snapshot test derives the expected session log from `session.jsonl` for every model scenario; authored sidecar scenarios commit their expected produced log as `session.jsonl` with `replay.override.json` as the model-behavior override; and the orphan-fixture guards know which files each scenario kind requires. The [ACP snapshot tests RFC](../../implemented/testing/2026-06-19-acp-snapshot-tests.md) describes the reduced fixture set. +`session.expected.jsonl` appears nowhere in the snapshot harness, fixtures, orphan guards, or docs; the snapshot test derives the expected session log from `session.jsonl` for every model scenario; authored sidecar scenarios commit their expected produced log as `session.jsonl` with `replay.override.json` as the model-behavior override; and the orphan-fixture guards know which files each scenario kind requires. The [ACP snapshot tests Agent Note](2026-06-19-acp-snapshot-tests.md) describes the reduced fixture set. ## Consequences diff --git a/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md b/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md similarity index 90% rename from docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md rename to .agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md index 14db415b1b..93280ed62e 100644 --- a/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md +++ b/.agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md @@ -1,10 +1,10 @@ -# RFC: Persist the seed boundary so fork-child replay routes correctly +# Agent Note: Persist the seed boundary so fork-child replay routes correctly Status: implemented ## Problem -The [per-session snapshot replay RFC](2026-06-22-subagent-snapshot-replay.md) made the snapshot tier express a nested-agent shape: a parent plus one recorded log per in-process subagent, each replayed as its own script keyed by calling session. It noted (§ Scope, final bullet) that a fork snapshot was "a trivial future addition, not a gap in the keying." That was wrong about a fork child specifically — not the keying, but the *script derivation*. +The [per-session snapshot replay Agent Note](2026-06-22-subagent-snapshot-replay.md) made the snapshot tier express a nested-agent shape: a parent plus one recorded log per in-process subagent, each replayed as its own script keyed by calling session. It noted (§ Scope, final bullet) that a fork snapshot was "a trivial future addition, not a gap in the keying." That was wrong about a fork child specifically — not the keying, but the *script derivation*. A subagent script is derived from a recorded session log by [`deriveReplayScript`](../../../../packages/support/llm-replay): it groups the log's `assistant/chunk` events by `(turn, step)` into one replay entry per `stream()` call. This is correct for a **spawn** child, whose log contains only its own model calls. diff --git a/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.md b/.agents/notes/implemented/testing/2026-06-22-fork-snapshot-scenarios.md similarity index 69% rename from docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.md rename to .agents/notes/implemented/testing/2026-06-22-fork-snapshot-scenarios.md index 34bebd1194..272e62ba77 100644 --- a/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.md +++ b/.agents/notes/implemented/testing/2026-06-22-fork-snapshot-scenarios.md @@ -1,10 +1,10 @@ -# RFC: Record fork and mixed spawn+fork snapshot scenarios +# Agent Note: Record fork and mixed spawn+fork snapshot scenarios Status: implemented ## Problem -The [seed-boundary RFC](2026-06-22-fork-child-replay-seed-boundary.md) made fork-child replay route correctly: `dsh-llm-replay` derives a child's script from the events at or after its persisted `seedLength` boundary, so a fork child's inherited parent prefix is not replayed as the child's own model calls. But it shipped with **no recorded fork scenario** — the slice was exercised only by `llm-replay`'s unit tests (a synthetic child fixture) and a persistence round-trip test. The full-transcript snapshot tier, the one net that boots the real `acp-agent` and replays an end-to-end nested transcript, had only spawn children (`subagent-spawn`, `subagent-multi`). A fork-routing regression that left the unit tests green would still have escaped the tier built to catch transcript regressions. +The [seed-boundary Agent Note](2026-06-22-fork-child-replay-seed-boundary.md) made fork-child replay route correctly: `dsh-llm-replay` derives a child's script from the events at or after its persisted `seedLength` boundary, so a fork child's inherited parent prefix is not replayed as the child's own model calls. But it shipped with **no recorded fork scenario** — the slice was exercised only by `llm-replay`'s unit tests (a synthetic child fixture) and a persistence round-trip test. The full-transcript snapshot tier, the one net that boots the real `acp-agent` and replays an end-to-end nested transcript, had only spawn children (`subagent-spawn`, `subagent-multi`). A fork-routing regression that left the unit tests green would still have escaped the tier built to catch transcript regressions. The snapshot infrastructure to express a fork scenario was already in place — both in-process backends are wired into `cordis.yml` / `cordis.snapshot.yml` as two model-facing tools (`subagent` → spawn, `subagent_fork` → fork), the harness harvests every child log, and replay forwards per-child fixtures keyed by `seedLength`. What was missing was a *recorded scenario* that drives a fork child through it. @@ -13,7 +13,7 @@ The snapshot infrastructure to express a fork scenario was already in place — Record two scenarios against the real API, both replayed keyless in the default gate: - **`subagent-fork`** — the parent completes a turn that establishes a fact, then delegates one subtask via `subagent_fork`. The fork child inherits the conversation (its log carries a non-zero `seedLength`), so it can answer from the parent's context. This is the focused regression: the child fixture's `seedLength` is the boundary the replay slice depends on, recorded from a real fork rather than hand-synthesized. -- **`subagent-mixed`** — the parent completes a turn, then delegates once via `subagent` (a fresh spawn child, `seedLength` 0) and once via `subagent_fork` (a fork child, non-zero `seedLength`) in one transcript. This is the mixed spawn+fork scenario the seed-boundary and per-session-replay RFCs both named as a future addition: one transcript exercises both transports and both branches of the slice (`seedLength` 0 = no-op, `seedLength > 0` = trim the inherited prefix), with the two children ordered spawn-then-fork by `createdAt`. +- **`subagent-mixed`** — the parent completes a turn, then delegates once via `subagent` (a fresh spawn child, `seedLength` 0) and once via `subagent_fork` (a fork child, non-zero `seedLength`) in one transcript. This is the mixed spawn+fork scenario the seed-boundary and per-session-replay Agent Notes both named as a future addition: one transcript exercises both transports and both branches of the slice (`seedLength` 0 = no-op, `seedLength > 0` = trim the inherited prefix), with the two children ordered spawn-then-fork by `createdAt`. ### Why a completed turn-1 is required @@ -26,4 +26,4 @@ The fork backend seeds the child with the parent's **balanced completed-turn pre - Out-of-process (ACP) subagent replay remains a different shape (each child is its own process with its own replay) and is still tracked as `TODO(acp-subagent-replay)` — these scenarios are in-process only. - Re-recording (`pnpm run test:snapshot:record`) regenerates all four fork/spawn fixtures from the live API; the two new scenarios self-skip without a key like every recorded scenario. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md b/.agents/notes/implemented/testing/2026-06-22-subagent-snapshot-replay.md similarity index 94% rename from docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md rename to .agents/notes/implemented/testing/2026-06-22-subagent-snapshot-replay.md index 811930af00..d21a081f9b 100644 --- a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.md +++ b/.agents/notes/implemented/testing/2026-06-22-subagent-snapshot-replay.md @@ -1,4 +1,4 @@ -# RFC: Per-session snapshot replay for nested agents +# Agent Note: Per-session snapshot replay for nested agents Status: implemented @@ -11,7 +11,7 @@ It was built for ONE session per process, and that assumption is wired into two - **`dsh-llm-replay` keyed nothing.** It served the Nth `llm/stream` call the Nth recorded entry from a single global cursor. With a parent agent AND an in-process subagent both streaming on one context, the calls interleave and the single cursor hands the child the parent's script (and vice versa). - **The harness harvested one log.** `findSessionLog` walked the sessions root and returned the FIRST `.jsonl` it found. A subagent runs as a second `Session` with its own log in the same cwd bucket, so the child's transcript was silently dropped. -This was the `TODO(subagent-snapshots)` deferral recorded in the [subagent seam RFC](../../implemented/feature/2026-06-21-subagent-capability-seam.md): the in-process backends (PR2) shipped with unit + e2e coverage, but the full-transcript snapshot tier could not express a nested-agent shape until this infrastructure landed. This RFC is that stacked follow-up. +This was the `TODO(subagent-snapshots)` deferral recorded in the [subagent seam Agent Note](../feature/2026-06-21-subagent-capability-seam.md): the in-process backends (PR2) shipped with unit + e2e coverage, but the full-transcript snapshot tier could not express a nested-agent shape until this infrastructure landed. This Agent Note is that stacked follow-up. ## Decision diff --git a/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.md b/.agents/notes/implemented/testing/2026-07-04-hook-snapshot-matrix.md similarity index 97% rename from docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.md rename to .agents/notes/implemented/testing/2026-07-04-hook-snapshot-matrix.md index 0ba04c4abb..7c2c33460c 100644 --- a/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.md +++ b/.agents/notes/implemented/testing/2026-07-04-hook-snapshot-matrix.md @@ -1,4 +1,4 @@ -# RFC: Hook snapshot matrix — end-to-end expected outputs for both bridges +# Agent Note: Hook snapshot matrix — end-to-end expected outputs for both bridges Status: implemented @@ -47,4 +47,4 @@ The matrix therefore covers every hook point that has a DETERMINISTIC, OBSERVABL - The prove-red discipline holds: tampering a hook config's output (e.g. changing a deny reason) turns its scenario red on replay — the hook process runs FOR REAL during replay (only the model is replayed), so the expected output guards the actual hook→seam→loop path, not a mock of it. - The `acp-agent` demo now loads a Codex bridge it will usually no-op (no `codex-hooks.json` in a typical project), which is the intended fail-soft behavior, not a cost. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md b/.agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md similarity index 91% rename from docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md rename to .agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md index 03c25572d4..3de645deeb 100644 --- a/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md +++ b/.agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md @@ -1,10 +1,10 @@ -# RFC: Single-source the acp-agent replay config +# Agent Note: Single-source the acp-agent replay config Status: implemented ## Problem -`examples/acp-agent` shipped two hand-maintained configs: `cordis.yml` (the live tree) and a `cordis.snapshot.yml` that mirrored it entry-for-entry with only the llm backend swapped — stripped of comments, the entire difference was the eight-line `llm-deepseek` stanza versus the two-line `llm-replay` stanza. Every app-shape change had to be made twice, and nothing gated the symmetry: if the copies drifted, the snapshot tier would silently exercise a different app than the one that ships — the ["green units, broken product" class of gap](../../../postmortem/0001-acp-default-export-drops-inject.md) the snapshot tier exists to close, reintroduced one level up, with reviewer vigilance as the only defense. +`examples/acp-agent` shipped two hand-maintained configs: `cordis.yml` (the live tree) and a `cordis.snapshot.yml` that mirrored it entry-for-entry with only the llm backend swapped — stripped of comments, the entire difference was the eight-line `llm-deepseek` stanza versus the two-line `llm-replay` stanza. Every app-shape change had to be made twice, and nothing gated the symmetry: if the copies drifted, the snapshot tier would silently exercise a different app than the one that ships — the ["green units, broken product" class of gap](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md) the snapshot tier exists to close, reintroduced one level up, with reviewer vigilance as the only defense. ## Decision diff --git a/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md b/.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md similarity index 94% rename from docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md rename to .agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md index 68f7fce66a..1dcfc2e085 100644 --- a/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md +++ b/.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md @@ -1,4 +1,4 @@ -# RFC: Pin request-header content in one snapshot scenario +# Agent Note: Pin request-header content in one snapshot scenario Status: implemented @@ -22,7 +22,7 @@ One pin covers the whole suite because every session — parent, spawn child, fo - **Scrub at compare time only, keeping fixtures raw** — lets compares pass while committed fixtures retain stale duplicate content and rewrite wholesale on the next recording. Stored tokens state honestly what each JSONL does not pin. - **Scrub everywhere, pin nowhere** — loses the only end-to-end record of the composed header as actually sent (prompt assembly, registered-tool order, full schemas). The generated tool catalog documents each tool in isolation; only a real fixture pins the composed set. - **Keep the one full pin entirely in JSONL** — removes suite-wide duplication but leaves prompt and schema changes as one escaped line. Markdown and structured JSON give each surface its natural review format without weakening the reconstructed-header assertion. -- **Slim the session log itself (log a content digest, store the header elsewhere)** — violates the reconstructability contract: the product log must reproduce each request bit-for-bit ([reconstructable-requests RFC](../architecture/2026-07-05-reconstructable-requests.md)). Header bulk is a test-artifact concern, solved in test normalization; the live log is untouched. +- **Slim the session log itself (log a content digest, store the header elsewhere)** — violates the reconstructability contract: the product log must reproduce each request bit-for-bit ([reconstructable-requests Agent Note](../architecture/2026-07-05-reconstructable-requests.md)). Header bulk is a test-artifact concern, solved in test normalization; the live log is untouched. ## Verification diff --git a/docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.md b/.agents/notes/implemented/testing/2026-07-08-shared-acp-snapshot-package.md similarity index 81% rename from docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.md rename to .agents/notes/implemented/testing/2026-07-08-shared-acp-snapshot-package.md index ecdd40f313..aadeaeea30 100644 --- a/docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.md +++ b/.agents/notes/implemented/testing/2026-07-08-shared-acp-snapshot-package.md @@ -1,10 +1,10 @@ -# RFC: Extract the ACP snapshot suite into a support package +# Agent Note: Extract the ACP snapshot suite into a support package Status: implemented ## Problem -The ACP snapshot tier ([snapshot RFC](2026-06-19-acp-snapshot-tests.md)) was built from three modules living inside one example's test directory: `snapshot-harness.ts` (boot the real bin subprocess, drive it over ACP JSON-RPC, harvest the persisted logs), `snapshot-normalize.ts` (the pure expected-output normalizers), and the ~150-line scenario body plus fixture guards in `acp.snapshot.ts` (record/replay modes, the stdout expected-output and log comparisons, the pinned-header uniformity guard, the orphan/required-file/single-pin meta-tests). +The ACP snapshot tier ([snapshot Agent Note](2026-06-19-acp-snapshot-tests.md)) was built from three modules living inside one example's test directory: `snapshot-harness.ts` (boot the real bin subprocess, drive it over ACP JSON-RPC, harvest the persisted logs), `snapshot-normalize.ts` (the pure expected-output normalizers), and the ~150-line scenario body plus fixture guards in `acp.snapshot.ts` (record/replay modes, the stdout expected-output and log comparisons, the pinned-header uniformity guard, the orphan/required-file/single-pin meta-tests). A second ACP example wanting snapshot coverage — the sandbox/approval composition is the immediate consumer — could only copy those modules, forking exactly the logic that must not drift: record write-back, header scrubbing, child-session harvest ordering. The spawn/client glue was also triplicated across `acp.e2e.ts`, `hooks.e2e.ts`, and the harness. Location decided test rigor: the per-file 100% coverage gate measures `packages/*/*/src` only, so none of this machinery was measured — the same gap that had moved `dsh-llm-replay` out of `examples/` into [packages/support](../../../../packages/support/README.md). And the harness's ACP client hardcoded `requestPermission → cancelled`, so an approval round-trip — the headline behavior of the sandbox composition — could not be expressed at the snapshot tier at all. @@ -18,11 +18,11 @@ The machinery lives in [`packages/support/acp-snapshot`](../../../../packages/su **`src/normalize.ts`** — the pure normalizers, hook-free by policy: when a future event carries a new volatile field (an approval duration, say), the shared normalizer learns it in the same change, keeping one home for what "normalized" means rather than per-suite scrub extensions. -**`src/suite.ts`** — the `Scenario` type and `defineAcpSnapshotSuite(options)`, registering the per-scenario compares, record/refresh fixture write-back, the header pin with its live uniformity guard, and the fixture guard block (no orphan scenario dirs, required files present, exactly one pin per class, every JSONL a `scrubSystemPrompts` fixed point, non-pinning fixtures also `scrubRequestHeaders` fixed points). A scenario directory's `session.jsonl` plus contiguous `session.<n>.jsonl` siblings are its ordered primary/child inventory, so the scenario table declares policy without duplicating a child count. The pinned-header contract ([pinned-header RFC](2026-07-06-pin-request-header-content-in-one-scenario.md)) is per-suite: each header class flags exactly one `pinsHeader` scenario, whose `system-prompt.expected.md` and JSONL tool list split the composed header into reviewable artifacts; the uniformity guard compares both against every live header in that class. A pinning scenario declares any legitimate changed-header count, and its Markdown artifact records every full changed prompt. The pure helpers (`sessionFixtureNames`, `fixtureContext`, `normalizedHeaders`, `normalizedSystemPrompts`, `formatSystemPromptSnapshot`, `headerChangeCount`) are exported from the module for direct unit coverage. +**`src/suite.ts`** — the `Scenario` type and `defineAcpSnapshotSuite(options)`, registering the per-scenario compares, record/refresh fixture write-back, the header pin with its live uniformity guard, and the fixture guard block (no orphan scenario dirs, required files present, exactly one pin per class, every JSONL a `scrubSystemPrompts` fixed point, non-pinning fixtures also `scrubRequestHeaders` fixed points). A scenario directory's `session.jsonl` plus contiguous `session.<n>.jsonl` siblings are its ordered primary/child inventory, so the scenario table declares policy without duplicating a child count. The pinned-header contract ([pinned-header Agent Note](2026-07-06-pin-request-header-content-in-one-scenario.md)) is per-suite: each header class flags exactly one `pinsHeader` scenario, whose `system-prompt.expected.md` and JSONL tool list split the composed header into reviewable artifacts; the uniformity guard compares both against every live header in that class. A pinning scenario declares any legitimate changed-header count, and its Markdown artifact records every full changed prompt. The pure helpers (`sessionFixtureNames`, `fixtureContext`, `normalizedHeaders`, `normalizedSystemPrompts`, `formatSystemPromptSnapshot`, `headerChangeCount`) are exported from the module for direct unit coverage. ## Alternatives considered -- **Copy the modules into each example** — the fork this RFC exists to prevent: the record/guard logic is exactly the code that must stay byte-identical across suites, and examples are outside the coverage gate, so each copy is also unmeasured. +- **Copy the modules into each example** — the fork this Agent Note exists to prevent: the record/guard logic is exactly the code that must stay byte-identical across suites, and examples are outside the coverage gate, so each copy is also unmeasured. - **A shared module directory under `examples/`** — keeps the code outside the coverage gate and forces relative imports across example boundaries, against the package-name import convention; `examples/` leaves stay thin by design. - **A `/testing` subpath export of `dsh-acp-demo`** — couples test infrastructure into a product package's surface and dependency set; `packages/support/` exists precisely for real-but-lower-compatibility dev/test packages, with `dsh-llm-replay` as the precedent this package completes. - **Export raw test-body functions instead of a suite factory** — each example would re-own the `describe`/`it` skeleton (~80 lines of registration boilerplate per suite) for no flexibility gain; the factory keeps consumers to a scenario table plus one call, and the exported pure helpers preserve unit-testability inside the factory design. diff --git a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml similarity index 62% rename from docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml rename to .agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml index 6f3df04716..c208a1e553 100644 --- a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.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-18-tui-terminal-state-snapshots.md: a609e7c167ddf7ebe594f6793e34a48c555c10a9 -2026-07-18-tui-terminal-state-snapshots.zh.md: 8690a6f83a827619682b82c2df56d360e104c53b +2026-07-18-tui-terminal-state-snapshots.md: 192e872ab63cf4ff8a121ea0a2ee9345379cfa26 +2026-07-18-tui-terminal-state-snapshots.zh.md: 9766a8087632daa1be0dcfb191696dbad354ff68 diff --git a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md similarity index 99% rename from docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md rename to .agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md index a609e7c167..192e872ab6 100644 --- a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md +++ b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md @@ -1,4 +1,4 @@ -# RFC: Snapshot semantic terminal state for the TUI +# Agent Note: Snapshot semantic terminal state for the TUI Status: implemented diff --git a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md similarity index 99% rename from docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md rename to .agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md index 8690a6f83a..9766a80876 100644 --- a/docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md +++ b/.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.zh.md @@ -1,4 +1,4 @@ -# RFC: TUI 语义终端状态快照 +# Agent Note: TUI 语义终端状态快照 Status: implemented diff --git a/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.md b/.agents/notes/proposed/architecture/2026-06-16-typed-event-schemas.md similarity index 91% rename from docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.md rename to .agents/notes/proposed/architecture/2026-06-16-typed-event-schemas.md index 9eb4c585a4..91679a0a6c 100644 --- a/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.md +++ b/.agents/notes/proposed/architecture/2026-06-16-typed-event-schemas.md @@ -1,10 +1,10 @@ -# RFC: Runtime schemas for the event vocabulary (Zod vs the merge-extensible-map pattern) +# Agent Note: Runtime schemas for the event vocabulary (Zod vs the merge-extensible-map pattern) Status: proposed ## Problem -The harness models its core vocabulary — content blocks, message sources, finish reasons, turn triggers, turn-end reasons, and session events — as **merge-extensible maps**: a TypeScript `interface` (e.g. `SessionEventMap`, `ContentBlockMap`) that plugins augment via declaration merging, with the public union derived as `Map[keyof Map]`. This is the repo's universal extension pattern, documented in [docs/architecture.md](../../../architecture.md) ("The same merge-extensible-map pattern is used for `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`") and relied on by the `defineTool` `InferArgs` DSL and the `assertNever` exhaustiveness convention. +The harness models its core vocabulary — content blocks, message sources, finish reasons, turn triggers, turn-end reasons, and session events — as **merge-extensible maps**: a TypeScript `interface` (e.g. `SessionEventMap`, `ContentBlockMap`) that plugins augment via declaration merging, with the public union derived as `Map[keyof Map]`. This is the repo's universal extension pattern, documented in [docs/architecture.md](../../../../docs/architecture.md) ("The same merge-extensible-map pattern is used for `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`") and relied on by the `defineTool` `InferArgs` DSL and the `assertNever` exhaustiveness convention. The pattern is **compile-time only**. The types vanish at runtime: there is no schema object to validate an incoming value against, parse untrusted input with, or enumerate at runtime. The [session-persistence contract](../../implemented/architecture/2026-06-14-session-persistence.md) exposes two consequences: @@ -13,7 +13,7 @@ The pattern is **compile-time only**. The types vanish at runtime: there is no s This raises whether the event vocabulary should move to **Zod** or another runtime-schema library so durable and plugin boundaries have runtime schemas rather than erased types. -This RFC scopes that question without proposing an implementation. +This Agent Note scopes that question without proposing an implementation. ## Why this is not a persistence change @@ -30,7 +30,7 @@ A migration of the event/vocabulary surface to runtime schemas touches, at minim - **The event producers** — 16 `session.append(...)` call sites in the loop — unchanged in shape but now validated at the boundary. - **~7 switch-consumers** that branch on these unions: `deriveMessages` (`dsh-session`), `BlockAssembler` (`dsh-llm`), the `dsh-invariants` plugin, both LLM adapters (`dsh-llm-deepseek`, `dsh-llm-pi-ai`), and the tool schema layer (`dsh-tools`). The `assertNever`-on-closed-unions vs fall-through-on-extensible-unions convention (a documented lint rule) would need rethinking — runtime variants are not statically exhaustive. - **The `defineTool` `InferArgs` DSL** (`dsh-tools`), which derives zero-cast `execute` arg types from a compile-time schema spec — the showcase of the current approach. -- **Docs**: architecture.md (the pattern is described as foundational), [dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md), and any RFC that references the pattern. +- **Docs**: architecture.md (the pattern is described as foundational), [dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md), and any Agent Note that references the pattern. This is a repository-wide vocabulary redesign, not a persistence implementation detail. @@ -56,11 +56,11 @@ Replace the merge-extensible maps with a runtime registry the producers contribu ## Proposal -Defer. If runtime validation is wanted at the durable boundary, **Option B** (schemastery on closed header and metadata shapes) is the proportionate step within the existing convention. **Option C** is an architecture decision that requires its own implementation RFC, including a choice between Zod and schemastery. +Defer. If runtime validation is wanted at the durable boundary, **Option B** (schemastery on closed header and metadata shapes) is the proportionate step within the existing convention. **Option C** is an architecture decision that requires its own implementation Agent Note, including a choice between Zod and schemastery. ## Acceptance criteria -- Option C proceeds only through its own implementation RFC, never as a persistence side effect. +- Option C proceeds only through its own implementation Agent Note, never as a persistence side effect. - If Option B is taken up, the closed header/metadata shapes (the JSONL `isHeaderLine` guard and kin) validate through schemastery in place of hand-rolled guards, with the merge-extensible maps untouched. ## Risks diff --git a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml similarity index 61% rename from docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml rename to .agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml index 6e301ee06a..83a2d4d788 100644 --- a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-15-sdk-project-editing-architecture.md: 985cc22c159c68801b78262aa96c7422bdfa1318 -2026-07-15-sdk-project-editing-architecture.zh.md: 6a194e8e5f193e62bfc283fd93a5fde0367fe196 +2026-07-15-sdk-project-editing-architecture.md: 8335af516dbaa85f4adb85286f976ce9be2c9da8 +2026-07-15-sdk-project-editing-architecture.zh.md: bec39cc896887678b2d3f74832a9d13d7b354d6e diff --git a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md similarity index 99% rename from docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md rename to .agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md index 985cc22c15..8335af516d 100644 --- a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md +++ b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md @@ -1,4 +1,4 @@ -# RFC: SDK project editing architecture +# Agent Note: SDK project editing architecture Status: proposed @@ -16,7 +16,7 @@ Structured files are modified through document objects, while one-shot text arti ## Terminology -| Term | Usage in this RFC | Meaning | +| Term | Usage in this Agent Note | Meaning | |---|---|---| | Feature | feature | A product unit curated and managed by the SDK; one feature may contain several feature options and contribute several Cordis config entries, npm dependencies, environment placeholders, and owned files | | Feature option | feature option | A finite selectable implementation or configuration shape within one feature; feature rules may make options fixed, exclusive, or additive | diff --git a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md similarity index 99% rename from docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md rename to .agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md index 6a194e8e5f..bec39cc896 100644 --- a/docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md +++ b/.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.zh.md @@ -1,4 +1,4 @@ -# RFC: SDK 工程编辑架构 +# Agent Note: SDK 工程编辑架构 Status: proposed diff --git a/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md b/.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md similarity index 88% rename from docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md rename to .agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md index 9657512082..e6c5d9eb1e 100644 --- a/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md +++ b/.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md @@ -1,10 +1,10 @@ -# RFC: Pre-tool input rewrite — a consistent design +# Agent Note: Pre-tool input rewrite — a consistent design Status: proposed ## Problem -The [interception-seams RFC](../../implemented/feature/2026-06-30-interception-seams.md) defines `tools/pre-execute` as an allow/deny/ask gate over an execution whose identity is already protected and whose arguments are deeply frozen. Claude Code's `PreToolUse` hook also offers `updatedInput`, so a faithful bridge needs an explicit rewrite mechanism. A rewrite cannot be a mutation escape hatch on the existing execution object: it must keep the durable history, audit record, presentation, and executed value consistent. +The [interception-seams Agent Note](../../implemented/feature/2026-06-30-interception-seams.md) defines `tools/pre-execute` as an allow/deny/ask gate over an execution whose identity is already protected and whose arguments are deeply frozen. Claude Code's `PreToolUse` hook also offers `updatedInput`, so a faithful bridge needs an explicit rewrite mechanism. A rewrite cannot be a mutation escape hatch on the existing execution object: it must keep the durable history, audit record, presentation, and executed value consistent. ## The problem: three readers of pre-execution arguments diff --git a/docs/rfc/proposed/feature/2026-07-06-recallable-compaction.md b/.agents/notes/proposed/feature/2026-07-06-recallable-compaction.md similarity index 96% rename from docs/rfc/proposed/feature/2026-07-06-recallable-compaction.md rename to .agents/notes/proposed/feature/2026-07-06-recallable-compaction.md index 6e28e8a31e..3f54030f9d 100644 --- a/docs/rfc/proposed/feature/2026-07-06-recallable-compaction.md +++ b/.agents/notes/proposed/feature/2026-07-06-recallable-compaction.md @@ -1,4 +1,4 @@ -# RFC: Recallable compaction — index checkpoints, a state checkpoint, and in-session history recall +# Agent Note: Recallable compaction — index checkpoints, a state checkpoint, and in-session history recall Status: proposed @@ -58,7 +58,7 @@ The design ships as a new backend `dsh-compact-recallable` on the existing `ctx. - **Tool-result pruning** (the in-flight pruning service): its replacement nodes carry `sourceEventSeqs`; the same registry fold lists pruned results as recallable. Follow-up scope; neither blocks the other. - **Provider-usage token accounting** (the in-flight move of compaction pressure onto provider-reported usage): supplies the guard's accounting; the implementation stacks after it. -- **"Query sessions" backlog item**: the cross-session generalization; this RFC scopes to the live session with tool names and rendering chosen so that work extends rather than collides. +- **"Query sessions" backlog item**: the cross-session generalization; this Agent Note scopes to the live session with tool names and rendering chosen so that work extends rather than collides. - **Training**: when to recall is a learned behavior. The deterministic footers and keyword anchors give training a stable target, and recall usage is fully visible in the session log for trajectory export; benchmark and RL design proceed with the post-training side. ### Follow-ups @@ -96,7 +96,7 @@ Specified during review, deferred until observation calls for them: - Nothing commits before all summaries exist and the guard passes on like-for-like accounting; a guard failure commits nothing and does not fail the turn; a mid-commit kill resumed at the next pre-step completes the pass with the state region committed unconditionally, merge base read from the log; a legacy head checkpoint is adopted as state-class. - `history_read` renders any logged checkpoint's span under budget with a working cursor; `history_search` covers every shadowed span with checkpoint-id snippets and coverage metadata, asserted in particular by finding content that exists only in a span shadowed by a superseded state checkpoint — the regression pin for trailing-slice reachability; both reject non-agent callers and never-existing ids or orphaned `compact/start` with typed errors; recalled content appears as ordinary `tool/result`s; request-reconstruction invariants pass over sessions with compaction plus recall; one keyless snapshot scenario covers compact-then-recall end to end; tool schemas and the prompt section are byte-identical across passes. - On the long-horizon bench suite: task success does not regress against `compact-basic` at equal budgets; a handoff-fidelity probe (restate K known decisions and constraints after a pass) scores no worse; recall usage frequency and hit usefulness are reported per run via the dsh bench report pipeline, alongside the stub-directory attention measurement and cache-hit telemetry. -- Seam JSDoc, the compaction capability-seam RFC, `architecture.md`, and the generated tool, config, persistence, and module-graph catalogs update in the same change; all budgets live in config; new source directories hold per-file 100% coverage with HMR disposal tests. +- Seam JSDoc, the compaction capability-seam Agent Note, `architecture.md`, and the generated tool, config, persistence, and module-graph catalogs update in the same change; all budgets live in config; new source directories hold per-file 100% coverage with HMR disposal tests. ## Risks diff --git a/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md b/.agents/notes/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md similarity index 90% rename from docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md rename to .agents/notes/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md index c05ae8d67f..620f39c4a4 100644 --- a/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md +++ b/.agents/notes/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md @@ -1,10 +1,10 @@ -# RFC: Claude Code and Codex subagent backends (out-of-process delegation to external coding agents) +# Agent Note: Claude Code and Codex subagent backends (out-of-process delegation to external coding agents) Status: proposed ## Problem -The subagent seam ([the seam RFC](../../implemented/feature/2026-06-21-subagent-capability-seam.md)) hosts multiple named providers on `ctx.subagents`, and the ACP backend ([the ACP backend RFC](../../implemented/feature/2026-06-22-acp-subagent-backend.md)) proved the seam generalizes across a process boundary; its Future-providers section explicitly named the Codex app-server and the Claude Code Agent SDK as mechanically similar siblings. Those two are the engines actually worth delegating to today: a harness turn should be able to hand a self-contained task to a real Claude Code or a real Codex — a separate product with its own model, tools, and sandbox — and get back one final answer, without the parent deployment leaking its secrets into the child or the child's behavior silently depending on whatever `~/.claude` / `~/.codex` state exists on the host machine. +The subagent seam ([the seam Agent Note](../../implemented/feature/2026-06-21-subagent-capability-seam.md)) hosts multiple named providers on `ctx.subagents`, and the ACP backend ([the ACP backend Agent Note](../../implemented/feature/2026-06-22-acp-subagent-backend.md)) proved the seam generalizes across a process boundary; its Future-providers section explicitly named the Codex app-server and the Claude Code Agent SDK as mechanically similar siblings. Those two are the engines actually worth delegating to today: a harness turn should be able to hand a self-contained task to a real Claude Code or a real Codex — a separate product with its own model, tools, and sandbox — and get back one final answer, without the parent deployment leaking its secrets into the child or the child's behavior silently depending on whatever `~/.claude` / `~/.codex` state exists on the host machine. ## Proposal @@ -49,7 +49,7 @@ Named at every tier per the root AGENTS.md rule, and de-risked up front: - **Keyless unit/integration**, mirroring the ACP spec list per backend (round-trip and output accumulation, every stop mapping, both cancel paths, already-aborted, permission auto-answer under both policies, unknown-message tolerance, bad-command spawn failure, HMR provider cleanup, export shape, isolation assertions on child env and temp-dir removal; Codex adds the auth-precheck failure path). Claude Code's harness is a scripted fake `claude` executable behind `pathToClaudeCodeExecutable` driven by the REAL SDK — a spike already passed end-to-end keyless in 24ms (the fake CLI answers one `control_request/initialize` and speaks plain stream-json, ~40 lines). Codex's harness is a scripted mock app-server subprocess speaking the verified wire protocol, the `mock-acp-server.ts` shape. - **With-key e2e** per backend: the real engine does real file work verified on disk, under a pinned opened-up config so acceptance and the do-no-harm defaults don't collide — `permissionMode: 'acceptEdits'` for Claude Code, `sandboxMode: 'workspace-write'` + `approvalPolicy: 'never'` for Codex; self-skips report exactly what is missing (binary vs key). CI has no secrets, so these run locally per the with-key policy. -- **Snapshot**: deferred as `TODO(claude-code-subagent-replay)` / `TODO(codex-subagent-replay)` — the same distinct replay shape the ACP backend deferred ([the per-session replay RFC](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md)); the keyless suites carry deterministic coverage meanwhile. +- **Snapshot**: deferred as `TODO(claude-code-subagent-replay)` / `TODO(codex-subagent-replay)` — the same distinct replay shape the ACP backend deferred ([the per-session replay Agent Note](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md)); the keyless suites carry deterministic coverage meanwhile. ## Alternatives considered @@ -59,7 +59,7 @@ The dispose ladder and env scrub require owning the child process (spawn args, e ### Why not a model-visible `subagent_type` parameter (one Task-style tool)? -Claude Code's own Task tool puts the subagent type in the model-facing schema, selecting a prompt-plus-toolset persona. Here the choice is between EXECUTION ENGINES, and only the deployer knows which engines have credentials configured — so selection stays deployment config, preserving `dsh-tool-subagent`'s documented one-provider-per-tool contract. A persona-style type selector would be a separate RFC against the tool, not the backends. +Claude Code's own Task tool puts the subagent type in the model-facing schema, selecting a prompt-plus-toolset persona. Here the choice is between EXECUTION ENGINES, and only the deployer knows which engines have credentials configured — so selection stays deployment config, preserving `dsh-tool-subagent`'s documented one-provider-per-tool contract. A persona-style type selector would be a separate Agent Note against the tool, not the backends. ### Why not login-state credentials and the user's own config? @@ -71,7 +71,7 @@ Injecting a fake `query()` would mock our own boundary and leave the real SDK lo ### Why not ACP adapters (e.g. `claude-code-acp`) reusing the existing backend? -Community shims wrap both engines in ACP, which would make them "just config" on `dsh-subagent-acp`. But that inserts an unofficial third-party layer between the harness and the engine, erases the native control surfaces this RFC exposes (permissionMode, sandboxMode/approvalPolicy, config-dir isolation, apiKey RPC), and trades first-party protocol stability for a shim's release cadence. First-party surfaces — the Agent SDK and the app-server — are the supported integration points. +Community shims wrap both engines in ACP, which would make them "just config" on `dsh-subagent-acp`. But that inserts an unofficial third-party layer between the harness and the engine, erases the native control surfaces this Agent Note exposes (permissionMode, sandboxMode/approvalPolicy, config-dir isolation, apiKey RPC), and trades first-party protocol stability for a shim's release cadence. First-party surfaces — the Agent SDK and the app-server — are the supported integration points. ## Acceptance criteria diff --git a/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.md b/.agents/notes/proposed/feature/2026-07-08-interactive-side-sessions.md similarity index 95% rename from docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.md rename to .agents/notes/proposed/feature/2026-07-08-interactive-side-sessions.md index 7caceed4ba..f2e3e74ca1 100644 --- a/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.md +++ b/.agents/notes/proposed/feature/2026-07-08-interactive-side-sessions.md @@ -1,4 +1,4 @@ -# RFC: Interactive side sessions and merge-back +# Agent Note: Interactive side sessions and merge-back Status: proposed @@ -13,7 +13,7 @@ A **side session** is an ordinary live session forked at the source's last compl - **Fork and attach:** create the child with the parent's balanced completed-turn prefix and stamp `parentSession` and `seedLength` in its metadata. This composes `ctx.agents.create({ seed, meta })`; it adds no core service or session-store method. - **Advisor framing:** inject one plugin-sourced `context/message` after creation that tells the child to explain without mutating or continuing the task. Keeping the system prompt byte-identical preserves the provider prefix cache over inherited history. - **Merge-back:** ask the child for a length-capped handback, then inject one plugin-sourced `context/message` into the parent. The next parent request sees it at its logged position, preserving replay and [request reconstructability](../../implemented/architecture/2026-07-05-reconstructable-requests.md) without a new session event. -- **Presentation:** invocation, session switching, and handback rendering belong to the first client-owned surface. This RFC specifies only the surface-independent mechanics. +- **Presentation:** invocation, session switching, and handback rendering belong to the first client-owned surface. This Agent Note specifies only the surface-independent mechanics. Rewind productization, session-tree views, a model-facing side-session tool, and `forkName`/`mergedInto` metadata are out of scope. A live-adapter spike validated source-log isolation, inherited context, a multi-turn child exchange, and merge-back visibility in the parent's next turn. diff --git a/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.md b/.agents/notes/proposed/feature/2026-07-10-sqlite-session-query-provider.md similarity index 97% rename from docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.md rename to .agents/notes/proposed/feature/2026-07-10-sqlite-session-query-provider.md index 36216f74d8..d643d223dd 100644 --- a/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.md +++ b/.agents/notes/proposed/feature/2026-07-10-sqlite-session-query-provider.md @@ -1,4 +1,4 @@ -# RFC: SQLite FTS5 session search +# Agent Note: SQLite FTS5 session search Status: proposed @@ -44,7 +44,7 @@ Reconciliation may use stable fingerprints to avoid rewriting unchanged persiste - Tests cover both search scopes, content-bearing results, chainable result filters, surface defaults, snippets, escaping, deterministic ties, pagination, scoped stale cursors, cancellation, dynamic persistence mount/unmount, and recovery after a failed transaction. - A schema mismatch resets only the derived database. - A keyless end-to-end test combines a real persistence backend with the real SQLite search package. -- The RFC is amended to the measured tokenizer and public API actually implemented before moving to `implemented/`. +- The Agent Note is amended to the measured tokenizer and public API actually implemented before moving to `implemented/`. ## Risks diff --git a/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md b/.agents/notes/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md similarity index 99% rename from docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md rename to .agents/notes/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md index 200ed50b1e..0c6516080a 100644 --- a/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md +++ b/.agents/notes/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md @@ -1,4 +1,4 @@ -# RFC: Stream workflow progress through tool calls +# Agent Note: Stream workflow progress through tool calls Status: proposed diff --git a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml similarity index 64% rename from docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml rename to .agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml index 36bb26161f..c876ddc68f 100644 --- a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml +++ b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.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-14-sdk-developer-projects.md: 0b5fe876f92153e1ccf5bd8fe383464f5087f4b1 -2026-07-14-sdk-developer-projects.zh.md: ec08f323acba1b9dc049182937fda34bfb50d4ee +2026-07-14-sdk-developer-projects.md: 1be9abcad1e51a1b9a1406f21ce60073427576e0 +2026-07-14-sdk-developer-projects.zh.md: a8ba1d658f78484a46a7148a4e2ff1b073a3e9f2 diff --git a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.md b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md similarity index 99% rename from docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.md rename to .agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md index 0b5fe876f9..1be9abcad1 100644 --- a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.md +++ b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md @@ -1,4 +1,4 @@ -# RFC: Developer-owned SDK projects +# Agent Note: Developer-owned SDK projects Status: proposed diff --git a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.zh.md b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.zh.md similarity index 99% rename from docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.zh.md rename to .agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.zh.md index ec08f323ac..a8ba1d658f 100644 --- a/docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.zh.md +++ b/.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.zh.md @@ -1,4 +1,4 @@ -# RFC: 开发者拥有的 SDK 工程 +# Agent Note: 开发者拥有的 SDK 工程 Status: proposed diff --git a/docs/rfc/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml similarity index 63% rename from docs/rfc/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml rename to .agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml index 933cff9cb5..8b70484312 100644 --- a/docs/rfc/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml +++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-17-sdk-follow-up-capabilities.md: dac859f95e4ee4c628c50be72b3a18720f2b19fb -2026-07-17-sdk-follow-up-capabilities.zh.md: 55648a368b4f9aa865129f3e0501e417d15f09ed +2026-07-17-sdk-follow-up-capabilities.md: 0f3ada6bdbb4ce933d14602cf59be9a51640e61c +2026-07-17-sdk-follow-up-capabilities.zh.md: d0d0b3e6bcdf192e64f003dc9f6e90cc2bdb060b diff --git a/docs/rfc/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md similarity index 96% rename from docs/rfc/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md rename to .agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md index dac859f95e..0f3ada6bdb 100644 --- a/docs/rfc/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md +++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.md @@ -1,4 +1,4 @@ -# RFC: SDK follow-up capabilities +# Agent Note: SDK follow-up capabilities Status: proposed @@ -6,7 +6,7 @@ English | [中文](2026-07-17-sdk-follow-up-capabilities.zh.md) ## Problem -The first SDK release creates and edits developer-owned Cordis projects through the shared model defined by the [developer-project RFC](2026-07-14-sdk-developer-projects.md) and the [project-editing architecture](../architecture/2026-07-15-sdk-project-editing-architecture.md). Its create and config workflows are interactive, external Cordis plugins require manual dependency and configuration edits, command-line telemetry has no owning boundary, and interactive branches lack a stable test strategy. +The first SDK release creates and edits developer-owned Cordis projects through the shared model defined by the [developer-project Agent Note](2026-07-14-sdk-developer-projects.md) and the [project-editing architecture](../architecture/2026-07-15-sdk-project-editing-architecture.md). Its create and config workflows are interactive, external Cordis plugins require manual dependency and configuration edits, command-line telemetry has no owning boundary, and interactive branches lack a stable test strategy. These gaps are coupled. Create and config already share questions, feature configuration, and `ProjectEditSession`; adding separate automation paths would duplicate that domain logic. External-plugin installation must update both the package manager's files and `cordis.yml`. Telemetry must observe commands such as create and build that do not boot Cordis. Interactive testing must exercise Harness behavior without making terminal rendering a brittle product contract. diff --git a/docs/rfc/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md similarity index 95% rename from docs/rfc/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md rename to .agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md index 55648a368b..d0d0b3e6bc 100644 --- a/docs/rfc/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md +++ b/.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md @@ -1,4 +1,4 @@ -# RFC: SDK 后续功能 +# Agent Note: SDK 后续功能 Status: proposed @@ -6,7 +6,7 @@ Status: proposed ## 问题 -首个 SDK 版本通过[开发者工程 RFC](2026-07-14-sdk-developer-projects.md) 和 [SDK 工程编辑架构](../architecture/2026-07-15-sdk-project-editing-architecture.md)定义的共享模型创建和编辑开发者拥有的 Cordis 工程。create 和 config 工作流仅支持交互调用,接入外部 Cordis 插件需要手工修改依赖和配置,命令行遥测没有明确的所属边界,交互分支也缺少稳定的测试策略。 +首个 SDK 版本通过[开发者工程 Agent Note](2026-07-14-sdk-developer-projects.md) 和 [SDK 工程编辑架构](../architecture/2026-07-15-sdk-project-editing-architecture.md)定义的共享模型创建和编辑开发者拥有的 Cordis 工程。create 和 config 工作流仅支持交互调用,接入外部 Cordis 插件需要手工修改依赖和配置,命令行遥测没有明确的所属边界,交互分支也缺少稳定的测试策略。 这些缺口彼此关联。create 和 config 已经共享问题、功能配置和 `ProjectEditSession`;若另建自动化路径,就会复制领域逻辑。安装外部插件必须同时修改包管理器文件和 `cordis.yml`。遥测需要观察 create、build 等不会启动 Cordis 的命令。交互测试需要覆盖 Harness 自身行为,同时避免把终端渲染固化成脆弱的产品契约。 diff --git a/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.md b/.agents/notes/proposed/process/2026-06-11-api-extractor-reports.md similarity index 81% rename from docs/rfc/proposed/process/2026-06-11-api-extractor-reports.md rename to .agents/notes/proposed/process/2026-06-11-api-extractor-reports.md index a6f3bfb14c..b32dc7e2af 100644 --- a/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.md +++ b/.agents/notes/proposed/process/2026-06-11-api-extractor-reports.md @@ -1,8 +1,8 @@ -# RFC: API extractor reports +# Agent Note: API extractor reports Status: proposed -> Split out from the original "Doc-sync and API reports" RFC (2026-06-11). Parts 1-2 (doc-block typechecking, event-taxonomy verification) shipped — see [doc-sync enforcement](../../implemented/process/2026-06-11-doc-sync-enforcement.md). This is the deferred part 3, kept as a standalone proposal. +> Split out from the original "Doc-sync and API reports" Agent Note (2026-06-11). Parts 1-2 (doc-block typechecking, event-taxonomy verification) shipped — see [doc-sync enforcement](../../implemented/process/2026-06-11-doc-sync-enforcement.md). This is the deferred part 3, kept as a standalone proposal. ## Problem diff --git a/docs/rfc/proposed/process/2026-06-11-architectural-conformance.md b/.agents/notes/proposed/process/2026-06-11-architectural-conformance.md similarity index 93% rename from docs/rfc/proposed/process/2026-06-11-architectural-conformance.md rename to .agents/notes/proposed/process/2026-06-11-architectural-conformance.md index e0d16b455d..006aa76ad1 100644 --- a/docs/rfc/proposed/process/2026-06-11-architectural-conformance.md +++ b/.agents/notes/proposed/process/2026-06-11-architectural-conformance.md @@ -1,4 +1,4 @@ -# RFC: Architectural conformance — dependency rules and the adapter kit +# Agent Note: Architectural conformance — dependency rules and the adapter kit Status: proposed @@ -31,4 +31,4 @@ dependency-cruiser config + CI step first (an hour of work, permanent guarantee) Dep-cruiser rule maintenance as packages are added — keep rules pattern-based (`dsh-*`) rather than enumerated. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md b/.agents/notes/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md similarity index 97% rename from docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md rename to .agents/notes/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md index e787133026..a79f719751 100644 --- a/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md +++ b/.agents/notes/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md @@ -1,4 +1,4 @@ -# RFC: Supply chain checks and vendor drift verification +# Agent Note: Supply chain checks and vendor drift verification Status: proposed diff --git a/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.md b/.agents/notes/proposed/process/2026-06-20-discover-package-inventory.md similarity index 95% rename from docs/rfc/proposed/process/2026-06-20-discover-package-inventory.md rename to .agents/notes/proposed/process/2026-06-20-discover-package-inventory.md index 8e4f4cce13..fa544d6ddb 100644 --- a/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.md +++ b/.agents/notes/proposed/process/2026-06-20-discover-package-inventory.md @@ -1,4 +1,4 @@ -# RFC: Discover package inventories instead of maintaining static lists +# Agent Note: Discover package inventories instead of maintaining static lists Status: proposed @@ -30,4 +30,4 @@ One cataloged item needs no generator at all: folding the e2e entry glob into kn Discovery scripts can become too clever. The implementation should stay boring: read manifests, filter on explicit fields, print the resolved list, and fail loud. The payoff is removing manual inventory drift, not inventing a build system. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/proposed/process/2026-07-13-human-review-skill-maintenance.md b/.agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md similarity index 96% rename from docs/rfc/proposed/process/2026-07-13-human-review-skill-maintenance.md rename to .agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md index 8098247fdf..6d4de215c9 100644 --- a/docs/rfc/proposed/process/2026-07-13-human-review-skill-maintenance.md +++ b/.agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md @@ -1,4 +1,4 @@ -# RFC: Periodic human-review maintenance for dsh-code-review +# Agent Note: Periodic human-review maintenance for dsh-code-review Status: proposed @@ -8,7 +8,7 @@ The `dsh-code-review` skill records failure modes that require reviewer judgment ## Proposal -Periodic out-of-repo maintenance. A private tool, kept on the skill maintainer's machine rather than committed to this repository, runs against a clean full-history checkout at refreshed `origin/master`. The intended scheduler runs daily with a two-UTC-day overlap; manual runs accept another `--since` duration or repeated `--pr` arguments for an explicit set. The scan is idempotent against the current skill and stores no repository cursor. The only repository file changed by promotion is [.agents/skills/dsh-code-review/SKILL.md](../../../../.agents/skills/dsh-code-review/SKILL.md); the draft PR carries a provenance summary so reviewers can audit the source feedback and adoption evidence without the private adapter logs. +Periodic out-of-repo maintenance. A private tool, kept on the skill maintainer's machine rather than committed to this repository, runs against a clean full-history checkout at refreshed `origin/master`. The intended scheduler runs daily with a two-UTC-day overlap; manual runs accept another `--since` duration or repeated `--pr` arguments for an explicit set. The scan is idempotent against the current skill and stores no repository cursor. The only repository file changed by promotion is [.agents/skills/dsh-code-review/SKILL.md](../../../skills/dsh-code-review/SKILL.md); the draft PR carries a provenance summary so reviewers can audit the source feedback and adoption evidence without the private adapter logs. ```mermaid flowchart TD @@ -50,7 +50,7 @@ The promote helper starts from a clean checkout at refreshed `origin/master` and ### Where the mechanism lives -The tool source, adapter binaries, provider credentials, and intended daily scheduler are kept private to the maintainer's machine rather than committed to this repository. This document specifies the protocol; the reference implementation is private infrastructure. The mechanism serves a single skill maintained by a single operator, so the ongoing cost of vetting mechanism edits through repository review outweighs any provenance benefit. If the mechanism is ever handed off to a second maintainer, that handoff is a follow-up RFC that revises this decision — the operator doc at [docs/cookbook/maintaining-dsh-code-review.md](../../../cookbook/maintaining-dsh-code-review.md) is the entry point for anyone taking over. +The tool source, adapter binaries, provider credentials, and intended daily scheduler are kept private to the maintainer's machine rather than committed to this repository. This document specifies the protocol; the reference implementation is private infrastructure. The mechanism serves a single skill maintained by a single operator, so the ongoing cost of vetting mechanism edits through repository review outweighs any provenance benefit. If the mechanism is ever handed off to a second maintainer, that handoff is a follow-up Agent Note that revises this decision — the operator doc at [docs/cookbook/maintaining-dsh-code-review.md](../../../../docs/cookbook/maintaining-dsh-code-review.md) is the entry point for anyone taking over. ## Alternatives considered @@ -80,4 +80,4 @@ Promotion from `proposed/` to `implemented/` requires all of the following to be - **Two-non-candidate classifications routed to `excluded` without a dispute round.** When both classifiers say "not a candidate" but disagree on which non-candidate reason applies (for example `covered` vs `specific`), the item is excluded rather than re-evaluated. Both classifiers agree the item does not become new reviewer behavior, so a dispute round would not change the outcome. - **Dual-reviewer independence beyond byte-hash distinctness is a deployment contract.** The tool refuses to run when the two commands resolve to byte-identical executables, but cannot verify that two distinct wrappers back different providers or models. Operators must configure independent primary and secondary adapters. - **Best-effort compare-and-swap for candidate writes and rollback.** File-based CAS on POSIX is not truly atomic; the window is one event-loop tick. The tool targets single-user periodic maintenance and a truly concurrent editor is out of scope. -- **Single-maintainer bus factor.** Because the mechanism lives on one machine, its interruption stops skill maintenance entirely until the operator restores service or hands off to a new maintainer through a follow-up RFC. +- **Single-maintainer bus factor.** Because the mechanism lives on one machine, its interruption stops skill maintenance entirely until the operator restores service or hands off to a new maintainer through a follow-up Agent Note. diff --git a/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md b/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md similarity index 87% rename from docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md rename to .agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md index f874784a6d..1f52bfac25 100644 --- a/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md +++ b/.agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md @@ -1,4 +1,4 @@ -# RFC: Prune dead public and result surface +# Agent Note: Prune dead public and result surface Status: proposed @@ -6,14 +6,14 @@ Status: proposed Several package-root exports, result fields, and convenience methods have no production consumer. They survive because tests import internals through public entry points or because a type anticipated a caller that never arrived. Each item is small in isolation, but together they enlarge the SDK contract, generated catalogs, documentation, and regression matrix without enabling a shipped path. -The production corpus is `packages/*/*/src`, example sources/config, and runtime scripts. Tests, package READMEs, and RFC prose are evidence of publication but not fixed callers. `cordis_inspect` makes `packages/cordis/tool-cordis/src/api-catalog.ts` model-visible, and `cordis_mount` can invoke injected services through guarded real-service proxies, so catalogued service methods and returned shapes are a genuine dynamic product surface. The table therefore distinguishes absence of a fixed repository caller from unreachability: rows touching catalogued vocabulary intentionally contract what model-written mounts can discover and call, while package-root implementation helpers are not reached through that service façade. Exact-symbol searches produce the following inventory: +The production corpus is `packages/*/*/src`, example sources/config, and runtime scripts. Tests, package READMEs, and Agent Note prose are evidence of publication but not fixed callers. `cordis_inspect` makes `packages/cordis/tool-cordis/src/api-catalog.ts` model-visible, and `cordis_mount` can invoke injected services through guarded real-service proxies, so catalogued service methods and returned shapes are a genuine dynamic product surface. The table therefore distinguishes absence of a fixed repository caller from unreachability: rows touching catalogued vocabulary intentionally contract what model-written mounts can discover and call, while package-root implementation helpers are not reached through that service façade. Exact-symbol searches produce the following inventory: | Surface | Production evidence | Simplification | | --- | --- | --- | | `SurfaceManager.invalidate()` | Only its unit test calls it; seeding completes before the lazily-created manager exists and the session never replaces its log reference. | Delete it and its impossible wholesale-replacement contract. | | `ToolExecutionResult.callId` | Every hook already receives the immutable `ToolExecution`; the loop and ACP correlate through the call/session event. No consumer reads the duplicate result field. | Remove the field, copy/mismatch guards, and tests that prove the duplicate cannot disagree. | | `ReactLoopAgent` root export | Outside-package named imports are tests; production programs against `Agent` and creates/resumes through `ctx.agents`. | Return/interface-type `Agent` and make the concrete loop class package-internal; keep the deliberate synchronous config-only `AgentLoop.create()` path. | -| `workflow-workerthread` protocol/runtime/session re-exports and named `WorkerWorkflowEngine` | Every package-name consumer uses the default engine; the workflow RFC already defines the worker wire protocol as private. | Keep the default plugin class/config contract; drop the duplicate named class export and keep protocol modules source-private. | +| `workflow-workerthread` protocol/runtime/session re-exports and named `WorkerWorkflowEngine` | Every package-name consumer uses the default engine; the workflow Agent Note already defines the worker wire protocol as private. | Keep the default plugin class/config contract; drop the duplicate named class export and keep protocol modules source-private. | | `code-runtime-worker` protocol/bootstrap re-exports | Outside-package production/e2e consumers use `WorkerCodeRuntime` and config, not `BootstrapPort`, `PatchableStream`, or worker message/boot types. | Keep the runtime class/config contract and make its wire/bootstrap vocabulary source-private. | | ACP translation/presenter root exports | `agentOptions`, `streamSessionEventUpdate`, `todosToPlan`, `ToolPresenter`, `nullToolPresenter`, and `TerminalRendering` have only same-file or ACP-test consumers; the sole outside-package production consumer mounts the plugin namespace. | Keep `name`, `inject`, `Config`, `AcpConfig`, and `apply`; make translation/presentation helpers source-private and test them in-package. | | `providerWording` and `completedTurnPrefix` root exports | Each has one same-package production caller; only the balanced-prefix helper has a same-package white-box test. | Make them source-private and test provider behavior. | @@ -23,7 +23,7 @@ The production corpus is `packages/*/*/src`, example sources/config, and runtime | `BlockAssembler.push()` return value | Both production callers ignore the returned completed block. | Return `void`; keep the deliberately public `blocks()`/`message()` contract. | | `compactRegion`'s separate `session` argument | The fixed caller passes the same object already present as `agent.session`; the model-visible mount API can also call the method, but accepting two identities permits a mounted plugin to provide an incoherent pair. | Keep the manual-region seam while deliberately narrowing it to `agent.session` as the one source of truth. | | `CompactionResult.startSeq`, `summarySeq`, `endSeq`, and `summary` | The production consumer reads only shadowed range/seq/token accounting; the durable log owns summary and event identity. | Remove the four result echoes while keeping both shared transcript renderers. | -| `BasicCompactService` estimation/summarization visibility | No outside production caller invokes the five methods; the implemented RFC names only `estimateContentTokens()` and `summarize()` as subclass hooks. | Make those two `protected` and the three orchestration-only estimators private. | +| `BasicCompactService` estimation/summarization visibility | No outside production caller invokes the five methods; the implemented Agent Note names only `estimateContentTokens()` and `summarize()` as subclass hooks. | Make those two `protected` and the three orchestration-only estimators private. | | `CodeLogEntry.source`/`level` and `RunCodeMeta.dispatches` | Every production consumer maps logs to text; no presenter/model path reads the other fields or the persisted dispatch count. | Make code-runtime logs strings (or text-only entries) and remove result-meta dispatch plumbing; keep the local counter that mints deterministic dispatch ids. | | `CodeRuntime.language` and `CodeRuntime.isolation` | The worker backend supplies the only production values, while Code Mode and every other production caller invoke only `run()`. | Remove the unread descriptors while preserving the worker's language, isolation, budgets, cancellation, and disposal behavior. | | `ToolNotFoundError.toolName`, `SystemPrompt.config`, and `BashTask.command` | Each stored public value has no production reader. | Drop the unread field while retaining error messages, resolved configuration behavior, and task lifecycle. | @@ -51,8 +51,8 @@ Remove or demote every row as one bounded coordinated public-surface cleanup. Up ## Acceptance criteria -- Exact-symbol searches show no removed surface outside this RFC and any implemented-RFC amendments. -- Every surface listed in this RFC is absent or demoted as specified; deliberately retained extension/test contracts outside the inventory are unchanged. +- Exact-symbol searches show no removed surface outside this Agent Note and any implemented-Agent Note amendments. +- Every surface listed in this Agent Note is absent or demoted as specified; deliberately retained extension/test contracts outside the inventory are unchanged. - Tool execution, compaction, both LLM adapters, both persistence backends, workflow isolation, and agent creation/resume retain their shipped behavior. - Typecheck, coverage, snapshots, doc-sync, module-graph verification, build, and hygiene pass. diff --git a/docs/rfc/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml similarity index 63% rename from docs/rfc/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml rename to .agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml index e04aed96a6..50dfd13aab 100644 --- a/docs/rfc/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml +++ b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-make-jsonrpc-directional.md: 2a9579c53c111887e9d93cf02cc832304a496795 -2026-07-19-make-jsonrpc-directional.zh.md: 4f3793f1b4de3b4f69c4219c10fe5b3b360c8692 +2026-07-19-make-jsonrpc-directional.md: 74de3c960a415a9a2601e57ec75f244ca753193d +2026-07-19-make-jsonrpc-directional.zh.md: 76228ba56cfbd4fb86f39d0d0873d49edb13309b diff --git a/docs/rfc/proposed/simplification/2026-07-19-make-jsonrpc-directional.md b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md similarity index 98% rename from docs/rfc/proposed/simplification/2026-07-19-make-jsonrpc-directional.md rename to .agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md index 2a9579c53c..74de3c960a 100644 --- a/docs/rfc/proposed/simplification/2026-07-19-make-jsonrpc-directional.md +++ b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.md @@ -1,4 +1,4 @@ -# RFC: Make JSON-RPC completion and transport directional +# Agent Note: Make JSON-RPC completion and transport directional Status: proposed diff --git a/docs/rfc/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md similarity index 98% rename from docs/rfc/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md rename to .agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md index 4f3793f1b4..76228ba56c 100644 --- a/docs/rfc/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md +++ b/.agents/notes/proposed/simplification/2026-07-19-make-jsonrpc-directional.zh.md @@ -1,4 +1,4 @@ -# RFC: 让 JSON-RPC 完成结果与传输方向单一化 +# Agent Note: 让 JSON-RPC 完成结果与传输方向单一化 Status: proposed diff --git a/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.md b/.agents/notes/proposed/testing/2026-06-11-deterministic-and-stress-testing.md similarity index 92% rename from docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.md rename to .agents/notes/proposed/testing/2026-06-11-deterministic-and-stress-testing.md index 2a30969ba4..c3b17c401a 100644 --- a/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.md +++ b/.agents/notes/proposed/testing/2026-06-11-deterministic-and-stress-testing.md @@ -1,4 +1,4 @@ -# RFC: Deterministic tests, the replay invariant fixture, and race stress +# Agent Note: Deterministic tests, the replay invariant fixture, and race stress Status: proposed @@ -28,4 +28,4 @@ Land 1 and 2 together (they touch the same helpers); add the nightly job after t Fake timers interact subtly with Promise scheduling in the loop — prefer event-driven waits; reserve fake timers for timer-service behavior itself. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/proposed/testing/2026-06-11-mutation-testing.md b/.agents/notes/proposed/testing/2026-06-11-mutation-testing.md similarity index 93% rename from docs/rfc/proposed/testing/2026-06-11-mutation-testing.md rename to .agents/notes/proposed/testing/2026-06-11-mutation-testing.md index 8c68ccf09f..35df228b85 100644 --- a/docs/rfc/proposed/testing/2026-06-11-mutation-testing.md +++ b/.agents/notes/proposed/testing/2026-06-11-mutation-testing.md @@ -1,4 +1,4 @@ -# RFC: Mutation testing as the coverage counterweight +# Agent Note: Mutation testing as the coverage counterweight Status: proposed @@ -31,4 +31,4 @@ Stryker (`@stryker-mutator/vitest-runner`) over `packages/*/src`: Runtime: mutation testing is expensive; per-file 100% coverage helps (every mutant is at least reached). If PR-scoped runs stay too slow, keep them nightly-only and rely on the score ratchet. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.md b/.agents/notes/rejected/architecture/2026-06-11-immutable-public-surfaces.md similarity index 95% rename from docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.md rename to .agents/notes/rejected/architecture/2026-06-11-immutable-public-surfaces.md index 3eedd92a2d..0c6652a94d 100644 --- a/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.md +++ b/.agents/notes/rejected/architecture/2026-06-11-immutable-public-surfaces.md @@ -1,4 +1,4 @@ -# RFC: Deep-readonly public surfaces +# Agent Note: Deep-readonly public surfaces Status: rejected — the pervasive `DeepReadonly<T>` type flip is replaced by source-owned runtime immutability in `Session` plus relational development assertions. See [source-owned session immutability and dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md). @@ -24,4 +24,4 @@ Introduce `DeepReadonly`, flip the session read paths, and fix the resulting com `DeepReadonly` types can produce noisy errors at waterfall boundaries where mutation IS the API — keep the mutable/readonly boundary exactly at "logged vs in-flight" and document it in the session README. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.md b/.agents/notes/rejected/architecture/2026-06-20-providerless-example-base.md similarity index 90% rename from docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.md rename to .agents/notes/rejected/architecture/2026-06-20-providerless-example-base.md index 0fd4ca27f7..81dbe40ee4 100644 --- a/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.md +++ b/.agents/notes/rejected/architecture/2026-06-20-providerless-example-base.md @@ -1,4 +1,4 @@ -# RFC: Make the shared example base providerless +# Agent Note: Make the shared example base providerless Status: rejected — superseded by [Extract example apps into packages](../../implemented/architecture/2026-06-20-extract-example-app-packages.md), which moves the spine into a `dsh-agent-spine-demo` bundle and deletes the `base*.yml` files, so there is no shared base YAML left to rename. @@ -20,10 +20,10 @@ The shared base should contain only provider-neutral services and tools: `llm`, - `examples/base-core.yml` is deleted. - Real demo configs explicitly add the DeepSeek adapter. - Snapshot replay config includes the same providerless base and its replay adapter. -- The [examples README](../../../../examples/README.md), example-specific READMEs, and RFC references stop explaining "base = base-core plus adapter". +- The [examples README](../../../../examples/README.md), example-specific READMEs, and Agent Note references stop explaining "base = base-core plus adapter". ## What we give up Real demos lose one layer of convenience: each must opt into the adapter. That is the right default for examples, because adapter choice is the variable part and providerless wiring is the shared product core. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/.agents/notes/rejected/process/2026-07-04-generate-agent-note-index-tables.md b/.agents/notes/rejected/process/2026-07-04-generate-agent-note-index-tables.md new file mode 100644 index 0000000000..90c942411d --- /dev/null +++ b/.agents/notes/rejected/process/2026-07-04-generate-agent-note-index-tables.md @@ -0,0 +1,36 @@ +# Agent Note: Generate the Agent Note index tables + +Status: rejected — a centralized generated list is merge-prone and adds little discovery value + +## Problem + +Per-lifecycle/per-class tables would list facts that are fully derivable: an Agent Note's path encodes lifecycle and class, its filename encodes the first-proposed date, and its H1 carries the title. A hand-maintained copy of those facts would also be a high-contention docs hotspot because concurrent Agent Note branches append rows to the same few lines. [The classification Agent Note](../../implemented/process/2026-06-20-agent-note-classification.md) makes the tree itself authoritative. + +## Proposal + +Keep the curated prose and generate the list as a fully generated `.agents/notes/INDEX.md`. A shared `scripts/agent-note-index.ts` module would own both the tree walker and the renderer. Two thin consumers would share it: + +- `scripts/gen-agent-note-index.ts` (`pnpm run gen-agent-note-index`) would rewrite INDEX.md in full from the tree. +- `scripts/verify-agent-note-classification.ts` would check structure and assert that the committed INDEX.md byte-matches a fresh render. + +Adding, moving, or deleting an Agent Note would mean editing the Agent Note file and running the generator. + +## Alternatives considered + +### Why not marker-delimited regions inside README.md? + +Marker-delimited tables inside README.md would mix generated and curated text, requiring splice mechanics and protection for the surrounding contract. A dedicated generated file would at least keep those concerns separate. + +### Why not the verifier-only model? + +It catches mistakes but still makes every proposal edit a shared hotspot in a hand-maintained table. The author has already named and placed the file, so the index copy adds no information. This is the same hand-list-versus-derivation judgment the [package-inventory proposal](../../proposed/process/2026-06-20-discover-package-inventory.md) applies to tsconfig references and knip stanzas. + +## Consequences + +- The generated file would be explicit and contain no curated region. +- A malformed or missing H1 would be a hard error because the H1 supplies each row title. +- Concurrent branches would still modify the same committed artifact, even if conflicts could be resolved by rerunning the generator. + +## Related + +The implemented [no-index decision](../../implemented/process/2026-07-19-remove-generated-agent-note-index.md) keeps the tree and repository search as the discovery mechanisms. diff --git a/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md b/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md similarity index 84% rename from docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md rename to .agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md index 088fa8d25d..62dd7609e8 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md +++ b/.agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md @@ -1,10 +1,10 @@ -# RFC: Persist assembled assistant messages, not stream chunks +# Agent Note: Persist assembled assistant messages, not stream chunks Status: rejected — high-fidelity chunk replay, partial failed streams, and snapshot replay currently depend on persisted `assistant/chunk` events. Dropping chunks is only viable with a no-information-loss replay/artifact replacement. ## Problem -The canonical session log currently persists every `assistant/chunk` exactly as streamed by the model. The [session persistence RFC](../../implemented/architecture/2026-06-14-session-persistence.md) chose this for token-level replay fidelity and contiguous `seq`, but the cost has grown: JSONL fixtures are dominated by tiny delta records, snapshot scenarios replay the model by grouping chunk events, ACP load reconstructs prior assistant output from chunks, and any future log reader must distinguish durable message history from token-level trace. +The canonical session log currently persists every `assistant/chunk` exactly as streamed by the model. The [session persistence Agent Note](../../implemented/architecture/2026-06-14-session-persistence.md) chose this for token-level replay fidelity and contiguous `seq`, but the cost has grown: JSONL fixtures are dominated by tiny delta records, snapshot scenarios replay the model by grouping chunk events, ACP load reconstructs prior assistant output from chunks, and any future log reader must distinguish durable message history from token-level trace. For successful steps that assemble completed content, the loop already appends an `assistant/message`. That is the event `deriveMessages()` uses for the next model request. In other words, the normal resumable conversation state is already present without the chunks; chunks are a live rendering and deterministic-test artifact, not required conversation history. Failed or aborted streams are different: partial assistant output may exist only as chunks, and empty max-token steps may produce no `assistant/message` at all. @@ -31,4 +31,4 @@ The canonical user session no longer reconstructs the exact token stream of an o This supersedes the chunk-persistence choice in [session persistence](../../implemented/architecture/2026-06-14-session-persistence.md) and affects [ACP snapshot tests](../../implemented/testing/2026-06-19-acp-snapshot-tests.md), whose current replay plugin derives its script from `assistant/chunk` events. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.md b/.agents/notes/rejected/simplification/2026-06-20-drop-acp-session-load.md similarity index 93% rename from docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.md rename to .agents/notes/rejected/simplification/2026-06-20-drop-acp-session-load.md index 18cd6e981d..b8b9b29987 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.md +++ b/.agents/notes/rejected/simplification/2026-06-20-drop-acp-session-load.md @@ -1,4 +1,4 @@ -# RFC: Drop ACP session/load until resume has a product shape +# Agent Note: Drop ACP session/load until resume has a product shape Status: rejected — Zed is the current target ACP client, advertises and exercises load-capable sessions, and keeps pending-load state for concurrent `session/load`. The bridge should keep `session/load` and make the resume contract solid. @@ -24,4 +24,4 @@ For now, ACP starts fresh sessions only. `initialize` advertises `loadSession: f An editor cannot reopen a prior persisted session through ACP. That is a real product feature, but the current implementation is ahead of the UX and ties the bridge to token-level log replay. Keeping persistence while dropping editor load narrows the bridge to the workflow it can currently present cleanly. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md b/.agents/notes/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md similarity index 68% rename from docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md rename to .agents/notes/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md index 2f1408dc4c..8ce4803f73 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md +++ b/.agents/notes/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md @@ -1,10 +1,10 @@ -# RFC: Drop ACP terminal `_meta` rendering +# Agent Note: Drop ACP terminal `_meta` rendering Status: rejected — Zed is the current target client, and the terminal `_meta` convention is intentional Zed UX with a plain ACP fallback for other clients. ## Problem -The ACP bridge implements a Zed-specific terminal-card convention through `_meta.terminal_info`, `_meta.terminal_output`, and `_meta.terminal_exit`. The implemented [rich ACP bash rendering RFC](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) deliberately avoided ACP's client-side `terminal/create` because bash execution belongs in the harness, but still adopted the reference agents' display-only `_meta` convention. That gives a nicer Zed card at the cost of bridge state, capability negotiation, terminal ids, special update mapping, text fallback tests, and exit-pill parsing in `dsh-tool-bash`. +The ACP bridge implements a Zed-specific terminal-card convention through `_meta.terminal_info`, `_meta.terminal_output`, and `_meta.terminal_exit`. The implemented [rich ACP bash rendering Agent Note](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) deliberately avoided ACP's client-side `terminal/create` because bash execution belongs in the harness, but still adopted the reference agents' display-only `_meta` convention. That gives a nicer Zed card at the cost of bridge state, capability negotiation, terminal ids, special update mapping, text fallback tests, and exit-pill parsing in `dsh-tool-bash`. The fallback path already exists: render the tool call and completed output as normal ACP content blocks. Non-Zed clients rely on that path anyway, but the Zed terminal card is a current target-client feature rather than speculative decoration. @@ -20,10 +20,10 @@ This proposal is narrower than [collapsing tool-owned UI presentation](2026-06-2 - `TerminalRendering`, terminal ids, terminal cwd resolution, and `_meta.terminal_*` update mapping disappear from `@deepseek-ai/dsh-acp`. - `ToolTerminal` disappears from `@deepseek-ai/dsh-tools`, or is unused and deleted with the presentation cleanup. - Bash result presentation no longer parses exit status for terminal pills. -- The implemented [rich ACP bash rendering RFC](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) stays in `implemented/` as shipped history and is cross-linked from this proposal if superseded. +- The implemented [rich ACP bash rendering Agent Note](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) stays in `implemented/` as shipped history and is cross-linked from this proposal if superseded. ## What we give up Zed users lose the dedicated terminal card: no cwd header, terminal display, or exit pill. They still see the command and output as plain content. That is a reasonable simplification while the ACP bridge is still unreleased and the `_meta` keys are a convention rather than a standard. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md b/.agents/notes/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md similarity index 88% rename from docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md rename to .agents/notes/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md index 859bd6f23d..939255f91e 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md +++ b/.agents/notes/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md @@ -1,4 +1,4 @@ -# RFC: Drop bash full-output spill files +# Agent Note: Drop bash full-output spill files Status: rejected — full-output recovery is a real bash behavior. A future artifact/blob service may generalize it, but dropping spill files before that replacement would lose useful command output. @@ -20,10 +20,10 @@ This proposal can land independently of [a generic long-running tool runtime](.. - `OutputCollector` keeps bounded buffers only and deletes the temp-file machinery. - `renderResult()` reports truncation without a filesystem path. - Tests cover tail truncation and no longer assert full-output file contents. -- Security guidance in [docs/defensive-patterns.md](../../../defensive-patterns.md) stops treating private spill files as a model-visible interface. +- Security guidance in [docs/defensive-patterns.md](../../../../docs/defensive-patterns.md) stops treating private spill files as a model-visible interface. ## What we give up A model or user cannot recover the omitted prefix of a huge command output from a temp file. That is acceptable until there is a real artifact service. The current spill path is too much bespoke machinery for a feature whose lifecycle and permissions are not designed. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md b/.agents/notes/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md similarity index 90% rename from docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md rename to .agents/notes/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md index 2aad598f6d..b2bf42a348 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md +++ b/.agents/notes/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md @@ -1,4 +1,4 @@ -# RFC: Drop durable step boundary events +# Agent Note: Drop durable step boundary events Status: rejected — `step/end` is the durable indication that a model step finished, and keeping the symmetric `step/start` / `step/end` pair makes crash repair, invariants, and transcript inspection clearer than inferring completion from adjacent step-scoped events. @@ -20,11 +20,11 @@ The invariants plugin should enforce that step-scoped events have valid positive - The loop has no `closeStep()` finalization path. - ACP snapshots and persistence contract fixtures stop expecting step-boundary lines. - `deriveMessages()` and replay derive the same message history from step-scoped events. -- The [event taxonomy docs](../../../architecture.md) describe turns as the durable boundary and steps as a field on step-scoped records. +- The [event taxonomy docs](../../../../docs/architecture.md) describe turns as the durable boundary and steps as a field on step-scoped records. - The session format version and recorded fixtures are refreshed; non-current stored logs are rejected per the pre-release format policy. ## What we give up The log no longer records "a model request started but produced no event before the process died" as a durable fact, and no longer has an explicit "this step completed" marker. That loss is not acceptable while the session log is the durable replay and audit surface. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.md b/.agents/notes/rejected/simplification/2026-06-20-drop-unused-session-lineage.md similarity index 94% rename from docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.md rename to .agents/notes/rejected/simplification/2026-06-20-drop-unused-session-lineage.md index fc06cc76c9..c85e943476 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.md +++ b/.agents/notes/rejected/simplification/2026-06-20-drop-unused-session-lineage.md @@ -1,4 +1,4 @@ -# RFC: Drop unused session lineage metadata +# Agent Note: Drop unused session lineage metadata Status: rejected — `parentSession` is part of the documented fork/sub-agent seam and is already preserved by the agent/session resume path. The field is future-facing, but it is not accidental dead state. @@ -26,4 +26,4 @@ If lineage returns, decide then whether it belongs in the immutable header, a se The codebase loses a ready-made lineage hook for future fork/sub-agent UX. That is intentional. The field is easy to reintroduce when the feature exists, and the unreleased stance lets the format change without migrations. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.md b/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md similarity index 94% rename from docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.md rename to .agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md index a59c992b0d..8e5d59172f 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.md +++ b/.agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md @@ -1,4 +1,4 @@ -# RFC: Fold the persistence interface into dsh-session +# Agent Note: Fold the persistence interface into dsh-session Status: rejected — the separate persistence interface package is the intended modular capability seam for durable backends. Folding it into `dsh-session` would reduce package count at the cost of a cleaner backend boundary. @@ -26,4 +26,4 @@ The implementing PR should update the [capability seams](../../implemented/archi `dsh-session` becomes heavier: it owns both the in-memory log and the persistence interface. That is the trade. If third-party persistence backends were already a public ecosystem, the separate interface package would be a cleaner SDK boundary; pre-release, the extra package looks like abstraction before there is an external consumer. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.md b/.agents/notes/rejected/simplification/2026-06-20-generic-tool-rendering.md similarity index 93% rename from docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.md rename to .agents/notes/rejected/simplification/2026-06-20-generic-tool-rendering.md index 87d739ba54..dbc6ffed44 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.md +++ b/.agents/notes/rejected/simplification/2026-06-20-generic-tool-rendering.md @@ -1,4 +1,4 @@ -# RFC: Collapse tool-owned UI presentation +# Agent Note: Collapse tool-owned UI presentation Status: rejected — tool-owned presentation should wait for more real tools before being generalized or deleted. Bash and ACP currently need the existing richer presentation path. @@ -30,4 +30,4 @@ Bash loses its custom terminal-looking card and model-written description placem ## Related -This is the broad version of [dropping ACP terminal metadata](2026-06-20-drop-acp-terminal-meta.md). If this RFC is accepted, that narrower RFC becomes unnecessary. +This is the broad version of [dropping ACP terminal metadata](2026-06-20-drop-acp-terminal-meta.md). If this Agent Note is accepted, that narrower Agent Note becomes unnecessary. diff --git a/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.md b/.agents/notes/rejected/simplification/2026-06-20-retire-mid-turn-steering.md similarity index 96% rename from docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.md rename to .agents/notes/rejected/simplification/2026-06-20-retire-mid-turn-steering.md index fd1a4687b3..b26243f197 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.md +++ b/.agents/notes/rejected/simplification/2026-06-20-retire-mid-turn-steering.md @@ -1,4 +1,4 @@ -# RFC: Retire mid-turn steering +# Agent Note: Retire mid-turn steering Status: rejected — mid-turn steering is an intentional agent capability for between-step user/plugin input and future goal/loop workflows. It is complexity with a product direction, not an accidental duplicate of `send()`. @@ -32,4 +32,4 @@ A user cannot add same-turn steering content while a model is between tool steps This pairs naturally with [dropping durable step boundaries](2026-06-20-drop-durable-step-boundaries.md), because removing same-turn steering and `agent/turn-continuation` leaves tool calls as the only reason a turn contains multiple model steps. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.md b/.agents/notes/rejected/simplification/2026-06-20-single-session-acp-bridge.md similarity index 88% rename from docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.md rename to .agents/notes/rejected/simplification/2026-06-20-single-session-acp-bridge.md index 8f1d4e4531..83c6f598d4 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.md +++ b/.agents/notes/rejected/simplification/2026-06-20-single-session-acp-bridge.md @@ -1,10 +1,10 @@ -# RFC: Return the ACP bridge to one live session per connection +# Agent Note: Return the ACP bridge to one live session per connection Status: rejected — Zed is the current target ACP client and its ACP implementation is explicitly multi-session: it stores live sessions in a `HashMap<SessionId, AcpSession>`, tracks `pending_sessions`, joins concurrent loads for the same id, and tests close-during-load behavior. ## Problem -The ACP bridge now supports multiple live sessions on one JSON-RPC connection. That capability brings multi-entry session maps, reverse session/agent lookups, per-session prompt state, loading ids, demux for every event, cross-session teardown, and isolation concerns for future permission prompts and background tasks. The older [multi-session ACP proposal](../../implemented/feature/2026-06-14-acp-multi-session.md) still tracks the unfinished permission-ownership piece; this RFC is the competing simplification path. +The ACP bridge now supports multiple live sessions on one JSON-RPC connection. That capability brings multi-entry session maps, reverse session/agent lookups, per-session prompt state, loading ids, demux for every event, cross-session teardown, and isolation concerns for future permission prompts and background tasks. The older [multi-session ACP proposal](../../implemented/feature/2026-06-14-acp-multi-session.md) still tracks the unfinished permission-ownership piece; this Agent Note is the competing simplification path. The product target has proven it needs concurrent editor conversations over one harness process: Zed's ACP connection owns multiple sessions and load states. The snapshot replay tier still avoids concurrent model streams because its replay entries are positional; that is a test-fixture limitation, not a reason to remove bridge multiplexing. @@ -20,10 +20,10 @@ Remove the multi-session maps and demux where a single `SessionRecord | undefine - `session/new` and `session/load` reject while that record exists. - Event handlers no longer demux across a `Map<sessionId, record>`. - Multi-session tests are removed or moved under the proposal that continues to defend multiplexing. -- The existing [multi-session ACP proposal](../../implemented/feature/2026-06-14-acp-multi-session.md) is updated to link this RFC and remains the live direction. +- The existing [multi-session ACP proposal](../../implemented/feature/2026-06-14-acp-multi-session.md) is updated to link this Agent Note and remains the live direction. ## What we give up An ACP client cannot host several concurrent conversations on one server process. That is a meaningful capability cut. The simpler model is still reasonable for an unreleased harness: one editor conversation maps to one agent process, and cross-session permission/background-task isolation stops being a live correctness burden. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.md b/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md similarity index 96% rename from docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.md rename to .agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md index 1ed26c66f8..a685765197 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.md +++ b/.agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md @@ -1,4 +1,4 @@ -# RFC: Truncate interrupted final turns on load +# Agent Note: Truncate interrupted final turns on load Status: rejected — a single turn can contain substantial real work, including many steps and large tool output. Preserving interrupted turns is preferable to silently dropping that tail on load. @@ -31,4 +31,4 @@ A crash can lose real work from the final turn: assistant text, tool calls, and This is a direct simplification of [session persistence](../../implemented/architecture/2026-06-14-session-persistence.md) and [turn enclosure](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md). It also removes much of the motivation for durable step boundary events, making [drop durable step boundary events](2026-06-20-drop-durable-step-boundaries.md) smaller. -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> +<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) --> diff --git a/docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md b/.agents/notes/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md similarity index 65% rename from docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md rename to .agents/notes/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md index 266a1d911d..582673f185 100644 --- a/docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md +++ b/.agents/notes/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md @@ -1,4 +1,4 @@ -# RFC: Prune the unimplemented subagent seam vocabulary +# Agent Note: Prune the unimplemented subagent seam vocabulary Status: rejected — the deferred capability vocabulary (`outputSchema`/`structured`, `toolFilter`, `sendMessage`/`resume`) is intentionally reserved surface: the seam advertises the full intended contract ahead of its implementations by design, so providers and consumers grow into a stable shape rather than re-negotiating it per capability. The consumer-evidence analysis below records the decision-time state. @@ -13,11 +13,11 @@ The only reason `dsh-subagent` depends on `dsh-tools` at all is `outputSchema`'s ## Proposal -Remove `outputSchema`/`structured`, `toolFilter`, `sendMessage`, and `resume` from the seam; shrink `SubagentCapabilities` to `{ depthLimit }`; drop the two capability-assert rows, the all-false flags on the three providers, the scripted fixture's structured branch and capability knobs, and the tests that exist to pin the removed surface. Drop the `dsh-tools` peer/dev dependency from `packages/subagent/subagent/package.json`. Update the [subagent.md](../../../core-data-structures/subagent.md) pastes and the type-equiv manifest, plus the affected provider READMEs. The implementing PR amends the seam RFC's capability catalog per [implemented/AGENTS.md](../../implemented/AGENTS.md). +Remove `outputSchema`/`structured`, `toolFilter`, `sendMessage`, and `resume` from the seam; shrink `SubagentCapabilities` to `{ depthLimit }`; drop the two capability-assert rows, the all-false flags on the three providers, the scripted fixture's structured branch and capability knobs, and the tests that exist to pin the removed surface. Drop the `dsh-tools` peer/dev dependency from `packages/subagent/subagent/package.json`. Update the [subagent.md](../../../../docs/core-data-structures/subagent.md) pastes and the type-equiv manifest, plus the affected provider READMEs. The implementing PR amends the seam Agent Note's capability catalog per [implemented/AGENTS.md](../../implemented/AGENTS.md). **Keep** `depthLimit`/`maxDepth` and capability checks. The in-process backend enforces the limit, although the shipping tool does not yet set it. Recursion is a known seam risk, so the appropriate follow-up is to supply a tool default rather than delete working enforcement. -Adjacent surface examined and deliberately left alone: `SubagentService.getProvider()`/`list()` have test-harness consumers only, but the [prune-dead-seam-methods implementation note](../../implemented/simplification/2026-06-20-prune-dead-seam-methods.md) records precisely this shape being removed from the bash executor and reverted — a test harness IS a consumer for a one-line accessor over an already-tracked map. `SubagentRunEndInfo.lastAssistantMessage` is a recorded keep (the [subagent-observe-enrich RFC](../../implemented/feature/2026-06-30-subagent-observe-enrich.md)'s review dropped `agentType` and kept it deliberately, as the only final-message channel for out-of-process children); its currently-unwired bridge forwarding is a gap to close or a consumer to document, not surface for this RFC to cut. +Adjacent surface examined and deliberately left alone: `SubagentService.getProvider()`/`list()` have test-harness consumers only, but the [prune-dead-seam-methods implementation note](../../implemented/simplification/2026-06-20-prune-dead-seam-methods.md) records precisely this shape being removed from the bash executor and reverted — a test harness IS a consumer for a one-line accessor over an already-tracked map. `SubagentRunEndInfo.lastAssistantMessage` is a recorded keep (the [subagent-observe-enrich Agent Note](../../implemented/feature/2026-06-30-subagent-observe-enrich.md)'s review dropped `agentType` and kept it deliberately, as the only final-message channel for out-of-process children); its currently-unwired bridge forwarding is a gap to close or a consumer to document, not surface for this Agent Note to cut. This is the seam-vocabulary echo of [prune dead methods from the persistence seam](../../implemented/simplification/2026-06-20-prune-dead-seam-methods.md): members every implementation must declare for nobody — weaker even, since here zero implementations exist. @@ -25,13 +25,13 @@ This is the seam-vocabulary echo of [prune dead methods from the persistence sea ### Why not keep it? -The two-kinds-of-capability design is the seam RFC's headline, and re-adding `outputSchema` later touches several files. But the design survives with `depthLimit` as its live example and the RFCs as its record, and the seam RFC itself concedes the shipped `toolFilter` shape is wrong (real enforcement needs a `tools/pre-execute` deny in the child's context, not schema filtering) — that deny primitive exists on the interception seams, so re-adding against a real implementing provider will pin a better contract than the current speculative one. +The two-kinds-of-capability design is the seam Agent Note's headline, and re-adding `outputSchema` later touches several files. But the design survives with `depthLimit` as its live example and the Agent Notes as its record, and the seam Agent Note itself concedes the shipped `toolFilter` shape is wrong (real enforcement needs a `tools/pre-execute` deny in the child's context, not schema filtering) — that deny primitive exists on the interception seams, so re-adding against a real implementing provider will pin a better contract than the current speculative one. ## Acceptance criteria -- The removed spellings appear only in this RFC and the amended seam RFCs; `SubagentCapabilities` is `{ depthLimit: boolean }`; the `dsh-tools` dependency edge is gone (`hygiene` green). +- The removed spellings appear only in this Agent Note and the amended seam Agent Notes; `SubagentCapabilities` is `{ depthLimit: boolean }`; the `dsh-tools` dependency edge is gone (`hygiene` green). - Depth-enforcement tests are unchanged and green. ## Risks -The subagent lifecycle events carry `lastAssistantMessage` on the end payload — that enrichment lives in the service module, not the seam vocabulary this RFC shrinks, and the observe-enrich RFC records dropping an `agentType` sibling for lacking a consumer: the judgment this RFC extends. The CC hooks bridge, the first outside consumer of those lifecycle events, reads only the event payloads and touches none of the surface removed here; the observe-enrich RFC's deferred control-flow redesign names implementing `resume` as its own future work — exactly the re-add trigger this RFC's pattern anticipates. +The subagent lifecycle events carry `lastAssistantMessage` on the end payload — that enrichment lives in the service module, not the seam vocabulary this Agent Note shrinks, and the observe-enrich Agent Note records dropping an `agentType` sibling for lacking a consumer: the judgment this Agent Note extends. The CC hooks bridge, the first outside consumer of those lifecycle events, reads only the event payloads and touches none of the surface removed here; the observe-enrich Agent Note's deferred control-flow redesign names implementing `resume` as its own future work — exactly the re-add trigger this Agent Note's pattern anticipates. diff --git a/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md b/.agents/notes/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md similarity index 91% rename from docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md rename to .agents/notes/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md index 7624629d41..dcfdfa13e6 100644 --- a/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md +++ b/.agents/notes/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md @@ -1,4 +1,4 @@ -# RFC: Collapse workflows to the exercised foreground core +# Agent Note: Collapse workflows to the exercised foreground core Status: rejected — Workflow progress is an intentional observation surface; make it useful through a consumer instead of deleting it. @@ -18,7 +18,7 @@ Cancellation also has two public channels for one synchronous start. `WorkflowSt Keep the exercised core: `agent(prompt, { schema, model })`, `parallel`, `pipeline`, `args`, concurrency/agent caps, cancellation, bounded disposal, structured results, worker isolation, and foreground tool collection. Remove all `workflow/*` events and their event-only info/outcome types; remove `phase()`, `log()`, agent `label`/`phase`, phase declarations, `whenToUse`, and their worker messages/host observers; collapse workflow metadata to the name the tool actually uses; remove event-only run ids/meta snapshots and the synthesized agent-end ledger. Shrink `WorkflowRun` to `result`, `cancel()`, and `dispose()`; the tool renders the request-owned name. Remove `WorkflowStartRequest.signal` and the worker host's input-signal listener/disarm state, retaining the caller-owned bridge from its abort signal to `run.cancel()`. Make `WorkflowError` one fatal error class without a boolean mode or `isFatalWorkflowError()` helper. -Amend the implemented dynamic-workflow RFC and update the seam/tool/worker READMEs, tool schema, generated catalogs and package graph, worker type-equivalence records, unit tests, and workflow snapshot/header fixtures. Progress UI work, if commissioned, starts from a correlation contract that names the parent agent/session/tool call instead of reviving this protocol unchanged. +Amend the implemented dynamic-workflow Agent Note and update the seam/tool/worker READMEs, tool schema, generated catalogs and package graph, worker type-equivalence records, unit tests, and workflow snapshot/header fixtures. Progress UI work, if commissioned, starts from a correlation contract that names the parent agent/session/tool call instead of reviving this protocol unchanged. ## Alternatives considered diff --git a/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md b/.agents/notes/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md similarity index 76% rename from docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md rename to .agents/notes/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md index cafd50e8a5..e96215a651 100644 --- a/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md +++ b/.agents/notes/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md @@ -1,4 +1,4 @@ -# RFC: Prune unused skill registry surface +# Agent Note: Prune unused skill registry surface Status: rejected — Direct runtime skill registration is an intentional extension path for third-party plugins. @@ -10,11 +10,11 @@ The skill service's embedded-runtime subsystem has zero production caller of `ct Remove `SkillService.register()`, `SkillRegistration`, the runtime pseudo-provider and reserved-name rules, runtime revisions/cache branches, and runtime-only source/rank normalization. Tests that need an embedded skill register a small real provider. Retain `providerRevision` as the in-flight discovery epoch, but key completed catalogs by cwd alone: every provider mutation synchronously clears the cache, and the post-await revision comparison already prevents inserting stale work. Remove `whenToUse`, `SkillCandidate.path`, and `SkillDefinition.path` from the skill contract and local-provider copies while retaining provider locator/root paths; retain `metadata`, `disableModelInvocation`, `source`, `provider`, `locator`, and `resourceBase` as either deliberate extension vocabulary or production-consumed fields. -Amend the skill-system RFC, README, JSDoc, catalogs, and tests. Agent-scoped system-prompt sections, tool providers, and variables are explicitly outside this proposal: the [agent-scope contributor contract](../../implemented/architecture/2026-07-08-agent-scope-contexts.md) intentionally allows all three to be registered during `setup(agentCtx)` through the agent-owned context, so absence of a fixed in-repo scoped registration is not evidence of non-consumption. +Amend the skill-system Agent Note, README, JSDoc, catalogs, and tests. Agent-scoped system-prompt sections, tool providers, and variables are explicitly outside this proposal: the [agent-scope contributor contract](../../implemented/architecture/2026-07-08-agent-scope-contexts.md) intentionally allows all three to be registered during `setup(agentCtx)` through the agent-owned context, so absence of a fixed in-repo scoped registration is not evidence of non-consumption. ## Alternatives considered -**Keep runtime skill registration for embedders.** It is a deliberate synchronous direct-definition convenience in the implemented skill RFC. A small provider wrapper can expose the same embedded data under effect-owned lifetime, but it must implement async `list()`/`get()`, carry provider identity, and accept provider duplicate semantics. The proposal chooses that one regular path over preserving a second ranking, validation, cache-invalidation, and lookup path. +**Keep runtime skill registration for embedders.** It is a deliberate synchronous direct-definition convenience in the implemented skill Agent Note. A small provider wrapper can expose the same embedded data under effect-owned lifetime, but it must implement async `list()`/`get()`, carry provider identity, and accept provider duplicate semantics. The proposal chooses that one regular path over preserving a second ranking, validation, cache-invalidation, and lookup path. ## Acceptance criteria diff --git a/docs/rfc/rejected/simplification/2026-07-19-fold-compaction-package-split.i18n.yaml b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.i18n.yaml similarity index 62% rename from docs/rfc/rejected/simplification/2026-07-19-fold-compaction-package-split.i18n.yaml rename to .agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.i18n.yaml index d2a15cc2c2..98acc791c3 100644 --- a/docs/rfc/rejected/simplification/2026-07-19-fold-compaction-package-split.i18n.yaml +++ b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-fold-compaction-package-split.md: 7c7a2da85beb956f8d6c24f813fc33aa350b5c0e -2026-07-19-fold-compaction-package-split.zh.md: 37d75671d57226742608a525ea711b34780e65c6 +2026-07-19-fold-compaction-package-split.md: 47c9feb6bb0dd06fec0f002b7c1e930b288abe5e +2026-07-19-fold-compaction-package-split.zh.md: 53717ff10d1210bd2072f322d1936ac6c389afcd diff --git a/docs/rfc/rejected/simplification/2026-07-19-fold-compaction-package-split.md b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.md similarity index 97% rename from docs/rfc/rejected/simplification/2026-07-19-fold-compaction-package-split.md rename to .agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.md index 7c7a2da85b..47c9feb6bb 100644 --- a/docs/rfc/rejected/simplification/2026-07-19-fold-compaction-package-split.md +++ b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.md @@ -1,4 +1,4 @@ -# RFC: Fold the single compaction backend into its service package +# Agent Note: Fold the single compaction backend into its service package Status: rejected — More compaction backends are planned, so the interface and basic implementation packages remain separate. diff --git a/docs/rfc/rejected/simplification/2026-07-19-fold-compaction-package-split.zh.md b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.zh.md similarity index 98% rename from docs/rfc/rejected/simplification/2026-07-19-fold-compaction-package-split.zh.md rename to .agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.zh.md index 37d75671d5..53717ff10d 100644 --- a/docs/rfc/rejected/simplification/2026-07-19-fold-compaction-package-split.zh.md +++ b/.agents/notes/rejected/simplification/2026-07-19-fold-compaction-package-split.zh.md @@ -1,4 +1,4 @@ -# RFC: 将唯一的压缩后端并入服务包 +# Agent Note: 将唯一的压缩后端并入服务包 Status: rejected — 计划增加更多压缩后端,因此接口包与 basic 实现包继续分离。 diff --git a/.agents/skills/dsh-code-review/SKILL.md b/.agents/skills/dsh-code-review/SKILL.md index e1493d04f2..814132d481 100644 --- a/.agents/skills/dsh-code-review/SKILL.md +++ b/.agents/skills/dsh-code-review/SKILL.md @@ -13,8 +13,8 @@ description: Use when reviewing a pull request in the deepseek-harness repo — - [docs/defensive-patterns.md](../../../docs/defensive-patterns.md): subprocess, callback, async-state, and disposal bug classes. - [docs/AGENTS.md](../../../docs/AGENTS.md): documentation placement and prose discipline. - [dsh-prose-standard](../dsh-prose-standard/SKILL.md): required coverage and editorial judgment for comments, docs, prompts, and visible strings. -- [docs/testing.md](../../../docs/testing.md) and the [quality-gates RFC](../../../docs/rfc/implemented/process/2026-06-11-quality-gates.md): required test tiers and gates. -- [RFC index](../../../docs/rfc/README.md): design rationale. Treat disagreement with an RFC as a design discussion, not an automatic veto. +- [docs/testing.md](../../../docs/testing.md) and the [quality-gates Agent Note](../../notes/implemented/process/2026-06-11-quality-gates.md): required test tiers and gates. +- [Agent Notes](../../notes/README.md): design rationale. Treat disagreement with an Agent Note as a design discussion, not an automatic veto. - For bilingual changes, read [translation-rules.md](../../../docs/i18n/translation-rules.md), [terminology.md](../../../docs/i18n/terminology.md), and [dsh-translate-docs](../dsh-translate-docs/SKILL.md). ## Blocking requirements @@ -27,7 +27,7 @@ description: Use when reviewing a pull request in the deepseek-harness repo — ## Manual checks -- **Intent and seam contracts:** trace both sides of every changed interface. Confirm the implementation matches the PR and any RFC, including errors, cancellation, ownership, and disposal. +- **Intent and seam contracts:** trace both sides of every changed interface. Confirm the implementation matches the PR and any Agent Note, including errors, cancellation, ownership, and disposal. - **Lifecycle and concurrency:** for async setup, callbacks, processes, or teardown, apply [defensive-patterns.md](../../../docs/defensive-patterns.md). Check races before publication, cancellation during awaits, independent error reporting, callback containment, ownership before reentry, complete detach cleanup, and quiescent disposal. - **Capability and consumer fit:** trace every current consumer, then flag consumer-specific behavior leaking into the interface under [the package contract](../../../packages/AGENTS.md). - **Scope, ownership, and necessity:** map each abstraction, state machine, option, defensive copy, and compatibility path to its current contract, production consumer, and owning plugin or service. Challenge unrelated features and speculative generality, then test the PR's coherence against [the root contract](../../../AGENTS.md#conventions). @@ -39,7 +39,7 @@ description: Use when reviewing a pull request in the deepseek-harness repo — - **Real entry path:** tests exercise the shipped Loader, bin, worker, ACP bridge, or subprocess where relevant. A hand-mounted plugin does not catch Loader export-shape failures; a function plugin must named-export its namespace and have no default export. - **Test strength:** assertions fail on the intended regression and verify external state, logs, events, or disposal rather than restating the implementation or trusting an agent's report. Coverage is necessary but not evidence that the scenario is correct. - **Mechanized invariants and negative controls:** trace each new or changed check through the executed top-level gate and its deliberately invalid case; confirm the real runner fails for the intended rule. -- **Implemented RFCs match shipped reality:** when a PR implements a proposed RFC, move and rewrite it as present-tense shipped state in the same diff, then verify paths, names, and mechanisms against the implementation. +- **Implemented Agent Notes match shipped reality:** when a PR implements a proposed Agent Note, move and rewrite it as present-tense shipped state in the same diff, then verify paths, names, and mechanisms against the implementation. - **Transcript changes:** editor-visible or model-visible changes update snapshots or explain why no snapshot applies. Review expected-output diffs as behavior changes, not formatting noise. - **Bilingual changes:** compare meaning and terminology on both sides; a green pairing hash does not prove translation quality. 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-doc-standards/SKILL.md b/.agents/skills/dsh-doc-standards/SKILL.md index 6ad8902d66..2a5458db7f 100644 --- a/.agents/skills/dsh-doc-standards/SKILL.md +++ b/.agents/skills/dsh-doc-standards/SKILL.md @@ -10,7 +10,7 @@ The contract lives in [docs/AGENTS.md](../../../docs/AGENTS.md). This workflow c ## Sources of truth (read, don't re-summarize) - [docs/AGENTS.md](../../../docs/AGENTS.md) — the taxonomy ("one home per fact"), budgets, slop checklist. -- [docs/rfc/README.md](../../../docs/rfc/README.md) — when a decision earns an RFC, how to file it, and what goes inside one (the header block, per-lifecycle skeleton, and Alternatives-considered mandate, gated by `verify-rfc-format`); [docs/postmortem/README.md](../../../docs/postmortem/README.md) — when an incident earns a postmortem. +- [.agents/notes/README.md](../../notes/README.md) — when a decision earns an Agent Note, how to file it, and what goes inside one (the header block, per-lifecycle skeleton, and Alternatives-considered mandate, gated by `verify-agent-note-format`); [docs/postmortem/README.md](../../../docs/postmortem/README.md) — when an incident earns a postmortem. - [docs/i18n/README.md](../../../docs/i18n/README.md) — the bilingual pairing contract; editing either side of a pair obligates the counterpart in the same change. - Root [AGENTS.md](../../../AGENTS.md) — the standing orders whose budget discipline this skill protects. @@ -32,8 +32,8 @@ The audit is a hunt for the standard's slop checklist, cheapest probes first. Es 3. Inspect long comments for reasoning transcripts: control-flow narration, test walkthroughs, proof of obvious branches, review findings, rejected local alternatives, and the same rationale repeated beside sibling methods. Preserve only a non-obvious contract or durable rationale; otherwise delete the comment. 4. Hunt duplication by grepping distinctive phrases. Keep one home and replace other copies with links. 5. Replace hand-written catalogs, test/status inventories, and JSDoc restatements with the authoritative tree, script, or generated reference. -6. In `implemented/` RFCs, remove migration plans, acceptance-task checklists, and future-tense spec language. Keep concise verification contracts that identify the behaviors and tiers pinning the shipped decision, plus named coverage gaps. -7. If removing prose changes a promised behavior rather than its explanation, use a proposed RFC first (follow [dsh-find-simplifications](../dsh-find-simplifications/SKILL.md)). +6. In `implemented/` Agent Notes, remove migration plans, acceptance-task checklists, and future-tense spec language. Keep concise verification contracts that identify the behaviors and tiers pinning the shipped decision, plus named coverage gaps. +7. If removing prose changes a promised behavior rather than its explanation, use a proposed Agent Note first (follow [dsh-find-simplifications](../dsh-find-simplifications/SKILL.md)). Keep every load-bearing rule, preferably as one to three lines plus a link to its rationale. Cut stories, duplicates, status notes, and the path used to derive the rule. Do not create a new explanation merely to relocate disposable reasoning. diff --git a/.agents/skills/dsh-find-simplifications/SKILL.md b/.agents/skills/dsh-find-simplifications/SKILL.md index 623cdf18e2..2ce80f3f74 100644 --- a/.agents/skills/dsh-find-simplifications/SKILL.md +++ b/.agents/skills/dsh-find-simplifications/SKILL.md @@ -1,17 +1,17 @@ --- name: dsh-find-simplifications -description: 'Use when working in the deepseek-harness repo to find non-obvious simplification candidates and write proposed RFCs or inline TODO/FIXME/XXX notes for dead, duplicated, speculative, or over-built code surfaces; especially for requests like "find simplification RFCs", "look for unnecessary complexity", "audit for removal-style cleanups", or "fold worthwhile simplification ideas from another PR".' +description: 'Use when working in the deepseek-harness repo to find non-obvious simplification candidates and write proposed Agent Notes or inline TODO/FIXME/XXX notes for dead, duplicated, speculative, or over-built code surfaces; especially for requests like "find simplification Agent Notes", "look for unnecessary complexity", "audit for removal-style cleanups", or "fold worthwhile simplification ideas from another PR".' --- # Finding DeepSeek Harness Simplifications -This skill helps turn a broad "find things to simplify" request into evidence-backed RFCs that remove or collapse existing harness surface area. It is guidance, not a checklist: follow the code, keep judgment active, and prefer a few well-proven candidates over a pile of thin guesses. +This skill helps turn a broad "find things to simplify" request into evidence-backed Agent Notes that remove or collapse existing harness surface area. It is guidance, not a checklist: follow the code, keep judgment active, and prefer a few well-proven candidates over a pile of thin guesses. ## Start With Repo Context -- Read `AGENTS.md`, especially the pre-release stance and the conventions (including the tests-are-not-golden-truth and RFCs-are-not-golden-truth doctrines), plus [docs/defensive-patterns.md](../../../docs/defensive-patterns.md) and [docs/testing.md](../../../docs/testing.md). +- Read `AGENTS.md`, especially the pre-release stance and the conventions (including the tests-are-not-golden-truth and Agent Notes-are-not-golden-truth doctrines), plus [docs/defensive-patterns.md](../../../docs/defensive-patterns.md) and [docs/testing.md](../../../docs/testing.md). - Skim [docs/architecture.md](../../../docs/architecture.md) before judging anything under `packages/`; simplifications that fight the service map or event taxonomy need extra evidence. -- Use the RFC index ([docs/rfc/README.md](../../../docs/rfc/README.md)) to understand intentional architecture. The most relevant implemented examples are [drop mutable session summary](../../../docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.md), [shared persistence write coordinator](../../../docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md), [capability seams](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md), and the twin adapter / dual persistence backend RFCs. +- Use the Agent Note tree and its [contract](../../notes/README.md) to understand intentional architecture. The most relevant implemented examples are [drop mutable session summary](../../notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md), [shared persistence write coordinator](../../notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md), [capability seams](../../notes/implemented/architecture/2026-06-13-capability-seams.md), and the twin adapter / dual persistence backend Agent Notes. - Treat dual LLM adapters and dual persistence backends as intentional by default. Do not propose deleting either twin/backend as "low effort" unless the user explicitly overrides that constraint. Removing an unused method or hook inside a protected seam can still be valid if it does not collapse the protected design. ## What Counts As A Strong Candidate @@ -27,7 +27,7 @@ A strong simplification removes, folds, or demotes something real and has clear - An invariant, rollback path, set of expected outputs, or special-case test exists only to protect an unused surface. - The simplified behavior may differ slightly, but the new behavior is still reasonable and easier to explain. -Thin candidates are usually not enough for an RFC: deleting one typo, running `knip` once, removing an intentionally documented backend/adapter, or flagging "this looks complex" without call-site proof. +Thin candidates are usually not enough for an Agent Note: deleting one typo, running `knip` once, removing an intentionally documented backend/adapter, or flagging "this looks complex" without call-site proof. ## Survey Broadly @@ -54,7 +54,7 @@ For complex asynchronous code, draw the ownership graph and map each sentinel, r For every symbol or behavior, classify consumers before writing: - Production corpus: `packages/*/src`, `examples/*/src`, `examples/**/*.yml`, runtime scripts, and loader/config paths. -- Non-production corpus: tests, README/docs, RFCs, snapshots, generated expected outputs, and comments. +- Non-production corpus: tests, README/docs, Agent Notes, snapshots, generated expected outputs, and comments. - Ambiguous corpus: examples and scripts that may be product smoke paths. Inspect usage before classifying. Use `rg` first. Good searches include the exact symbol, event name, package name, config key, method name with both `.name(` and `name(`, and any wire strings. Then read the call sites. `knip` can help, but it is not a substitute for understanding public interfaces, dynamic event names, tests, docs, and Cordis loader paths. @@ -62,17 +62,17 @@ Use `rg` first. Good searches include the exact symbol, event name, package name Reject or downgrade a candidate when: - A production caller exists and the simplification would be a feature decision rather than a cleanup. -- The surface is explicitly justified by an implemented RFC or a hard-won defensive pattern, and the new evidence does not beat that reason. +- The surface is explicitly justified by an implemented Agent Note or a hard-won defensive pattern, and the new evidence does not beat that reason. - The removal would force unrelated churn without actually making the contract smaller. - The idea is correct but tiny. Add a targeted TODO/FIXME/XXX instead, using the urgency semantics in [docs/development.md](../../../docs/development.md). -## Write The RFC +## Write The Agent Note -Create one file per durable proposal under `docs/rfc/<lifecycle>/<class>/yyyy-mm-dd-topic.md`, following the lifecycle/classification contract in `docs/rfc/README.md`. Regenerate `docs/rfc/INDEX.md`; never add a manual RFC table to the README. Keep prose paragraphs on one physical line and use relative Markdown links. +Create one file per durable proposal under `.agents/notes/<lifecycle>/<class>/yyyy-mm-dd-topic.md`, following the lifecycle/classification contract in `.agents/notes/README.md`. Keep prose paragraphs on one physical line and use relative Markdown links. Prefer this shape, adjusting when the idea needs it: -- `# RFC: <action-oriented title>` +- `# Agent Note: <action-oriented title>` - `Status: proposed` - `## Problem`: name the current surface, cite the relevant files, and state the consumer evidence. Separate production callers from tests/docs. - `## Proposal`: say exactly what to remove, fold, demote, or rehome. Include tests, docs, READMEs, JSDoc, event-taxonomy, snapshot, and generated-file cleanup when relevant. @@ -80,7 +80,7 @@ Prefer this shape, adjusting when the idea needs it: - `## Acceptance criteria`: observable end state and gates. - `## Risks`: public API changes, behavior changes, future product wants, and why the tradeoff is still reasonable. -Be concrete enough that an implementing PR can follow the trail. Avoid vague "simplify this package" RFCs. When a proposal overlaps an existing RFC, consolidate the useful details into the existing one rather than creating a duplicate. +Be concrete enough that an implementing PR can follow the trail. Avoid vague "simplify this package" Agent Notes. When a proposal overlaps an existing Agent Note, consolidate the useful details into the existing one rather than creating a duplicate. ## Inline TODO Notes @@ -88,25 +88,25 @@ Use inline TODO/FIXME/XXX only for small, local cleanups that are clearly useful - Name the smell with a stable tag, e.g. `TODO(double-default)` or `XXX(unused-default)`. - Explain why it is safe to revisit and what action would simplify it. -- Do not add TODOs for speculative complaints or for behavior that needs an RFC-level decision. +- Do not add TODOs for speculative complaints or for behavior that needs an Agent Note-level decision. ## When Folding Another PR Or Branch Diff the sibling branch against `origin/master`, not against the current PR branch, so you see its independent contribution. For each item: -- Port non-overlapping RFCs or TODOs that meet the quality bar. -- Consolidate overlapping material into the existing RFC that owns the topic. +- Port non-overlapping Agent Notes or TODOs that meet the quality bar. +- Consolidate overlapping material into the existing Agent Note that owns the topic. - Do not port duplicate or lower-confidence proposals just to preserve the count. - Update the PR body so reviewers see the true candidate count and scope. - Close the duplicate PR only when the user asked you to, or when you clearly own that housekeeping. ## Validation And PR Hygiene -For docs-only RFC work, run at least `pnpm run doc-sync`, `pnpm run lint`, and `git diff --check`. For code comments or skill changes, also run the relevant validator when one exists. Before pushing, expect the pre-push hook to run module graph freshness, unit tests, snapshots, doc-sync, and hygiene. +For docs-only Agent Note work, run at least `pnpm run doc-sync`, `pnpm run lint`, and `git diff --check`. For code comments or skill changes, also run the relevant validator when one exists. Before pushing, expect the pre-push hook to run module graph freshness, unit tests, snapshots, doc-sync, and hygiene. When opening or updating a PR, summarize: -- How many RFCs and inline notes were added. +- How many Agent Notes and inline notes were added. - The main areas surveyed. - What was intentionally excluded. - Which checks passed. diff --git a/.agents/skills/dsh-pre-push-checks/SKILL.md b/.agents/skills/dsh-pre-push-checks/SKILL.md index 433aa38942..5c51209113 100644 --- a/.agents/skills/dsh-pre-push-checks/SKILL.md +++ b/.agents/skills/dsh-pre-push-checks/SKILL.md @@ -41,7 +41,7 @@ Why `test:coverage`, not only `test`: CI enforces per-file 100% coverage. A bran ## Add Gates By Touched Surface -Run `pnpm run doc-sync` and `pnpm run verify-module-graph` when the diff touches Markdown docs, package manifests, package imports/exports, generated catalogs, RFCs, architecture docs, translation pairs, Mermaid diagrams, or comments that cite docs/packages. +Run `pnpm run doc-sync` and `pnpm run verify-module-graph` when the diff touches Markdown docs, package manifests, package imports/exports, generated catalogs, Agent Notes, architecture docs, translation pairs, Mermaid diagrams, or comments that cite docs/packages. Run `pnpm run build` and `pnpm run hygiene` when the diff touches any package `package.json`, dependency graph, public exports, build config, declaration surface, bundled runtime path, or code that will be consumed from built `lib/`. diff --git a/.agents/skills/dsh-prose-standard/SKILL.md b/.agents/skills/dsh-prose-standard/SKILL.md index b4cc300f14..faf234ae7c 100644 --- a/.agents/skills/dsh-prose-standard/SKILL.md +++ b/.agents/skills/dsh-prose-standard/SKILL.md @@ -45,7 +45,7 @@ This is not a one-way shortening pass. Add or restore prose when code, types, an - **Tests:** explain only non-obvious test design—why a fixture, assertion, platform accommodation, real entry path, or indirect observation is necessary. Delete walkthroughs and inventories. - **Cookbooks:** include prerequisites, required actions, the real entry path, observable verification, and concise warnings. - **READMEs:** include the consumer contract: configuration, semantics, failures, limitations, extension points, and model-visible effects. Quote stable model-visible text owned by the package; link generated catalogs and cross-package owners. Keep durable gaps and maintainer traps, not ordinary cleanup inventories. Follow the [package README contract](../../../docs/cookbook/adding-a-package.md#4-write-the-package-readme). -- **RFCs:** retain unique rationale, mechanisms, alternatives, consequences, shipped verification contracts, and named coverage gaps. Implemented RFCs state shipped reality in the present tense; remove planning checklists, not evidence of what pins the decision. +- **Agent Notes:** retain unique rationale, mechanisms, alternatives, consequences, shipped verification contracts, and named coverage gaps. Implemented Agent Notes state shipped reality in the present tense; remove planning checklists, not evidence of what pins the decision. - **Postmortems:** retain the incident sequence, evidence, causal chain, impact, and prevention. Remove repeated persuasion or implementation detail that does not establish causality. - **Skills and agent instructions:** state behavioral guardrails and explicit scope limitations such as “guidance, not a script/checklist.” Keep the workflow concise and link its source of truth. - **Examples and configuration comments:** explain boundaries, non-obvious wiring or load order, security stance, replay behavior, exceptions, and likely misuse. Do not narrate entries that the configuration already shows. diff --git a/.agents/skills/dsh-prose-standard/references/examples.md b/.agents/skills/dsh-prose-standard/references/examples.md index 7edb01af3d..c4270ecc98 100644 --- a/.agents/skills/dsh-prose-standard/references/examples.md +++ b/.agents/skills/dsh-prose-standard/references/examples.md @@ -56,7 +56,7 @@ Event order and its current-request consequence are caller-visible behavior, not **Over-trimmed:** “Worker realm support.” -**Balanced:** “Owns the worker realm and its host bridge. Realm initialization is single-shot; disposal terminates the worker and rejects later calls. See the worker-isolation RFC for the protocol rationale.” +**Balanced:** “Owns the worker realm and its host bridge. Realm initialization is single-shot; disposal terminates the worker and rejects later calls. See the worker-isolation Agent Note for the protocol rationale.” **Over-detailed:** A paragraph-by-paragraph preview of the classes and helper functions below. @@ -84,17 +84,17 @@ Keep mapping details that explain an abstraction boundary or intentional informa ## Link rationale while keeping the local contract -**Over-trimmed:** “Disposal is documented in the lifecycle RFC.” +**Over-trimmed:** “Disposal is documented in the lifecycle Agent Note.” -**Balanced:** “Disposal aborts the run and waits for provider quiescence. See the lifecycle RFC for ownership and race handling.” +**Balanced:** “Disposal aborts the run and waits for provider quiescence. See the lifecycle Agent Note for ownership and race handling.” -**Over-detailed:** Repeating the RFC's promise choreography and rejected ownership models beside every disposer. +**Over-detailed:** Repeating the Agent Note's promise choreography and rejected ownership models beside every disposer. Keep the behavior and completion guarantee where callers need them. Link aggressively for the algorithm and rationale; a link cannot replace the local contract. -## Implemented RFCs retain verification contracts +## Implemented Agent Notes retain verification contracts -**Over-trimmed:** Deleting the entire Testing section because the RFC has already shipped. +**Over-trimmed:** Deleting the entire Testing section because the Agent Note has already shipped. **Balanced:** “Unit tests cover cancellation before and after publication, disposal quiescence, and provider reload. A built-entry smoke covers the real loader path; snapshot coverage is deferred because the transport is process-specific.” @@ -162,6 +162,6 @@ Know what the generator extracts. That fragment must preserve the contract neede **Over-detailed:** Listing private helper cleanup and unused test-only accessors with no caller or maintainer consequence. -**Balanced:** “Provider selection is cached for the plugin lifetime; installing or repairing a provider requires reload.” Keep ordinary cleanup in its TODO or RFC. +**Balanced:** “Provider selection is cached for the plugin lifetime; installing or repairing a provider requires reload.” Keep ordinary cleanup in its TODO or Agent Note. Retain gaps and non-obvious constraints that affect use or safe maintenance. A package README is not a backlog dump. diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml index 198e7b4e09..48c96e4d20 100644 --- a/.github/workflows/build-exe-for-python-sdk.yml +++ b/.github/workflows/build-exe-for-python-sdk.yml @@ -1,7 +1,7 @@ name: Build single-exe # Native builds for the release targets; see -# docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md. +# .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md. # A full target run retains one SDK wheel and three runtime wheels; subset # dispatch retains the SDK wheel and selected runtime wheels. Bare executables # and source closures are test inputs. Run manually or label a PR diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index dd3965f5cd..3d1bba6c17 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -23,7 +23,7 @@ name: E2E (real DeepSeek API) # in the BASE repo's context WITH secrets while still able to check out untrusted # fork code — a textbook key-leak vector, especially once this repo is public. # The fork/secret model and its public-repo implications are recorded in -# docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md. +# .agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md. # # Note: scheduled triggers are auto-disabled after 60 days of repo inactivity; # push/pull_request/workflow_dispatch act as backstops. diff --git a/.github/workflows/landlock-run.yml b/.github/workflows/landlock-run.yml new file mode 100644 index 0000000000..8916f59a56 --- /dev/null +++ b/.github/workflows/landlock-run.yml @@ -0,0 +1,127 @@ +# Manually-dispatched CI for the landlock-run source of record +# (native/landlock-run). A separate workflow from ci.yml on purpose: the +# subtree is a self-contained pnpm workspace with its own gates, exercised on +# demand — per-architecture native legs (build + behavioral tests + pack +# rehearsal on real kernels) plus one darwin leg proving the documented +# degradation on hosts without a platform package. Legs derive from the +# subtree's checked-in package matrix (scripts/github-matrix.mjs). Packing +# for npm happens in the release mirror (node-addon-landlock-run) after an +# export — see native/README.md; this workflow never packs for release. +name: Landlock Run + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +defaults: + run: + working-directory: native/landlock-run + +jobs: + matrix: + name: Matrix + runs-on: ubuntu-24.04 + outputs: + ci: ${{ steps.matrix.outputs.ci }} + steps: + - uses: actions/checkout@v4 + + - id: matrix + run: echo "ci=$(node ./scripts/github-matrix.mjs ci)" >> "$GITHUB_OUTPUT" + + native: + name: ${{ matrix.platform }} + needs: matrix + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix.outputs.ci) }} + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + package_json_file: native/landlock-run/package.json + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: native/landlock-run/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install musl toolchain + run: | + sudo apt-get update -q + sudo apt-get install -yq musl-tools + + - name: Build TypeScript + run: pnpm build:ts + + - name: Typecheck + run: pnpm typecheck + + - name: Build native binaries (this architecture is the builder of record) + run: pnpm build:native + + - name: Entry tests (keyless) + run: node ./test/entry.test.js + + # NALR_REQUIRE_LANDLOCK: a self-skip on the very platform that exists to + # prove enforcement would be a false green, so an unenforcing kernel + # fails the leg instead of skipping. + - name: Launcher tests (real kernel enforcement) + run: node ./test/launcher.test.js + env: + NALR_REQUIRE_LANDLOCK: 1 + + - name: Pack rehearsal (pack → install → confine, this platform only) + run: | + node ./scripts/pack-release.mjs .release/npm --current-platform-only + node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only + env: + NALR_REQUIRE_LANDLOCK: 1 + + darwin: + name: darwin (no platform package — degradation proof) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + package_json_file: native/landlock-run/package.json + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + cache-dependency-path: native/landlock-run/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build TypeScript + run: pnpm build:ts + + - name: Typecheck + run: pnpm typecheck + + - name: Entry tests (keyless) + run: node ./test/entry.test.js + + - name: Launcher tests (must self-skip cleanly) + run: node ./test/launcher.test.js + + - name: Pack rehearsal (entry only — fallback resolution + unusable probe) + run: | + node ./scripts/pack-release.mjs .release/npm --current-platform-only + node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only diff --git a/.github/workflows/sandbox.yml b/.github/workflows/sandbox.yml index 561d74b587..51dfe06f1c 100644 --- a/.github/workflows/sandbox.yml +++ b/.github/workflows/sandbox.yml @@ -19,7 +19,7 @@ permissions: contents: read jobs: - # Keyless real-kernel sandbox proofs (sandbox RFC § Testing): each ladder + # Keyless real-kernel sandbox proofs (sandbox Agent Note § Testing): each ladder # rung is only provable on a host where it enforces, so this job fans out # an OS×runner matrix — bwrap and Landlock on Linux (separate legs: the # Landlock files force the bwrap rung off, so each leg proves exactly one diff --git a/AGENTS.md b/AGENTS.md index cd3505ea8b..24a138e396 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,10 +32,12 @@ packages/ @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/ support/ dev/test infrastructure packages util/ zero-dependency utilities python/ Python SDK and bundled runtime (see python/README.md) +native/ node-addon-landlock-run source of record (see native/README.md) examples/ Runnable cordis.yml leaves over packages/examples bundles (see examples/AGENTS.md) -docs/ architecture, generated catalogs, RFCs, postmortems, cookbook (see docs/AGENTS.md) +.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). @@ -116,7 +118,7 @@ Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, - **An empty `catch` names what it swallows** and why nothing else can reach it; keep the `try` to one statement. - **Prefer symmetry for parallel values**; unexplained asymmetry usually signals a missed extraction. - **Tests describe behavior, not correctness.** Change obsolete behavior with its tests; explain why in the PR. -- **Validate RFC premises against current code**; friction may expose overreach, so amend proposals before moving them to `implemented/`. +- **Every non-trivial change MUST include at least one Agent Note in the same PR.** Update the owning note or add one, validate its premises against code, and exempt only mechanical/local edits ([scope](.agents/notes/README.md#when-to-write-one)). - **Testing policy** — [docs/testing.md](docs/testing.md). Transcript changes need snapshots or a PR note. Fixtures must replay on macOS/Linux; fix fixtures, not normalizers. - **A tool's ACP render intent is part of its design**, decided up front (`generic`/`terminal`/`diff`, `locations`); presentation methods are pure functions of `args` ([cookbook](docs/cookbook/adding-a-tool.md)). - **Plan unit, e2e, and snapshot coverage** for new seams, lifecycle shapes, and transcript surfaces, and schedule any missing harness support before implementation. @@ -134,7 +136,7 @@ Everything compiles under `strict: true` with `noImplicitAny`; every remaining ` Comments and docs preserve complete contracts and non-obvious orientation, not reasoning transcripts. Do not narrate control flow or tests, preserve review history, or restate code. Keep factual clauses affecting behavior, failure, timing, ownership, or safe use; link aggressively to owning rationale. Use [dsh-prose-standard](.agents/skills/dsh-prose-standard/SKILL.md) for prose decisions. Wire mechanically checkable invariants into an executed top-level gate and prove each new or changed acceptance path rejects an invalid case. Use narrow justified exceptions instead of disabling a rule globally. -Docs are part of every change: code changes update their README and JSDoc in the SAME change; a bilingual-pair edit updates the counterpart and re-records ([i18n contract](docs/i18n/README.md)). The writing rules — document the current state never the history, one physical line per paragraph, one home per fact — and the word-budget gate live in [docs/AGENTS.md](docs/AGENTS.md). +Docs accompany every code change: update affected README/JSDoc contracts together; update both sides of a bilingual pair and re-record it ([i18n contract](docs/i18n/README.md)). Current-state prose, one physical line per paragraph, one home per fact, and word budgets live in [docs/AGENTS.md](docs/AGENTS.md). ## Editing these instructions diff --git a/docs/AGENTS.md b/docs/AGENTS.md index a0c30c5545..da0c03b088 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — The documentation standard -This file defines Markdown tiers, writing rules, and `verify-doc-budgets` ceilings. Use [dsh-doc-standards](../.agents/skills/dsh-doc-standards/SKILL.md) for placement and validation, and [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for required coverage and editorial judgment; the [doc-tiers RFC](rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.md) owns rationale. +This file defines Markdown tiers, writing rules, and `verify-doc-budgets` ceilings. Use [dsh-doc-standards](../.agents/skills/dsh-doc-standards/SKILL.md) for placement and validation, and [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for required coverage and editorial judgment; the [doc-tiers Agent Note](../.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md) owns rationale. ## The tier taxonomy: one home per fact @@ -9,23 +9,24 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home; | Tier | Job | Does NOT belong there | |---|---|---| | Root `AGENTS.md` | Standing orders: rules an agent needs in context in every session, one to three lines each, linking its home | Stories, worked examples, situational procedures, anything restated from a linked home | -| Subtree `AGENTS.md` (`packages/`, `examples/`, `docs/`) | Orders specific to that subtree | Repo-wide rules the root file already carries | -| [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ core-data-structures), per-package detail (→ package READMEs), decision rationale (→ RFCs), implementation-status annotations | +| Subtree `AGENTS.md` (`packages/`, `examples/`, `docs/`, `.agents/notes/`) | Orders specific to that subtree | Repo-wide rules the root file already carries | +| [architecture.md](architecture.md) | The system map: services, the loop, extension seams — read before changing `packages/` | Type shapes (→ core-data-structures), per-package detail (→ package READMEs), decision rationale (→ Agent Notes), implementation-status annotations | | [core-data-structures/](core-data-structures/core.md) | The type catalog: literal shapes and semantics of the spine and seam vocabulary | Behavior narration (→ architecture.md) | -| [rfc/](rfc/README.md) | Decision records: the why, what-was-given-up, and concise verification contract; `implemented/` RFCs describe shipped reality in present tense | Migration plans, acceptance-task checklists, fixture walkthroughs, and spec-speak ("should…") once the decision has shipped | +| [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 RFC each guide links) | +| [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 (→ RFCs), 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 | +| [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), [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 → RFCs; procedures → cookbooks; type shapes → core data; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link. +Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type shapes → core data; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link. ## Writing rules -- **Document current state, not change history.** Avoid "previously/now/no longer", PRs, commits, and stack positions in durable prose; name the live mechanism. Put change stories in commits, PRs, RFCs, or postmortems. -- **Write an RFC in the same PR for decisions a maintainer may reasonably revisit.** Mechanical or self-evident changes need none ([when to write one](rfc/README.md)). +- **Document current state, not change history.** Avoid "previously/now/no longer", PRs, commits, and stack positions in durable prose; name the live mechanism. Put change stories in commits, PRs, Agent Notes, or postmortems. +- **Every non-trivial change includes at least one Agent Note in the same PR.** Update the owning note or add one; only mechanical/local edits are exempt ([scope](../.agents/notes/README.md#when-to-write-one)). - **One physical line per paragraph** (`verify-md-wrap`): use editor soft-wrap. Code blocks, tables, and list structure keep their formatting; code comments stay under the linter's column limit. - **Fenced `ts` blocks must compile** (`doc-typecheck`); a pasted type declaration and its original JSDoc use ` ```ts type-equiv `, while a body-stripped public class declaration uses ` ```ts public-api `; register either in the manifest so neither can drift ([mechanics](development.md#documenting-types-verbatim-ts-type-equiv)). - **The [core-data-structures catalog](core-data-structures/core.md) updates in the same change** that reshapes a documented type. `verify-type-equiv` catches drifted pastes, not never-documented new types ([what counts as core](core-data-structures/core.md#what-counts-as-core)). @@ -50,8 +51,8 @@ Ceilings are guardrails, not reduction targets. Retain at least 5% headroom; low Hunt these in any doc; the [dsh-doc-standards](../.agents/skills/dsh-doc-standards/SKILL.md) skill runs this list as an audit: - The same rule stated in more than one home. Grep a distinctive phrase; keep one home, convert the rest to links. -- Narrated history: "previously", "now", "no longer", "used to", "renamed", "was moved", references to PRs or commits. State the current fact; the why belongs in an RFC, the story in a postmortem or git. -- A war story told inline where a one-line rule plus a postmortem/RFC link would do. +- Narrated history: "previously", "now", "no longer", "used to", "renamed", "was moved", references to PRs or commits. State the current fact; the why belongs in an Agent Note, the story in a postmortem or git. +- A war story told inline where a one-line rule plus a postmortem/Agent Note link would do. - Implementation-status annotations in prose or diagrams ("implemented!", "future: …"). Status rots; the repo layout and package manifests carry it. - Hand-restating a generated catalog or JSDoc: event tables, tool arg tables, method signatures. Link instead. - Hand-maintained inventories of tests, packages, or implementation status when the tree or a generator is authoritative. @@ -59,10 +60,10 @@ Hunt these in any doc; the [dsh-doc-standards](../.agents/skills/dsh-doc-standar - The same rationale repeated beside sibling methods. State it once at the owning seam or shared helper. - Paragraph walls: one paragraph carrying several rules and parenthetical asides. Split it, or demote the detail to the linked home. - Emphasis inflation: bold, CAPS, or "critically" everywhere means nothing stands out. Reserve emphasis for the clause that changes behavior. -- Spec-speak in `implemented/` RFCs: "should", migration plans, acceptance checklists. An implemented RFC describes what is, per [rfc/implemented/AGENTS.md](rfc/implemented/AGENTS.md). +- Spec-speak in `implemented/` Agent Notes: "should", migration plans, acceptance checklists. An implemented Agent Note describes what is, per the [implemented-note instructions](../.agents/notes/implemented/AGENTS.md). ## Cross-reference with machine-checkable links, never free prose -Link repository references with relative Markdown paths, never bare filenames or RFC numbers. `verify-md-links` catches missing targets; the [cross-link RFC](rfc/implemented/process/2026-06-18-markdown-cross-link-lint.md) owns the rationale. +Link repository references with relative Markdown paths, never bare filenames or Agent Note numbers. `verify-md-links` catches missing targets; the [cross-link Agent Note](../.agents/notes/implemented/process/2026-06-18-markdown-cross-link-lint.md) owns the rationale. The gate checks file existence, not `#anchor` validity — verify anchors yourself when linking to one. 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 fd21648dd2..bab5ddee73 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 | @@ -54,9 +55,9 @@ Waterfall events behave like around-middleware: a listener delegates by calling ## Default Loop Lifecycle -The shipped loop drains work from prompt through checkpoint. Every pause is a service call or event available to plugins. +The shipped loop drains prompt-to-checkpoint work through plugin-visible services and events. -A **session** is an append-only event log. A **turn** drains queued input until the model stops asking for tools and no plugin requests continuation. A **step** is one model request plus the tool executions caused by that response. 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 `<config-id>-session-<uuid>`; `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. @@ -68,13 +69,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 @@ -106,11 +107,11 @@ forever: checkpoint persistence and notify idle/running status ``` -Each step assembles ordered prompt sections, tool schemas, and `{{name}}` variables; unknown or valueless references fail the turn. `dsh-system-prompt` owns the harness identity and default persona, which an agent scope may shadow. The loop supplies `model` and `cwd` ([prompt-ownership RFC](rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)). +Each step assembles ordered prompt sections, tool schemas, and `{{name}}` variables; unknown or valueless references fail the turn. `dsh-system-prompt` owns the harness identity and default persona, which an agent scope may shadow. The loop supplies `model` and `cwd` ([prompt ownership](../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)). Tool-time context—including async `agent.inject()` notices and post-tool `additionalContexts`—settles, then follows 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, stays authoritative through turn close and flush, and discards later steering but preserves queued prompts. -`dsh-compact-basic` handles pressure and canonical overflow at these checkpoints; retry requires a balanced surface replacement ([RFC](rfc/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 @@ -126,7 +127,7 @@ Every session event is turn-enclosed. Reloading preserves an interrupted tail an ### Agent Scope -Every live agent owns a scoped `agent.ctx`. Its registrations shadow globals, receive only that agent's dispatches, and unwind with it; async effects such as background-task cleanup are awaited. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. Typed resolvers derive carrier checks from merged `Events` signatures and `scopeTarget` ([semantic-gates RFC](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md)). See the [agent-scope RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md) and [subagent composition controls](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md). `AgentLoop` runs drivers inside `ctx.agents.withInitiator()`; private orchestration derives `agent.session`; other identities stay explicit ([RFC](rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md)). +Every live agent owns a scoped `agent.ctx`. Its registrations shadow globals, receive only that agent's dispatches, and unwind with it; async effects such as background-task cleanup are awaited. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. Typed resolvers derive carrier checks from merged `Events` signatures and `scopeTarget` ([semantic gates](../.agents/notes/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md)). See [agent scope](../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md) and [subagent composition controls](../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md). `AgentLoop` runs drivers inside `ctx.agents.withInitiator()`; private orchestration derives `agent.session`; other identities stay explicit ([decision](../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)). ## State @@ -134,7 +135,7 @@ Every live agent owns a scoped `agent.ctx`. Its registrations shadow globals, re The session log is the source of truth. `deriveMessages()` projects session events into the `Message[]` sent to the model; raw `assistant/chunk` events stay in the log for replay and UI fidelity. Replay, fork, resume, transcript rendering, telemetry, and persistence all derive from the same event stream. -**Model-visible ⟺ logged**: the log reconstructs every request — messages at `step/start` fronted by the header's session prefix, headers by folding `request/header` — and dev invariants assert this ([reconstructability RFC](rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)). +**Model-visible ⟺ logged**: the log reconstructs every request — messages at `step/start` fronted by the header's session prefix, headers by folding `request/header` — and dev invariants assert this ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)). Durability is a plugin concern. Persistence backends buffer synchronous `session/event` notifications and the loop awaits a turn-end checkpoint before moving on. The `SessionPersistence` seam stores `SessionEvent` directly, with metadata in `SessionHeader`; JSONL and SQLite share one contract suite. @@ -152,7 +153,7 @@ A swappable capability usually splits into **interface / implementation / consum Some seams bend the template deliberately: LLM combines interface and consumer because adapters implement it; filesystem wraps provider primitives with policy; web keeps search/fetch provider registries behind one service; skills and subagents use named providers. Subagents spawn fresh, fork a completed-turn prefix, or use ACP children ([subagent.md](core-data-structures/subagent.md)). -`dsh-workspace-context` composes baselines on `agent/session-prefix` and appends `ctx.fs`-discovered nested changes on `tools/post-execute`; its [RFC](rfc/implemented/feature/2026-06-24-workspace-context.md) records isolation. `dsh-paths` owns shared paths. +`dsh-workspace-context` composes baselines on `agent/session-prefix` and appends `ctx.fs`-discovered nested changes on `tools/post-execute`; its [decision](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md) records isolation. `dsh-paths` owns shared paths. ### Bundles And Apps @@ -185,4 +186,4 @@ The [extension cookbook](cookbook/extension-cookbook.md) carries plugin skeleton - Exact event and service signatures in [events](cordis-catalog/events.md) - [services](cordis-catalog/services.md) catalogs - package contracts in the [package map](../packages/README.md) -- [RFCs](rfc/README.md) +- [Agent Notes](../.agents/notes/README.md) diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 58641456b1..a7ec9b638b 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<br/>Replay token measurement"] + pkg_compact_tool_result_prune["compact-tool-result-prune"] + svc_toolResultPrune["ctx.toolResultPrune<br/>Model-free tool-result pruning"] pkg_session["session"] svc_sessions["ctx.sessions<br/>In-memory session store"] pkg_agent["agent"] @@ -60,6 +62,9 @@ flowchart LR pkg_sandbox["sandbox"] svc_sandbox["ctx.sandbox<br/>Process-sandbox seam"] pkg_sandbox_local["sandbox-local"] + pkg_sandbox_policy["sandbox-policy"] + svc_sandboxPolicy["ctx.sandboxPolicy<br/>Sandbox policy home"] + pkg_fs_sandbox["fs-sandbox"] pkg_approval["approval"] svc_approval["ctx.approval<br/>Approval seam"] pkg_permission["permission"] @@ -107,8 +112,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_llm --> svc_llm pkg_llm_deepseek --> svc_llm pkg_llm_pi_ai --> svc_llm @@ -116,6 +123,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 @@ -162,6 +170,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 @@ -186,6 +196,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 @@ -208,6 +219,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. | @@ -220,10 +232,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 ad8ffceede..c2d31f93e3 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -64,7 +64,7 @@ export interface Config { skills?: agentCore.SkillConfig /** Model-facing bash tool config forwarded through agent-core. */ toolBash?: NonNullable<agentCore.Config['toolBash']> - /** Generic background-task control-tool config forwarded through agent-core. */ + /** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */ toolTasks?: NonNullable<agentCore.Config['toolTasks']> } ``` @@ -140,12 +140,14 @@ export interface Config { skills?: SkillConfig /** Model-facing bash tool config, including this producer's background opt-in. */ toolBash?: toolBash.Config - /** Generic background-task control-tool wait bounds. */ - toolTasks?: toolTasks.Config + /** Generic background-task controls; set false to keep the task service without model-facing task tools. */ + toolTasks?: toolTasks.Config | false } /** Skill bundle config forwarded to the registry, local provider, and model-facing consumer. */ export interface SkillConfig { + /** Mount the bundled local skill provider and model-facing skill tool (default true). */ + enabled?: boolean /** Registry-level discovery cache settings. */ registry?: SkillRegistryConfig /** Local filesystem skill provider settings. */ @@ -157,7 +159,7 @@ export interface SkillConfig { Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts) · [`workspaceContext`](../packages/context/workspace-context/src/index.ts) -Source: [`packages/examples/agent-spine-demo/src/index.ts:57`](../packages/examples/agent-spine-demo/src/index.ts) +Source: [`packages/examples/agent-spine-demo/src/index.ts:59`](../packages/examples/agent-spine-demo/src/index.ts) ## `@deepseek-ai/dsh-bash-local` @@ -172,7 +174,9 @@ export interface Config { maxTimeoutMs?: number /** Per-stream in-memory output cap; overflow spills to a temp file. */ maxOutputBytes?: number - /** Grace period between the SIGTERM and the SIGKILL escalation on a kill. */ + /** Per-stream spill-file cap; larger streams retain only their in-memory tail. */ + maxSpillBytes?: number + /** Grace period for kill escalation and for inherited pipes after shell exit. */ graceMs?: number } ``` @@ -181,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) @@ -306,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 @@ -318,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-hooks-claude` Requires: `bash` @@ -384,8 +415,10 @@ Source: [`packages/hooks/hooks-codex/src/index.ts:42`](../packages/hooks/hooks-c Requires: `agents` ```ts config-catalog -/** Runtime-only test seams; no field is configurable from `cordis.yml`. */ +/** JSON-RPC deployment config plus runtime-only test seams. */ export interface JsonRpcConfig { + /** Report max-token turn/subagent termination as a successful SDK result. */ + maxTokensAsSuccess?: boolean /** Transport input override; production uses `process.stdin`. */ input?: Readable /** Transport output override; production uses `process.stdout`. */ @@ -592,7 +625,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 @@ -605,7 +638,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` @@ -667,6 +700,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` @@ -856,7 +914,7 @@ export interface Config { skills?: agentCore.SkillConfig /** Model-facing bash tool config forwarded through agent-core. */ toolBash?: NonNullable<agentCore.Config['toolBash']> - /** Generic background-task control-tool config forwarded through agent-core. */ + /** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */ toolTasks?: NonNullable<agentCore.Config['toolTasks']> /** * If set, the pre-created agent RESUMES this persisted session id instead of @@ -1022,7 +1080,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` @@ -1034,7 +1092,7 @@ export interface Config { /** * Milliseconds the SYNCHRONOUS portion of mount code may run in the vm * before evaluation is aborted (default 5000). An async body escapes this - * bound — see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md for the trust stance. + * bound — see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md for the trust stance. */ vmTimeoutMs?: number } @@ -1060,7 +1118,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` @@ -1082,7 +1140,7 @@ export interface Config { } ``` -Source: [`packages/fs/tool-fs-search/src/index.ts:59`](../packages/fs/tool-fs-search/src/index.ts) +Source: [`packages/fs/tool-fs-search/src/index.ts:62`](../packages/fs/tool-fs-search/src/index.ts) ## `@deepseek-ai/dsh-tool-skill` @@ -1470,7 +1528,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co ## Seam packages (not directly loadable) -Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](rfc/implemented/architecture/2026-06-13-capability-seams.md)). +Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)). - `@deepseek-ai/dsh-bash` — abstract `BashExecutor` ([`packages/bash/bash/src/index.ts`](../packages/bash/bash/src/index.ts)) - `@deepseek-ai/dsh-code-runtime` — abstract `CodeRuntime` ([`packages/code-runtime/code-runtime/src/index.ts`](../packages/code-runtime/code-runtime/src/index.ts)) diff --git a/docs/cookbook/adding-a-package.i18n.yaml b/docs/cookbook/adding-a-package.i18n.yaml index 854acc2750..6fd3feebbd 100644 --- a/docs/cookbook/adding-a-package.i18n.yaml +++ b/docs/cookbook/adding-a-package.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -adding-a-package.md: 404492a9903d823feb011ec2536e4b66ef110e32 -adding-a-package.zh.md: 4be67137d416f373bf3477e30785055fedc5ac6f +adding-a-package.md: 556a48493af4452c178634c0abb4e23e2419dd8e +adding-a-package.zh.md: 5f7e4692233448c746d25e4808c078c390cf39e6 diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md index 404492a990..556a48493a 100644 --- a/docs/cookbook/adding-a-package.md +++ b/docs/cookbook/adding-a-package.md @@ -44,7 +44,7 @@ For a swappable capability, split interface / implementation / consumer into sep ## 4. Write the package README -Keep package-specific service API, config, events, extension points, and design notes first. The limitations section records durable consumer gaps and non-obvious maintainer constraints owned by this package; ordinary cleanup stays in its source TODO or RFC. An indirect Model Experience sentence may name the consumer that surfaces this package's contribution, but it does not restate that consumer's implementation. End a package README with this canonical sequence: +Keep package-specific service API, config, events, extension points, and design notes first. The limitations section records durable consumer gaps and non-obvious maintainer constraints owned by this package; ordinary cleanup stays in its source TODO or Agent Note. An indirect Model Experience sentence may name the consumer that surfaces this package's contribution, but it does not restate that consumer's implementation. End a package README with this canonical sequence: ````markdown ## Model Experience @@ -76,7 +76,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex Fill Model Experience from the implementation. Use one H3 per direct, conditional, capped, lifetime, or auxiliary-model surface, with the three ordered H4 fields shown above and one prose paragraph under each. Quote stable text owned by the package: system-prompt prose goes in a titled H5 plus `markdown` fence under the field that introduces it—normally `What the model sees`—other short literals stay inline with named placeholders, and other long literals use the same nested form. Summarize only data-dependent or provider-owned text. A tool-schema surface links its anchored section in the generated [tool catalog](../tool-catalog.md) and states only deltas absent there. Keep prompt and schema surfaces separate when scoping can hide one without the other. In `KV Cache effect`, distinguish append-only growth, a stable repeated prefix, replacement of earlier request tokens, and an independent model request, then name the package-owned changes that can invalidate reuse. “Does not invalidate” means the package preserves an already-reusable prefix; provider cache availability and eviction remain outside the package contract. The [prose standard](../../.agents/skills/dsh-prose-standard/SKILL.md) governs completeness and ownership; the verifier enforces the mechanical shape. -A package with no context effect or one consumer-owned path uses the audited `None, as ` or `Indirectly, through ` sentence in [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts), followed by a `KV Cache effect` H4 and one non-empty paragraph; a model-agnostic generic package may instead join `NO_MODEL_EXPERIENCE_SECTION`. Do not expand either case into a description of another package's work. The limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) is independent. The [Model Experience RFC](../rfc/implemented/process/2026-07-12-package-model-experience-contract.md) records the rationale. +A package with no context effect or one consumer-owned path uses the audited `None, as ` or `Indirectly, through ` sentence in [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts), followed by a `KV Cache effect` H4 and one non-empty paragraph; a model-agnostic generic package may instead join `NO_MODEL_EXPERIENCE_SECTION`. Do not expand either case into a description of another package's work. The limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) is independent. The [Model Experience Agent Note](../../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md) records the rationale. ## 5. Verify diff --git a/docs/cookbook/adding-a-package.zh.md b/docs/cookbook/adding-a-package.zh.md index 4be67137d4..5f7e469223 100644 --- a/docs/cookbook/adding-a-package.zh.md +++ b/docs/cookbook/adding-a-package.zh.md @@ -44,7 +44,7 @@ package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-c ## 4. 编写包 README -将包特有的服务 API、配置、事件、扩展点和设计说明放在前面。limitations 部分记录持久的消费方缺口和本包拥有的非显而易见的维护者约束;日常清理事项留在源码 TODO 或 RFC 中。间接的 Model Experience 语句可以点名暴露本包贡献的消费方,但不重述该消费方的实现。包 README 以如下规范序列结尾: +将包特有的服务 API、配置、事件、扩展点和设计说明放在前面。limitations 部分记录持久的消费方缺口和本包拥有的非显而易见的维护者约束;日常清理事项留在源码 TODO 或 Agent Note 中。间接的 Model Experience 语句可以点名暴露本包贡献的消费方,但不重述该消费方的实现。包 README 以如下规范序列结尾: ````markdown ## Model Experience @@ -76,7 +76,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex 根据实现填写 Model Experience。每个直接、条件、上限、生命周期或辅助模型的 surface 使用一个 H3,包含上述三个有序 H4 字段,每个字段下有一个正文段落。引用包拥有的稳定文本:系统提示词放在引出它的字段下,用带标题的 H5 加 `markdown` 围栏表示,通常归入 `What the model sees`;其他短文本以命名占位符内联,其他长文本使用相同的嵌套形式。仅概述数据依赖或提供方拥有的文本。tool-schema surface 链接到生成的[工具目录](../tool-catalog.md)中对应的锚定章节,仅说明该处缺失的差异。当作用域可以隐藏 prompt 或 schema 其中之一而不影响另一个时,将二者分开。填写 `KV Cache effect` 时,应区分仅追加增长、稳定重复的前缀、替换既有请求 token 和独立模型请求,并列出会使缓存复用失效、且由本包拥有的变化。“不使缓存失效”仅表示本包保留了已有的可复用前缀;缓存是否可用以及何时淘汰不属于本包契约。[行文标准](../../.agents/skills/dsh-prose-standard/SKILL.md)约束完整性与归属;验证器强制执行机械形状。 -没有上下文效果或仅有消费方拥有路径的包使用 [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts) 中经过审计的 `None, as ` 或 `Indirectly, through ` 语句,随后添加 `KV Cache effect` H4 和一个非空正文段落;与模型无关的通用包可以改为加入 `NO_MODEL_EXPERIENCE_SECTION`。两种情况都不要展开为对另一个包工作的描述。limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) 独立管理。[Model Experience RFC](../rfc/implemented/process/2026-07-12-package-model-experience-contract.md) 记录了设计动机。 +没有上下文效果或仅有消费方拥有路径的包使用 [`SENTENCE_MODEL_EXPERIENCE`](../../scripts/verify-package-readme-model-experience.ts) 中经过审计的 `None, as ` 或 `Indirectly, through ` 语句,随后添加 `KV Cache effect` H4 和一个非空正文段落;与模型无关的通用包可以改为加入 `NO_MODEL_EXPERIENCE_SECTION`。两种情况都不要展开为对另一个包工作的描述。limitations [allowlist](../../scripts/verify-package-readme-limitations.ts) 独立管理。[Model Experience Agent Note](../../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md) 记录了设计动机。 ## 5. 验证 diff --git a/docs/cookbook/adding-a-tool.i18n.yaml b/docs/cookbook/adding-a-tool.i18n.yaml index 6ec964c335..5bcb3bac1d 100644 --- a/docs/cookbook/adding-a-tool.i18n.yaml +++ b/docs/cookbook/adding-a-tool.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -adding-a-tool.md: da214702939e01fedf3d0d69be7560bbafe0696a -adding-a-tool.zh.md: b216d18b1593cd7e6074685bd39684f1b9694eac +adding-a-tool.md: 68a8449bc189497b917efe678837d757f85aaf75 +adding-a-tool.zh.md: 003534e04550bfbee6740aa3b6bee02ac2cdc237 diff --git a/docs/cookbook/adding-a-tool.md b/docs/cookbook/adding-a-tool.md index da21470293..68a8449bc1 100644 --- a/docs/cookbook/adding-a-tool.md +++ b/docs/cookbook/adding-a-tool.md @@ -35,7 +35,7 @@ Registration is effect-based: disposing the plugin fiber unregisters the tool (w ## Rules of the execute() contract -- **Args are validated for you.** `defineTool` validates the model-generated `arguments` against the `SchemaSpec` before `execute` runs (type, required keys, enum membership, nested objects/arrays — [runtime arg validation](../rfc/implemented/architecture/2026-06-11-runtime-arg-validation.md)), so inside `execute` the args already match `InferArgs`. You still hand-check value constraints the DSL can't express (non-empty strings, positive numbers, cross-field rules); throw a descriptive Error for those. Raw JSON-Schema tools registered directly (MCP) are NOT validated by the harness — they validate their own input. +- **Args are validated for you.** `defineTool` validates the model-generated `arguments` against the `SchemaSpec` before `execute` runs (type, required keys, enum membership, nested objects/arrays — [runtime arg validation](../../.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md)), so inside `execute` the args already match `InferArgs`. You still hand-check value constraints the DSL can't express (non-empty strings, positive numbers, cross-field rules); throw a descriptive Error for those. Raw JSON-Schema tools registered directly (MCP) are NOT validated by the harness — they validate their own input. - **Registration borrows your readonly definition.** A typed same-process contribution is not a serialization boundary; do not mutate its schema or replace callbacks after registration. `schemas()` materializes only the explicit model-facing projection. To hot-swap a tool, dispose its owning effect and register the replacement; mutable state inside the callback's closure remains ordinary plugin state. - **Execution identity is protected.** The registry materializes `arguments` as detached lossless JSON in one recursive pass, freezes that value before policy starts, and assigns an opaque `exec.token`; `callId`, `name`, `arguments`, `agent`, `token`, and an optional enclosing-transport `parent` token stay immutable through dispatch. `parent` is identity-only and exposes no live outer execution. Treat `args` as readonly input. An around-dispatch wrapper may add, replace, or remove only `exec.signal` to impose cancellation or a deadline. - **Throwing or returning non-JSON data means `isError`.** The registry catches throws and materializes the final result before observers run. A malformed or non-JSON result becomes `{ isError: true }`, preventing a live success that cannot be logged. Throw for infrastructure failures; report domain failures in result text when the model must interpret them. @@ -47,11 +47,11 @@ Registration is effect-based: disposing the plugin fiber unregisters the tool (w Gate `run_in_background` with producer config, reject a pre-aborted call, then register through `ctx.tasks.start({ kind, label, owner: exec.agent, run })`. The runtime validates ownership and control-surface availability before `run()` starts work, then supplies the id, session fence, generic control tools, notices, and owner cleanup. -The producer supplies synchronous `cancel`, non-rejecting `done` that settles after resource cleanup, and optional consuming `readOutput` with bounded-output formatting. Once the id is returned, use a task-owned cancellation signal rather than `exec.signal`. See the [background task runtime RFC](../rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) and `dsh-tool-bash` for a stream producer. +The producer supplies synchronous `cancel`, non-rejecting `done` that settles after resource cleanup, and optional consuming `readOutput` with bounded-output formatting. Once the id is returned, use a task-owned cancellation signal rather than `exec.signal`. See the [background task runtime Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) and `dsh-tool-bash` for a stream producer. ## Execution policy and observation -Prefer not to build deployment policy into the tool. Use `tools/pre-execute` for extensible allow/deny/ask policy (the [permission-gate example](./extension-cookbook.md#a-hook-plugin-permission-gate-example)), `ctx.tools.guard()` for a final monotonic deny that later listeners cannot undo, `tools/execute` to wrap core dispatch with a deadline/retry/metrics scope, `tools/post-execute` to transform or attach model-facing context, and `tools/result` to observe the immutable normalized outcome without changing it. A sandboxing implementation can also sit behind the tool's executor capability seam; the exact contracts are in the [`dsh-tools` README](../../packages/core/tools/README.md#extension-points). +Prefer not to build deployment policy into the tool. Use `tools/pre-execute` for extensible allow/deny/ask policy (the [permission-gate example](extension-cookbook.md#a-hook-plugin-permission-gate-example)), `ctx.tools.guard()` for a final monotonic deny that later listeners cannot undo, `tools/execute` to wrap core dispatch with a deadline/retry/metrics scope, `tools/post-execute` to transform or attach model-facing context, and `tools/result` to observe the immutable normalized outcome without changing it. A sandboxing implementation can also sit behind the tool's executor capability seam; the exact contracts are in the [`dsh-tools` README](../../packages/core/tools/README.md#extension-points). ## Code Mode reaches your tool for free @@ -78,8 +78,8 @@ Hard rules (they bite if broken): - **UI-only formatting stays out of the model result.** A fenced ` ```console ` block, a diff, a relativized path — none of these may appear in what `execute` returns to the model; they live only in the presentation. (A `terminal` result view carries RAW `output`; the bridge adds the fences.) - **`defineTool` soft-validates the display path.** A malformed/older logged arg shape makes the wrapper return `undefined` (a generic fallback) rather than throw — display must never crash a replay. -The neutral vocabulary lives in `dsh-tools` (never import an ACP type into a tool); the ACP bridge maps each `card` to the wire. The design and the why are in [the render-intent-union RFC](../rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md); `dsh-tool-fs` (generic/diff) and `dsh-tool-bash` (terminal) are the reference implementations. +The neutral vocabulary lives in `dsh-tools` (never import an ACP type into a tool); the ACP bridge maps each `card` to the wire. The design and the why are in [the render-intent-union Agent Note](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md); `dsh-tool-fs` (generic/diff) and `dsh-tool-bash` (terminal) are the reference implementations. ## Tests every tool needs -Cover argument rejection, every result shape, and HMR disposal. For a side-effecting tool, drive the real tool through the agent loop with a scripted `MockAdapter` and assert its `tool/call` and `tool/result` session events. For an editor card, assert the exact `presentCall` and `presentResult` views and add an [ACP snapshot](../rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md) through the real bridge; a terminal card's scenario sets `terminalOutput: true` to exercise the capable-client path. +Cover argument rejection, every result shape, and HMR disposal. For a side-effecting tool, drive the real tool through the agent loop with a scripted `MockAdapter` and assert its `tool/call` and `tool/result` session events. For an editor card, assert the exact `presentCall` and `presentResult` views and add an [ACP snapshot](../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md) through the real bridge; a terminal card's scenario sets `terminalOutput: true` to exercise the capable-client path. diff --git a/docs/cookbook/adding-a-tool.zh.md b/docs/cookbook/adding-a-tool.zh.md index b216d18b15..003534e045 100644 --- a/docs/cookbook/adding-a-tool.zh.md +++ b/docs/cookbook/adding-a-tool.zh.md @@ -35,7 +35,7 @@ export function apply(ctx: Context) { ## execute() 契约的规则 -- **参数已为你校验。** `defineTool` 在 `execute` 运行前,会根据 `SchemaSpec` 校验模型生成的 `arguments`(类型、必填键、枚举成员、嵌套对象/数组——见[运行时参数校验](../rfc/implemented/architecture/2026-06-11-runtime-arg-validation.md)),因此 `execute` 内部的 args 已匹配 `InferArgs`。你仍需手动检查 DSL 无法表达的值约束(非空字符串、正数、跨字段规则),对这些情况抛出描述性 Error。直接注册的原始 JSON-Schema 工具(MCP)不由 harness 校验,它们自行校验输入。 +- **参数已为你校验。** `defineTool` 在 `execute` 运行前,会根据 `SchemaSpec` 校验模型生成的 `arguments`(类型、必填键、枚举成员、嵌套对象/数组——见[运行时参数校验](../../.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md)),因此 `execute` 内部的 args 已匹配 `InferArgs`。你仍需手动检查 DSL 无法表达的值约束(非空字符串、正数、跨字段规则),对这些情况抛出描述性 Error。直接注册的原始 JSON-Schema 工具(MCP)不由 harness 校验,它们自行校验输入。 - **注册借用你的只读定义。** 类型化的同进程贡献不是序列化边界;注册后不要修改其 schema 或替换回调。`schemas()` 只物化显式的模型可见投影。如需热替换工具,请 dispose 其所属副作用并注册替代品;回调闭包内的可变状态仍是普通的插件状态。 - **执行身份受保护。** 注册表在一次递归遍历中将 `arguments` 物化为分离的无损 JSON,在策略开始前冻结该值,并分配一个不透明的 `exec.token`;`callId`、`name`、`arguments`、`agent`、`token` 以及可选的外层传输 `parent` token 在整个分发过程中保持不可变。`parent` 仅用于身份标识,不暴露活跃的外层执行。请将 `args` 视为只读输入。around-dispatch 包装器只能添加、替换或移除 `exec.signal`,以施加取消或截止时间。 - **抛出异常或返回非 JSON 数据意味着 `isError`。** 注册表捕获异常,并在观察者运行前物化最终结果。格式错误或非 JSON 的结果变为 `{ isError: true }`,防止出现无法记录的活跃成功。基础设施故障请抛异常;当模型需要解读领域失败时,请在结果文本中报告。 @@ -47,11 +47,11 @@ export function apply(ctx: Context) { 通过 producer 配置控制 `run_in_background`,拒绝已预先中止的调用,然后使用 `ctx.tasks.start({ kind, label, owner: exec.agent, run })` 注册任务。运行时会在 `run()` 启动工作前校验 owner 和控制面是否可用,随后提供 id、会话围栏、通用控制工具、通知和 owner cleanup。 -producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 `done`,以及可选的消费式 `readOutput`(负责有界输出的格式化)。返回 id 后,应使用 task 自有的取消信号,而不是 `exec.signal`。流式 producer 的示例和完整契约见[后台 task 运行时 RFC](../rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与 `dsh-tool-bash`。 +producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 `done`,以及可选的消费式 `readOutput`(负责有界输出的格式化)。返回 id 后,应使用 task 自有的取消信号,而不是 `exec.signal`。流式 producer 的示例和完整契约见[后台 task 运行时 Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与 `dsh-tool-bash`。 ## 执行策略与观测 -尽量不要把部署策略内建到工具中。使用 `tools/pre-execute` 实现可扩展的允许/拒绝/询问策略(见[权限门禁示例](./extension-cookbook.md#a-hook-plugin-permission-gate-example));使用 `ctx.tools.guard()` 设置最终的单调拒绝(后续监听器无法撤销);使用 `tools/execute` 为核心分发包装截止时间/重试/指标作用域;使用 `tools/post-execute` 转换或附加模型可见的上下文;使用 `tools/result` 观测不可变的归一化结果而不改变它。沙箱实现也可以位于工具执行器的能力 seam 之后;确切契约见 [`dsh-tools` README](../../packages/core/tools/README.md#extension-points)。 +尽量不要把部署策略内建到工具中。使用 `tools/pre-execute` 实现可扩展的允许/拒绝/询问策略(见[权限门禁示例](extension-cookbook.md#a-hook-plugin-permission-gate-example));使用 `ctx.tools.guard()` 设置最终的单调拒绝(后续监听器无法撤销);使用 `tools/execute` 为核心分发包装截止时间/重试/指标作用域;使用 `tools/post-execute` 转换或附加模型可见的上下文;使用 `tools/result` 观测不可变的归一化结果而不改变它。沙箱实现也可以位于工具执行器的能力 seam 之后;确切契约见 [`dsh-tools` README](../../packages/core/tools/README.md#extension-points)。 ## Code Mode 自动触达你的工具 @@ -78,8 +78,8 @@ producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 - **UI 格式不进入模型结果。** 围栏 ` ```console ` 块、diff、相对化路径——这些都不得出现在 `execute` 返回给模型的内容中;它们只存在于展示层。(`terminal` 结果视图携带原始 `output`;桥接层添加围栏。) - **`defineTool` 对展示路径做软校验。** 格式错误或旧版日志中的 arg 形态会使包装器返回 `undefined`(通用回退)而非抛异常——展示绝不能导致回放崩溃。 -中性词汇定义在 `dsh-tools` 中(绝不在工具中导入 ACP 类型);ACP 桥接层将每个 `card` 映射到协议格式(wire format)。设计与原因见[渲染意图联合体 RFC](../rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md);`dsh-tool-fs`(generic/diff)和 `dsh-tool-bash`(terminal)是参考实现。 +中性词汇定义在 `dsh-tools` 中(绝不在工具中导入 ACP 类型);ACP 桥接层将每个 `card` 映射到协议格式(wire format)。设计与原因见[渲染意图联合体 Agent Note](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md);`dsh-tool-fs`(generic/diff)和 `dsh-tool-bash`(terminal)是参考实现。 ## 每个工具必须的测试 -覆盖参数拒绝、每种结果形态和 HMR dispose。对于有副作用的工具,使用脚本化的 `MockAdapter` 驱动真实工具通过 agent loop(智能体循环),并断言其 `tool/call` 和 `tool/result` 会话事件。对于编辑器卡片,断言 `presentCall` 和 `presentResult` 的精确视图,并通过真实桥接层添加一个 [ACP 快照](../rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md);终端卡片的场景设置 `terminalOutput: true` 以覆盖 capable-client 路径。 +覆盖参数拒绝、每种结果形态和 HMR dispose。对于有副作用的工具,使用脚本化的 `MockAdapter` 驱动真实工具通过 agent loop(智能体循环),并断言其 `tool/call` 和 `tool/result` 会话事件。对于编辑器卡片,断言 `presentCall` 和 `presentResult` 的精确视图,并通过真实桥接层添加一个 [ACP 快照](../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md);终端卡片的场景设置 `terminalOutput: true` 以覆盖 capable-client 路径。 diff --git a/docs/cookbook/adding-a-vendored-package.i18n.yaml b/docs/cookbook/adding-a-vendored-package.i18n.yaml index 0a359ead84..b3ca7ca791 100644 --- a/docs/cookbook/adding-a-vendored-package.i18n.yaml +++ b/docs/cookbook/adding-a-vendored-package.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -adding-a-vendored-package.md: d7b5b93b59fb39d8369be6eb42fb0a8b977c68b4 -adding-a-vendored-package.zh.md: 86b1e6c959180ba15b6fcb56b6dfe5a3be791b47 +adding-a-vendored-package.md: 1b82f2e582ca5cd040a7f3237505848dbb304fae +adding-a-vendored-package.zh.md: 7245682ef8b7d85ace2c626f8d47aa36f739506b diff --git a/docs/cookbook/adding-a-vendored-package.md b/docs/cookbook/adding-a-vendored-package.md index d7b5b93b59..1b82f2e582 100644 --- a/docs/cookbook/adding-a-vendored-package.md +++ b/docs/cookbook/adding-a-vendored-package.md @@ -2,7 +2,7 @@ English | [中文](adding-a-vendored-package.zh.md) -When the harness needs another upstream Cordis package (e.g. `@cordisjs/plugin-http`), it is **vendored** as pinned source under `vendor/`, not added as an npm dependency — see [the vendoring decision](../rfc/implemented/process/2026-06-11-vendor-cordis-as-source.md) for why. [vendor/README.md](../../vendor/README.md) covers *updating* an already-vendored package; this guide is the file-by-file checklist for adding a **new** one. (Verified against the existing vendored set; if it drifts, fix it here.) +When the harness needs another upstream Cordis package (e.g. `@cordisjs/plugin-http`), it is **vendored** as pinned source under `vendor/`, not added as an npm dependency — see [the vendoring decision](../../.agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md) for why. [vendor/README.md](../../vendor/README.md) covers *updating* an already-vendored package; this guide is the file-by-file checklist for adding a **new** one. (Verified against the existing vendored set; if it drifts, fix it here.) ## 1. Copy the source in diff --git a/docs/cookbook/adding-a-vendored-package.zh.md b/docs/cookbook/adding-a-vendored-package.zh.md index 86b1e6c959..7245682ef8 100644 --- a/docs/cookbook/adding-a-vendored-package.zh.md +++ b/docs/cookbook/adding-a-vendored-package.zh.md @@ -2,7 +2,7 @@ [English](adding-a-vendored-package.md) | 中文 -当 harness 需要引入另一个上游 Cordis 包(如 `@cordisjs/plugin-http`)时,应将其作为固定版本的源码 **vendor** 到 `vendor/` 下,而非作为 npm 依赖添加——原因见[vendoring 决策](../rfc/implemented/process/2026-06-11-vendor-cordis-as-source.md)。[vendor/README.md](../../vendor/README.md) 介绍如何*更新*已有的 vendored 包;本指南是添加**新** vendored 包的逐文件清单。(已对照现有 vendored 集合验证;如有偏差,请在此修正。) +当 harness 需要引入另一个上游 Cordis 包(如 `@cordisjs/plugin-http`)时,应将其作为固定版本的源码 **vendor** 到 `vendor/` 下,而非作为 npm 依赖添加——原因见[vendoring 决策](../../.agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md)。[vendor/README.md](../../vendor/README.md) 介绍如何*更新*已有的 vendored 包;本指南是添加**新** vendored 包的逐文件清单。(已对照现有 vendored 集合验证;如有偏差,请在此修正。) ## 1. 复制源码 diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml index 313cb8489b..5b7f226916 100644 --- a/docs/cookbook/extension-cookbook.i18n.yaml +++ b/docs/cookbook/extension-cookbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -extension-cookbook.md: c6bf6ddd4bf0da8bd7377ab57e779750b72bd25e -extension-cookbook.zh.md: dcc188b7d3ac44f5d86253c20147e95da0bea648 +extension-cookbook.md: 37793e4e76bf5171c759ca78be473912101bd9f4 +extension-cookbook.zh.md: 8f170f225b55721c78ef27c0e87e481b5cb00f64 diff --git a/docs/cookbook/extension-cookbook.md b/docs/cookbook/extension-cookbook.md index c6bf6ddd4b..37793e4e76 100644 --- a/docs/cookbook/extension-cookbook.md +++ b/docs/cookbook/extension-cookbook.md @@ -4,11 +4,11 @@ English | [中文](extension-cookbook.zh.md) > FIXME: This important guide has not received sufficient human design review; complete that review before the first release. -The three plugin shapes you write against the harness extension surface, as illustrative snippets (elided imports and helper stubs — not copy-paste-complete). For the full step-by-step guides see [adding a package](./adding-a-package.md), [adding a tool](./adding-a-tool.md), and [adding an LLM adapter](./adding-an-llm-adapter.md); for the seams these hook into see [docs/architecture.md](../architecture.md). +The three plugin shapes you write against the harness extension surface, as illustrative snippets (elided imports and helper stubs — not copy-paste-complete). For the full step-by-step guides see [adding a package](adding-a-package.md), [adding a tool](adding-a-tool.md), and [adding an LLM adapter](adding-an-llm-adapter.md); for the seams these hook into see [docs/architecture.md](../architecture.md). ## A tool plugin -A tool registers on `ctx.tools`. The annotated `defineTool` example (typed `execute` args, result shaping, the `run_in_background` pattern) lives in [adding-a-tool.md](./adding-a-tool.md) — that guide is the source of truth for the tool shape. Raw JSON-Schema `ToolDefinition`s are also accepted by `ctx.tools.register()` directly (that is how MCP-sourced tools arrive); `defineTool` is the typed sugar for first-party tools. +A tool registers on `ctx.tools`. The annotated `defineTool` example (typed `execute` args, result shaping, the `run_in_background` pattern) lives in [adding-a-tool.md](adding-a-tool.md) — that guide is the source of truth for the tool shape. Raw JSON-Schema `ToolDefinition`s are also accepted by `ctx.tools.register()` directly (that is how MCP-sourced tools arrive); `defineTool` is the typed sugar for first-party tools. ## A hook plugin (permission-gate example) @@ -32,7 +32,7 @@ export function apply(ctx: Context) { } ``` -This waterfall is the reorderable policy layer. Use `ctx.tools.guard()` when an invariant needs a monotonic final denial, `tools/execute` when a plugin must wrap the actual dispatch lifetime (timeouts/retries/metrics; only `exec.signal` is replaceable), `tools/post-execute` for explicit result transformation, and `tools/result` for contained observation of the immutable final outcome. The [adding-a-tool guide](./adding-a-tool.md#execution-policy-and-observation) gives the selection rule. +This waterfall is the reorderable policy layer. Use `ctx.tools.guard()` when an invariant needs a monotonic final denial, `tools/execute` when a plugin must wrap the actual dispatch lifetime (timeouts/retries/metrics; only `exec.signal` is replaceable), `tools/post-execute` for explicit result transformation, and `tools/result` for contained observation of the immutable final outcome. The [adding-a-tool guide](adding-a-tool.md#execution-policy-and-observation) gives the selection rule. ## A UI plugin @@ -91,7 +91,7 @@ Six runnable leaves load their plugin trees from `cordis.yml`: [`examples/echo-a ## The feature → mechanism map -Every product feature maps to a listener on a documented extension seam — the microkernel claim made checkable ([microkernel RFC](../rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md)). No row modifies the loop. +Every product feature maps to a listener on a documented extension seam — the microkernel claim made checkable ([microkernel Agent Note](../../.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md)). No row modifies the loop. `system-prompt/assemble` is an expert cooperative whole-assembly transform: its returned assembly is authoritative, so listener authors own preserving active Code Mode and structured-output protocol contributions. Prefer `ctx.tools.restrict()` for tool filtering that must stay aligned across presentation, lookup, and execution. @@ -102,7 +102,7 @@ Every product feature maps to a listener on a documented extension seam — the | `/loop` | on the `turn/end` session event, `send()` the next iteration; or force-continue | | Dynamic workflow | `ctx.workflows` + the worker-thread engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and terminal `agent/turn-stop` | | Queued + steering messages | core `Agent.send()` / `Agent.steer()` | -| Context compaction (auto + manual) | the `ctx.compact` seam + `dsh-compact-basic`; automatic pressure runs on serial `agent/post-step`, canonical overflow recovery runs on `agent/request-error`, and manual callers use the same compact service ([compaction RFC](../rfc/implemented/feature/2026-06-18-compaction-capability-seam.md) — the model-facing `/compact` consumer tool is deferred) | +| Context compaction (auto + manual) | the `ctx.compact` seam + `dsh-compact-basic`; automatic pressure runs on serial `agent/post-step`, canonical overflow recovery runs on `agent/request-error`, and manual callers use the same compact service ([compaction Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) — the model-facing `/compact` consumer tool is deferred) | | System prompt configurability | `ctx.systemPrompt.section()` with ordering and scope-local shadowing | | AGENTS.md (root) | a section provider reading the file | | AGENTS.md (subdir, on-touch) + file-change notices | `agent.inject()` from a watcher / tool-result listener | diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md index dcc188b7d3..8f170f225b 100644 --- a/docs/cookbook/extension-cookbook.zh.md +++ b/docs/cookbook/extension-cookbook.zh.md @@ -4,11 +4,11 @@ > FIXME:这篇重要指南尚未经过充分的人工设计审查;请在首次发布前完成审查。 -针对 harness 扩展表面编写的三种插件形态,以示意性代码片段呈现(省略了 import 和辅助桩——不可直接复制运行)。完整的分步指南见[添加包(package)](./adding-a-package.md)、[添加工具](./adding-a-tool.md)和[添加 LLM(大语言模型)适配器](./adding-an-llm-adapter.md);这些插件所挂接的 seam 见 [docs/architecture.md](../architecture.md)。 +针对 harness 扩展表面编写的三种插件形态,以示意性代码片段呈现(省略了 import 和辅助桩——不可直接复制运行)。完整的分步指南见[添加包(package)](adding-a-package.md)、[添加工具](adding-a-tool.md)和[添加 LLM(大语言模型)适配器](adding-an-llm-adapter.md);这些插件所挂接的 seam 见 [docs/architecture.md](../architecture.md)。 ## 工具插件 -工具在 `ctx.tools` 上注册。带注解的 `defineTool` 示例(类型化的 `execute` 参数、结果塑形、`run_in_background` 模式)见 [adding-a-tool.md](./adding-a-tool.md)——该指南是工具形态的真源。`ctx.tools.register()` 也直接接受原始 JSON-Schema `ToolDefinition`(MCP 来源的工具就是这样到达的);`defineTool` 是为第一方工具提供的类型化语法糖。 +工具在 `ctx.tools` 上注册。带注解的 `defineTool` 示例(类型化的 `execute` 参数、结果塑形、`run_in_background` 模式)见 [adding-a-tool.md](adding-a-tool.md)——该指南是工具形态的真源。`ctx.tools.register()` 也直接接受原始 JSON-Schema `ToolDefinition`(MCP 来源的工具就是这样到达的);`defineTool` 是为第一方工具提供的类型化语法糖。 ## 钩子插件(以权限门禁为例) @@ -32,7 +32,7 @@ export function apply(ctx: Context) { } ``` -这个 waterfall(瀑布式事件)是可重排的策略层。当不变式需要单调的最终拒绝时使用 `ctx.tools.guard()`;当插件需要包裹实际分发生命周期时(超时/重试/指标;仅 `exec.signal` 可替换)使用 `tools/execute`;显式结果变换使用 `tools/post-execute`;对不可变最终结果的受限观察使用 `tools/result`。选择规则见[添加工具指南](./adding-a-tool.md#execution-policy-and-observation)。 +这个 waterfall(瀑布式事件)是可重排的策略层。当不变式需要单调的最终拒绝时使用 `ctx.tools.guard()`;当插件需要包裹实际分发生命周期时(超时/重试/指标;仅 `exec.signal` 可替换)使用 `tools/execute`;显式结果变换使用 `tools/post-execute`;对不可变最终结果的受限观察使用 `tools/result`。选择规则见[添加工具指南](adding-a-tool.md#execution-policy-and-observation)。 ## UI 插件 @@ -91,7 +91,7 @@ export function apply(ctx: Context) { ## 功能→机制映射 -每个产品功能都映射到一个文档化扩展 seam 上的监听器——微内核声明由此可验证([微内核 RFC](../rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md))。没有任何一行修改循环本身。 +每个产品功能都映射到一个文档化扩展 seam 上的监听器——微内核声明由此可验证([微内核 Agent Note](../../.agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md))。没有任何一行修改循环本身。 `system-prompt/assemble` 是一个专家协作式的整体装配变换:其返回的装配结果具有权威性,因此监听器作者有责任保留活跃的 Code Mode 和结构化输出协议的贡献。对于需要在展示、查找和执行之间保持对齐的工具过滤,优先使用 `ctx.tools.restrict()`。 @@ -102,7 +102,7 @@ export function apply(ctx: Context) { | `/loop` | 在 `turn/end` 会话事件上 `send()` 下一次迭代;或强制继续 | | 动态工作流 | `ctx.workflows` + worker-thread 引擎 + `workflow` 工具;结构化的进程内子任务通过作用域化的 prompt/工具注册、单调工具守卫、最终 `tools/result` 提交(包括外层 `run_code`)和终端 `agent/turn-stop` 来强制输出 | | 排队消息 + steering(中途引导) | 核心 `Agent.send()` / `Agent.steer()` | -| 上下文压缩(context compaction)(自动 + 手动) | `ctx.compact` seam + `dsh-compact-basic`;自动压力检查运行在串行 `agent/post-step`,规范化溢出恢复运行在 `agent/request-error`,手动调用方使用同一个压缩服务([压缩 RFC](../rfc/implemented/feature/2026-06-18-compaction-capability-seam.md)——面向模型的 `/compact` 消费方工具已推迟) | +| 上下文压缩(context compaction)(自动 + 手动) | `ctx.compact` seam + `dsh-compact-basic`;自动压力检查运行在串行 `agent/post-step`,规范化溢出恢复运行在 `agent/request-error`,手动调用方使用同一个压缩服务([压缩 Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)——面向模型的 `/compact` 消费方工具已推迟) | | 系统提示词可配置性 | `ctx.systemPrompt.section()`,支持排序与作用域局部覆盖 | | AGENTS.md(根目录) | 一个读取该文件的 section provider | | AGENTS.md(子目录,按需触发)+ 文件变更通知 | 从 watcher / tool-result 监听器调用 `agent.inject()` | diff --git a/docs/cookbook/maintaining-dsh-code-review.md b/docs/cookbook/maintaining-dsh-code-review.md index 5a6c2df120..8af449b749 100644 --- a/docs/cookbook/maintaining-dsh-code-review.md +++ b/docs/cookbook/maintaining-dsh-code-review.md @@ -1,6 +1,6 @@ # Maintaining the dsh-code-review skill -The [`dsh-code-review`](../../.agents/skills/dsh-code-review/SKILL.md) skill is kept current by a single designated operator running a private periodic maintenance tool. This cookbook is the entry point for that operator — and for anyone taking over the role — and for repo contributors who want to understand why skill updates arrive as small periodic PRs rather than one-off audits. The workflow itself is specified in the [human-review skill-maintenance RFC](../rfc/proposed/process/2026-07-13-human-review-skill-maintenance.md). +The [`dsh-code-review`](../../.agents/skills/dsh-code-review/SKILL.md) skill is kept current by a single designated operator running a private periodic maintenance tool. This cookbook is the entry point for that operator — and for anyone taking over the role — and for repo contributors who want to understand why skill updates arrive as small periodic PRs rather than one-off audits. The workflow itself is specified in the [human-review skill-maintenance Agent Note](../../.agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md). ## What the maintainer receives @@ -55,8 +55,8 @@ The mechanism lives on one machine. Interruptions the operator handles as they a - **Daily run missed.** The two-day overlap window catches one skipped day automatically; longer gaps recover by running the wrapper manually with `DSH_CODE_REVIEW_SINCE=<Nd>`. Overlapping windows are idempotent: guidance already in the current skill is classified `covered` and does not re-enter as a candidate. - **Adapter provider outage.** The tool refuses to run when the two reviewer commands resolve to byte-identical executables. A single batch whose adapter response fails schema or id validation is failed closed at the batch level (every item in the batch marked unclear) and the run continues; the raw output is preserved for debugging. If either adapter produces no valid result for any nonempty batch in an operation, the run fails, writes a failure record, and notifies the operator; it never collapses a total-provider outage into "no candidate." -- **Handoff to another maintainer.** Open a follow-up RFC that supersedes the current one: either move the mechanism into the repository or record the new operator's private setup. Do not silently transfer the tool — the "single-maintainer bus factor" in the RFC's Risks section is the reason the handoff needs a documented decision. +- **Handoff to another maintainer.** Open a follow-up Agent Note that supersedes the current one: either move the mechanism into the repository or record the new operator's private setup. Do not silently transfer the tool — the "single-maintainer bus factor" in the Agent Note's Risks section is the reason the handoff needs a documented decision. ## Where the operator's private setup lives -The tool source, reviewer adapters, provider credentials, and scheduler are the operator's private infrastructure and are outside this repository by design (see the RFC's "Where the mechanism lives" section). This cookbook and the RFC describe **what the workflow guarantees**; **how** those guarantees are implemented is a private-infrastructure concern. If you are the new operator, the RFC's `## Proposal` sections are the specification you build against. +The tool source, reviewer adapters, provider credentials, and scheduler are the operator's private infrastructure and are outside this repository by design (see the Agent Note's "Where the mechanism lives" section). This cookbook and the Agent Note describe **what the workflow guarantees**; **how** those guarantees are implemented is a private-infrastructure concern. If you are the new operator, the Agent Note's `## Proposal` sections are the specification you build against. 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 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> +<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand. + Run `pnpm run gen-cordis-catalog` to regenerate. --> # 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<Property.Accessor, 'type'>): 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<T extends {}>(source: T, mixins: (keyof this & keyof T)[] | Dict<string>): ``` 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 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> +<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand. + Run `pnpm run gen-cordis-catalog` to regenerate. --> # 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<K extends keyof Events>(thisArg: NoInfer<ThisType<Events[K]>>, 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 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> +<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand. + Run `pnpm run gen-cordis-catalog` to regenerate. --> # 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<Promise<void>> ``` 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<void> ``` 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<Impl> | 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<void> | 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<Promise<void>> ``` 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<T = any> = | AsyncEffect<T> ``` -[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 = any> = () => 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 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> +<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand. + Run `pnpm run gen-cordis-catalog` to regenerate. --> # 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<void>): Fiber & PromiseLike<Fiber ``` Run a callback once the requested services are available. + Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is unloaded and re-run whenever a required service changes. - `deps` — required services, as an array or a name → config map. @@ -28,11 +30,11 @@ Shorthand for `ctx.plugin({ inject, apply: callback })`: the callback is unloade **Returns** the fiber; awaiting it settles once loading finished. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L175) +[Source](../../../vendor/cordis/src/registry.ts#L175) ### ctx.plugin(plugin, ...args) -```ts website-api +```ts cordis-catalog /** * Load a plugin in the current context. * @@ -51,13 +53,13 @@ Load a plugin in the current context. **Returns** the fiber; awaiting it settles once loading finished (rejecting on config or startup errors). -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/vendor/cordis/src/registry.ts#L184) +[Source](../../../vendor/cordis/src/registry.ts#L184) ## Plugin Supported plugin entrypoint shapes. -```ts website-api +```ts cordis-catalog /** Supported plugin entrypoint shapes. */ type Plugin<T = any> = | Plugin.Function<T> @@ -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 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> +<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand. + Run `pnpm run gen-cordis-catalog` to regenerate. --> # Service -Base class for context services: subclass it and load the subclass as a plugin to register `ctx.<name>`. +The base class for context services. A subclass loaded as a plugin registers itself as `ctx.<name>`. 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 7d7472a33e..ae17230a7a 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) ## `llm/*` @@ -463,7 +463,7 @@ Waterfall around every streaming model call (retry, replay, routing). Bound to t * adapter's stream, or yield your own chunks to short-circuit. * @param options - the full request. A LOOP-built request arrives * deep-frozen (mutation throws): its content is a pure function of the - * session log (the reconstructability RFC), so listeners read it, never + * session log (the reconstructability Agent Note), so listeners read it, never * rewrite it. A hand-built one-shot (compaction summarize) is the * caller's own object and stays mutable here. * @mode waterfall diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 10cd0bbf05..d1cd96afa3 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -7,7 +7,7 @@ Every `ctx.<key>` 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` @@ -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<FsDirEntry[]> * @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<FsWriteOutcome> +abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxMode?: SandboxMode, ): Promise<FsWriteOutcome> /** * 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<FsEditOutcome> +abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxMode?: SandboxMode, ): Promise<FsEditOutcome> ``` -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.llm` — `LlmService` @@ -569,7 +575,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) @@ -592,7 +598,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) @@ -820,7 +832,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:549`](../../packages/core/session/src/index.ts) ## `ctx.skills` — `SkillService` @@ -1108,6 +1120,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 060d249eb7..2070c464cf 100644 --- a/docs/core-data-structures/bash.md +++ b/docs/core-data-structures/bash.md @@ -105,7 +105,7 @@ interface BashExecSpec { } ``` -`stdin` and `env` are trusted in-process plugin inputs and are not exposed by `dsh-tool-bash`. The local executor scrubs ambient credentials before merging explicit caller-supplied env. See [the bash-stdin-env RFC](../rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md). +`stdin` and `env` are trusted in-process plugin inputs and are not exposed by `dsh-tool-bash`. The local executor scrubs ambient credentials before merging explicit caller-supplied env. See [the bash-stdin-env Agent Note](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md). `stdoutMaxBytes` is also trusted-plugin-only. It lets a foreground consumer request complete stdout up to a bounded parser budget without changing stderr, background tasks, or the model-facing bash tool's ordinary output cap. @@ -125,7 +125,7 @@ interface BashRunResult { * short. Mutually exclusive with {@link aborted}: one fused deadline drives * both the timeout and the caller's cancellation, so a timeout and an abort * racing before process close report the single first-abort cause, not both - * (see the [timeout-library RFC](../../../../docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md)). + * (see the [timeout-library Agent Note](../../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)). */ timedOut: boolean /** @@ -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. @@ -181,7 +181,7 @@ interface BashSandboxInfo { } ``` -One more piece completes the vocabulary: the `SANDBOX_UNAVAILABLE` error code (owned by the [sandbox seam](sandbox.md)) is what the `ctx.sandbox` provider throws — and the executor propagates — when a confined mode has no usable backend. A selected runner refusing its profile reaches the same fail-closed foreground error; a settled background task records `runnerFailed`. The model receives denial/runner facts in results, learns the effective mode only when a denial marker names it, and can request a one-shot strictly wider retry through `sandbox_permissions` plus `justification`; `ctx.approval` must grant that exact call before anything executes. The complete policy and switching design is the [sandbox RFC](../rfc/implemented/feature/2026-07-06-sandbox.md). +One more piece completes the vocabulary: the `SANDBOX_UNAVAILABLE` error code (owned by the [sandbox seam](sandbox.md)) is what the `ctx.sandbox` provider throws — and the executor propagates — when a confined mode has no usable backend. A selected runner refusing its profile reaches the same fail-closed foreground error; a settled background task records `runnerFailed`. The model receives denial/runner facts in results, learns the effective mode only when a denial marker names it, and can request a one-shot strictly wider retry through `sandbox_permissions` plus `justification`; `ctx.approval` must grant that exact call before anything executes. The complete policy and switching design is the [sandbox Agent Note](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). ## Background processes: `BashProcess` diff --git a/docs/core-data-structures/code-runtime.md b/docs/core-data-structures/code-runtime.md index cea69180af..af3fdbc649 100644 --- a/docs/core-data-structures/code-runtime.md +++ b/docs/core-data-structures/code-runtime.md @@ -1,6 +1,6 @@ # Code Runtime -The code-execution seam — a [capability seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md) whose interface ([dsh-code-runtime](../../packages/code-runtime/code-runtime), `ctx.codeRuntime`) runs one model-written program against host-provided async bindings and reports what it printed and returned. Code execution is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). Backends differ by execution substrate and source language, both readonly descriptors on the service; the worker-thread backend and the tool-registry consumer (Code Mode) are specified in the [Code Mode RFC](../rfc/implemented/feature/2026-06-15-code-mode.md). +The code-execution seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) whose interface ([dsh-code-runtime](../../packages/code-runtime/code-runtime), `ctx.codeRuntime`) runs one model-written program against host-provided async bindings and reports what it printed and returned. Code execution is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). Backends differ by execution substrate and source language, both readonly descriptors on the service; the worker-thread backend and the tool-registry consumer (Code Mode) are specified in the [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md). Source: [`packages/code-runtime/code-runtime/src/types.ts`](../../packages/code-runtime/code-runtime/src/types.ts) diff --git a/docs/core-data-structures/compaction.md b/docs/core-data-structures/compaction.md index f6cdfd5b8b..bb302ff52b 100644 --- a/docs/core-data-structures/compaction.md +++ b/docs/core-data-structures/compaction.md @@ -1,17 +1,17 @@ # Compaction -The compaction seam — a [capability seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md) split like bash: interface ([dsh-compact](../../packages/compact/compact), `ctx.compact`), implementation (a backend such as [dsh-compact-basic](../../packages/compact/compact-basic)), and consumer (a `/compact` tool, deferred). Compaction is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A tokenizer- or template-based backend is a sibling package implementing the same interface. Unlike bash, the interface necessarily depends on `dsh-session` and `dsh-llm`: its verbs act on an agent-owned `Session`, and its durable summary event uses the `ContentBlock` vocabulary (see the [compaction capability-seam RFC](../rfc/implemented/feature/2026-06-18-compaction-capability-seam.md)). +The compaction seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) split like bash: interface ([dsh-compact](../../packages/compact/compact), `ctx.compact`), implementation (a backend such as [dsh-compact-basic](../../packages/compact/compact-basic)), and consumer (a `/compact` tool, deferred). Compaction is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A tokenizer- or template-based backend is a sibling package implementing the same interface. Unlike bash, the interface necessarily depends on `dsh-session` and `dsh-llm`: its verbs act on an agent-owned `Session`, and its durable summary event uses the `ContentBlock` vocabulary (see the [compaction capability-seam Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)). Source: [`packages/compact/compact/src/types.ts`](../../packages/compact/compact/src/types.ts) ## 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 RFC 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 | |---|---|---| | `compact/start` | `{ turn }` | acquires the log-recorded lock | -| `compact/summary` | `{ summary, shadowedRange, shadowedSeqs, shadowedTokenCount, provider, model, maxTokens? }` | provenance: the summary blocks, the shadowed surface-boundary pair (`start`/`end` seqs — a position span, not a numeric interval), the shadowed seqs in surface order, the estimated token count, and the summarize call's envelope (`provider`, `model`, plus its generation cap when one applied) — logged so the one-shot request is reconstructable from log + code (the reconstructability RFC) | +| `compact/summary` | `{ summary, shadowedRange, shadowedSeqs, shadowedTokenCount, provider, model, maxTokens? }` | provenance: the summary blocks, the shadowed surface-boundary pair (`start`/`end` seqs — a position span, not a numeric interval), the shadowed seqs in surface order, the estimated token count, and the summarize call's envelope (`provider`, `model`, plus its generation cap when one applied) — logged so the one-shot request is reconstructable from log + code (the reconstructability Agent Note) | | `compact/end` | `{ turn, error? }` | releases the lock (`error` set when summarization threw) | The lock brackets the **whole** operation: `compact/start` is appended first, then summarization, the `compact/summary` provenance record, and the `user/message` replacement all land, and only then `compact/end`. Releasing the lock last turns a crash mid-operation into a detectable orphaned lock (a `compact/start` with no matching `compact/end`) rather than a `compact/end` that falsely claims compaction finished. @@ -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 1d45f3814a..6adee2e281 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -264,7 +264,7 @@ The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition` ### The request envelope: `LlmCallConfig` and the logged header -The loop builds each request from logged state. `EpochHeader` records call config, rendered prompt, authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset), and session prefix through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability RFC](../rfc/implemented/architecture/2026-07-05-reconstructable-requests.md). +The loop builds each request from logged state. `EpochHeader` records call config, rendered prompt, authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset), and session prefix through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md). `agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, or sampling. `agent/session-prefix` composes request-only prefix messages once per loop instance, and the header records the exact result used. Requests reaching `llm/stream` are deep-frozen, so mutation throws. @@ -338,13 +338,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 } @@ -362,15 +360,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 @@ -384,10 +387,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 @@ -397,17 +401,17 @@ 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. ## Initiating Agent -The process-local initiator carried by `ctx.agents` is the exact `Agent` above, not a separate frame or copied identity. Ambient presence is neither liveness proof nor authorization; the [initiator-scope decision](../rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns its lifetime and boundary rules. +The process-local initiator carried by `ctx.agents` is the exact `Agent` above, not a separate frame or copied identity. Ambient presence is neither liveness proof nor authorization; the [initiator-scope decision](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns its lifetime and boundary rules. ## 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) @@ -416,27 +420,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/llm-streaming.md b/docs/core-data-structures/llm-streaming.md index 3526687f3e..41ce6427e6 100644 --- a/docs/core-data-structures/llm-streaming.md +++ b/docs/core-data-structures/llm-streaming.md @@ -46,7 +46,7 @@ This contract was pinned down by two deliberately independent implementations: ` ## `AppIdentity` — app attribution -The static public application identity every adapter sends to providers ([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts)). `attributionHeaders(identity?)` maps it to the standard `User-Agent` header only; OpenRouter-specific app attribution headers are intentionally not supported by this contract. The default `APP_IDENTITY` sources its version from the package manifest; every field is a public product fact - no secrets, paths, session ids, or per-user identifiers, and nothing per-request may influence the values. Rationale: [Mandatory `User-Agent` attribution](../rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md). +The static public application identity every adapter sends to providers ([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts)). `attributionHeaders(identity?)` maps it to the standard `User-Agent` header only; OpenRouter-specific app attribution headers are intentionally not supported by this contract. The default `APP_IDENTITY` sources its version from the package manifest; every field is a public product fact - no secrets, paths, session ids, or per-user identifiers, and nothing per-request may influence the values. Rationale: [Mandatory `User-Agent` attribution](../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md). ```ts type-equiv /** diff --git a/docs/core-data-structures/persistence.md b/docs/core-data-structures/persistence.md index 72ebcc5852..18ef172ef8 100644 --- a/docs/core-data-structures/persistence.md +++ b/docs/core-data-structures/persistence.md @@ -2,11 +2,11 @@ The **durability seam** for the event log. [session.md](session.md) describes the in-memory `Session` — the append-only `SessionEvent` log that is the source of truth. This page describes how that log is made durable: the abstract `SessionPersistence` service, its backends, the flush checkpoint, crash recovery, and the metadata header that travels alongside the log. The event vocabulary the log carries is enumerated, member by member, in the generated [persistence log event catalog](../persistence-catalog.md). -The seam is a textbook [capability seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md): one abstract service ([dsh-session-persistence](../../packages/session-persistence/session-persistence), `ctx.sessionPersistence`) defining locate/create/append/load/list over the existing `SessionEvent` — **no parallel persisted type** — and two interchangeable backends that pass the same `runPersistenceContract` suite. See the [session-persistence RFC](../rfc/implemented/architecture/2026-06-14-session-persistence.md). +The seam is a textbook [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md): one abstract service ([dsh-session-persistence](../../packages/session-persistence/session-persistence), `ctx.sessionPersistence`) defining locate/create/append/load/list over the existing `SessionEvent` — **no parallel persisted type** — and two interchangeable backends that pass the same `runPersistenceContract` suite. See the [session-persistence Agent Note](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md). ## 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 @@ -98,4 +98,4 @@ Both implement the same abstract `SessionPersistence` (locate/create/append/load - **[dsh-session-persistence-jsonl](../../packages/session-persistence/session-persistence-jsonl)** — an append-only JSONL log per session with crash-safe atomic writes, the interrupted-turn crash recovery above, and a read/replay path. - **[dsh-session-persistence-sqlite](../../packages/session-persistence/session-persistence-sqlite)** — `node:sqlite`, one row per `SessionEvent`. The row shape `(session_id, seq, type, time, data, source_event_seqs, surface_op)` maps 1:1 onto the event, including optional surface metadata, so there is no parallel persisted schema to keep in sync. -Multiple backends sharing one on-disk session coordinate writes through the [shared persistence write-coordinator](../rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md). +Multiple backends sharing one on-disk session coordinate writes through the [shared persistence write-coordinator](../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md). diff --git a/docs/core-data-structures/scope.md b/docs/core-data-structures/scope.md index d79f464b3e..93e6d76598 100644 --- a/docs/core-data-structures/scope.md +++ b/docs/core-data-structures/scope.md @@ -1,6 +1,6 @@ # Scoped Registration -The [scope package](../../packages/core/scope) supplies the identity and carrier vocabulary that makes one registration context mean both per-agent visibility and shared lifetime ownership. It is a library primitive rather than a Cordis service; the [agent-scope runtime-design RFC](../rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#scope-routing-one-opaque-key-selects-one-layer) owns the implementation rationale, while the package [README](../../packages/core/scope/README.md) owns the callable API and filtering semantics. +The [scope package](../../packages/core/scope) supplies the identity and carrier vocabulary that makes one registration context mean both per-agent visibility and shared lifetime ownership. It is a library primitive rather than a Cordis service; the [agent-scope runtime-design Agent Note](../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#scope-routing-one-opaque-key-selects-one-layer) owns the implementation rationale, while the package [README](../../packages/core/scope/README.md) owns the callable API and filtering semantics. Source: [`packages/core/scope/src/index.ts`](../../packages/core/scope/src/index.ts). diff --git a/docs/core-data-structures/session.md b/docs/core-data-structures/session.md index 3668191fb2..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. */ @@ -101,7 +96,7 @@ interface SessionEventMap { ### `TodoItem` — one todo-list entry -The unit of the `todo/write` event's whole-list snapshot. Deliberately minimal — a `content` line and a three-state `status` (no id, priority, or `activeForm`): the list is replaced wholesale on every write, so entries need no stable identity, and the status triple is exactly the ACP `PlanEntryStatus`, so a UI bridge can map a todo list onto an ACP `plan` 1:1 (synthesizing the priority ACP additionally requires). See the [todo_write RFC](../rfc/implemented/feature/2026-06-29-todo-write-tool.md). +The unit of the `todo/write` event's whole-list snapshot. Deliberately minimal — a `content` line and a three-state `status` (no id, priority, or `activeForm`): the list is replaced wholesale on every write, so entries need no stable identity, and the status triple is exactly the ACP `PlanEntryStatus`, so a UI bridge can map a todo list onto an ACP `plan` 1:1 (synthesizing the priority ACP additionally requires). See the [todo_write Agent Note](../../.agents/notes/implemented/feature/2026-06-29-todo-write-tool.md). ```ts type-equiv /** @@ -125,7 +120,7 @@ interface TodoItem { ### The request header event: `request/header` -The request envelope — the `EpochHeader` (call config + rendered system prompt + assembled tool schemas + the session prefix) — is logged session state, so every conversation request is a pure function of the log (the reconstructability RFC). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message. +The request envelope — the `EpochHeader` (call config + rendered system prompt + assembled tool schemas + the session prefix) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message. ```ts type-equiv /** @@ -200,7 +195,7 @@ For `assistant/message`, a present `sourceEventSeqs: []` is a complete known-emp ## Surface types -The five message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`) carry surface metadata declaring how they join the ordered derived surface. See the [session surface RFC](../rfc/implemented/architecture/2026-06-18-session-surface.md). +The five message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`) carry surface metadata declaring how they join the ordered derived surface. See the [session surface Agent Note](../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md). ### `SurfaceEventType` — the message-producing subset of event types @@ -414,7 +409,7 @@ declare class Session { * The per-node pure function {@link deriveMessages} folds over the surface; * an external reconstructor (or the dev invariant) folds the same function * over a log prefix's surface to rebuild the exact messages any request was - * built from (the reconstructability RFC). The returned message wrapper is + * built from (the reconstructability Agent Note). The returned message wrapper is * fresh; its content reuses the logged event's already deep-frozen durable * data, so changing the wrapper cannot rewrite the log and changing content * throws. @@ -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 `<context source="…">…</context>`; `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 `<steering source="…">…</steering>` 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,17 +493,17 @@ 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 -Every session event lives **inside** a turn (between a `turn/start` and its `turn/end`). The loop appends queued `user/message` events *after* `turn/start`, and an idle `agent.inject()` wraps its `context/message` in a one-shot `injection` turn. This makes the turn the single durability/replay boundary: a backend can treat anything after the last `turn/end` as an interrupted-crash tail without risking the loss of legitimately-recorded between-turn context. The `dsh-invariants` plugin enforces it in dev (a message event outside an open turn throws). See [the turn-enclosure invariant RFC](../rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md). +Every session event lives **inside** a turn (between a `turn/start` and its `turn/end`). The loop appends queued `user/message` events *after* `turn/start`, and an idle `agent.inject()` wraps its `context/message` in a one-shot `injection` turn. This makes the turn the single durability/replay boundary: a backend can treat anything after the last `turn/end` as an interrupted-crash tail without risking the loss of legitimately-recorded between-turn context. The `dsh-invariants` plugin enforces it in dev (a message event outside an open turn throws). See [the turn-enclosure invariant Agent Note](../../.agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md). ## Plugin-contributed log-only events A plugin may declaration-merge extra `SessionEventMap` types. These are **log-only**: NOT `SurfaceEventType`s (they carry no `surfaceOp` and contribute nothing to derived history), but, like every event, they must sit inside an open turn. The full per-event enumeration — core and plugin-contributed alike, with payloads and provenance — is the generated [persistence log event catalog](../persistence-catalog.md); the compaction seam's `compact/*` semantics are discussed on [compaction.md](compaction.md). -The hook bridges' `hook/invoked` / `hook/result` provenance pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. The mid-turn hook points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record — its injected `context/message` is the durable evidence — because it has no open turn to enclose one (see [the hook-bridges RFC](../rfc/implemented/feature/2026-06-30-hook-bridges.md)). +The hook bridges' `hook/invoked` / `hook/result` provenance pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. The mid-turn hook points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record — its injected `context/message` is the durable evidence — because it has no open turn to enclose one (see [the hook-bridges Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md)). ## Durability contract diff --git a/docs/core-data-structures/skills.md b/docs/core-data-structures/skills.md index daccb38550..e367adf006 100644 --- a/docs/core-data-structures/skills.md +++ b/docs/core-data-structures/skills.md @@ -152,6 +152,6 @@ interface Config { ## Session catalog and tool contract -`dsh-tool-skill` contributes a user-role `<system-reminder>` through `agent/session-prefix`. The catalog contains sorted skill `name` and normalized, XML-escaped `description` only; it omits bodies, paths, sources, providers, and routing hints. Prefix discovery forwards the caller's abort signal through `SkillLookupOptions`. `catalogDescriptionMaxLength` is the consumer config for the description bound, with default `500` and integer minimum `3`. Its request-only, header-logged lifecycle is defined by the [session-prefix RFC](../rfc/implemented/feature/2026-07-07-session-prefix.md). +`dsh-tool-skill` contributes a user-role `<system-reminder>` through `agent/session-prefix`. The catalog contains sorted skill `name` and normalized, XML-escaped `description` only; it omits bodies, paths, sources, providers, and routing hints. Prefix discovery forwards the caller's abort signal through `SkillLookupOptions`. `catalogDescriptionMaxLength` is the consumer config for the description bound, with default `500` and integer minimum `3`. Its request-only, header-logged lifecycle is defined by the [session-prefix Agent Note](../../.agents/notes/implemented/feature/2026-07-07-session-prefix.md). The model-facing `skill({ name })` tool validates the kebab-case name, loads the complete definition for the calling agent cwd, reports an unresolved skill as unknown or no longer available, rejects `disableModelInvocation` skills, and returns a tool result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`. `resourceBase` resolves explicitly referenced scripts, references, and assets only as needed; the loaded result does not enumerate a skill directory. The tool result is the model-visible path for complete instructions. diff --git a/docs/core-data-structures/spill.md b/docs/core-data-structures/spill.md index d046874b0e..a964064912 100644 --- a/docs/core-data-structures/spill.md +++ b/docs/core-data-structures/spill.md @@ -1,6 +1,6 @@ # Spill Storage -The spill storage seam — a [capability seam](../rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md) that persists a tool's oversized text and returns a model-facing locator plus retrieval guidance, split across packages: interface ([dsh-spill](../../packages/spill/spill), `ctx.spillStore`), implementation ([dsh-spill-local](../../packages/spill/spill-local), private session-scoped files on the host filesystem), and consumer ([dsh-spill-policy](../../packages/spill/spill-policy), the `tools/post-execute` policy). Spill is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). Preview mechanics stay in [dsh-retention](../../packages/util/retention); this seam only saves the final text the policy hands it. +The spill storage seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md) that persists a tool's oversized text and returns a model-facing locator plus retrieval guidance, split across packages: interface ([dsh-spill](../../packages/spill/spill), `ctx.spillStore`), implementation ([dsh-spill-local](../../packages/spill/spill-local), private session-scoped files on the host filesystem), and consumer ([dsh-spill-policy](../../packages/spill/spill-policy), the `tools/post-execute` policy). Spill is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). Preview mechanics stay in [dsh-retention](../../packages/util/retention); this seam only saves the final text the policy hands it. Source: [`packages/spill/spill/src/types.ts`](../../packages/spill/spill/src/types.ts) diff --git a/docs/core-data-structures/subagent.md b/docs/core-data-structures/subagent.md index d279f26a9d..54e79aca68 100644 --- a/docs/core-data-structures/subagent.md +++ b/docs/core-data-structures/subagent.md @@ -2,7 +2,7 @@ The subagent seam — an agent delegating work to a child agent. Like [bash](bash.md) it is **one optional capability**, not part of the agent-loop spine, so its vocabulary lives here rather than in [core.md](core.md). But it differs from every other seam on one axis: **multiple provider implementations coexist** in one context, registered by name (`ctx.subagents`), where bash allows only one executor. The registry shape mirrors the [LLM adapter registry](llm-streaming.md), not the single-service bash executor. -Interface: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Implementations are sibling packages (`dsh-subagent-spawn`, `-fork`, `-acp`); the model-facing consumer is [dsh-tool-subagent](../../packages/subagent/tool-subagent). The proposal and rationale: [the subagent RFC](../rfc/implemented/feature/2026-06-21-subagent-capability-seam.md). +Interface: [dsh-subagent](../../packages/subagent/subagent) (`ctx.subagents` + the vocabulary below). Implementations are sibling packages (`dsh-subagent-spawn`, `-fork`, `-acp`); the model-facing consumer is [dsh-tool-subagent](../../packages/subagent/tool-subagent). The proposal and rationale: [the subagent Agent Note](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md). Source: [`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts) @@ -94,7 +94,7 @@ interface SubagentStartRequest { } ``` -`signal` is the single cancellation channel before and after readiness. The [subagent composition-controls RFC](../rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns the persona, live global-tool filter, absolute-depth, and visibility-not-authority rationale. +`signal` is the single cancellation channel before and after readiness. The [subagent composition-controls Agent Note](../../.agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) owns the persona, live global-tool filter, absolute-depth, and visibility-not-authority rationale. ## The terminal result: `SubagentResult` diff --git a/docs/core-data-structures/tasks.md b/docs/core-data-structures/tasks.md index ebe2062a84..491f380166 100644 --- a/docs/core-data-structures/tasks.md +++ b/docs/core-data-structures/tasks.md @@ -1,6 +1,6 @@ # Background Task Runtime -Types shared by long-running producers, `ctx.tasks`, and task control surfaces. The [runtime RFC](../rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) owns the design; this page records the literal shapes from [`packages/tasks/tasks/src/types.ts`](../../packages/tasks/tasks/src/types.ts). +Types shared by long-running producers, `ctx.tasks`, and task control surfaces. The [runtime Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) owns the design; this page records the literal shapes from [`packages/tasks/tasks/src/types.ts`](../../packages/tasks/tasks/src/types.ts). ## Ids and status diff --git a/docs/core-data-structures/tools.md b/docs/core-data-structures/tools.md index 7cc0910d90..9e3d698440 100644 --- a/docs/core-data-structures/tools.md +++ b/docs/core-data-structures/tools.md @@ -28,7 +28,7 @@ interface ToolDefinition extends ToolSchema { * Opted-in executions must not mutate parent-owned state. Shared state must * tolerate concurrent dispatch; recorder races are permitted only when they * commute or fail closed. See the - * [parallel-tool-call RFC](../../../../docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md) + * [parallel-tool-call Agent Note](../../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md) * for the full contract. * @param args - parsed arguments; `defineTool` validates before calling. * @returns Whether this call may join a parallel group. @@ -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 } @@ -346,6 +346,6 @@ How a tool wants its call shown in a UI (an editor tool-call card, a CLI log lin - `ToolCallView` (pending): `{ card: 'generic', title, kind?, rawInput?, content?, locations? }` (the default card; `locations` is `{ path, line? }[]` files the call reads/modifies, for editor follow-along), `{ card: 'terminal', title, description?, cwd? }` (a shell command → a terminal card), or `{ card: 'diff', title, diffs, locations? }` (a file create/modify → an inline diff card; `diffs` is `{ path, oldText, newText }[]`, `oldText: null` for a new file). - `ToolResultView` (completed): `{ card: 'generic', title?, content? }`, `{ card: 'terminal', title?, output?, exitCode?, signal? }` (the captured run output + exit; a capable UI shows an exit-status pill, an incapable one gets a fenced ` ```console ` fallback the bridge derives from `output`), or `{ card: 'diff', title?, diffs }` (a completed file mutation → the change to show, typically the applied hunks with context lines computed from the before/after content, or a whole-file diff when there is no before-image — e.g. a file create. A `tool_call_update`'s content REPLACES the call's content, so a mutation tool returns this even when it duplicates the call-time snippet, to keep the result from clobbering the diff with result text). -`ToolCallKind` (`'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'`) picks an icon on a generic card. `FileLocation` (`{ path, line? }`) and `FileDiff` (`{ path, oldText, newText }`) are the shared file-card vocabulary. The design is pinned in [the render-intent-union RFC](../rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md); the ACP bridge maps a `diff` card to a `{ type: 'diff' }` content block, a `terminal` card to the `_meta` terminal convention, and relativizes a file card's title against the session cwd. +`ToolCallKind` (`'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'`) picks an icon on a generic card. `FileLocation` (`{ path, line? }`) and `FileDiff` (`{ path, oldText, newText }`) are the shared file-card vocabulary. The design is pinned in [the render-intent-union Agent Note](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md); the ACP bridge maps a `diff` card to a `{ type: 'diff' }` content block, a `terminal` card to the `_meta` terminal convention, and relativizes a file card's title against the session cwd. The full presentation field docs live in [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts). The `bash` schema and executor are on [bash.md](bash.md); generic background controls are on [tasks.md](tasks.md). diff --git a/docs/core-data-structures/web.md b/docs/core-data-structures/web.md index af14f54466..22909b8dfb 100644 --- a/docs/core-data-structures/web.md +++ b/docs/core-data-structures/web.md @@ -1,6 +1,6 @@ # Web Access -The web access seam — a [capability seam](../rfc/implemented/architecture/2026-06-24-web-capability-seam.md) that spans **two capabilities** (search and fetch) on one `ctx.web` service, split across packages: interface ([dsh-web](../../packages/web/web), `ctx.web` + the provider registries), implementations ([dsh-web-search-exa](../../packages/web/web-search-exa), [dsh-web-search-perplexity](../../packages/web/web-search-perplexity), [dsh-web-search-deepseek](../../packages/web/web-search-deepseek), [dsh-web-fetch-local](../../packages/web/web-fetch-local)), and consumer ([dsh-tool-web](../../packages/web/tool-web), the `web_search`/`web_fetch` tool schemas). Web is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A search-provider swap does not change how the model asks for a query, and a fetch-implementation swap does not change how the model asks for a URL. +The web access seam — a [capability seam](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) that spans **two capabilities** (search and fetch) on one `ctx.web` service, split across packages: interface ([dsh-web](../../packages/web/web), `ctx.web` + the provider registries), implementations ([dsh-web-search-exa](../../packages/web/web-search-exa), [dsh-web-search-perplexity](../../packages/web/web-search-perplexity), [dsh-web-search-deepseek](../../packages/web/web-search-deepseek), [dsh-web-fetch-local](../../packages/web/web-fetch-local)), and consumer ([dsh-tool-web](../../packages/web/tool-web), the `web_search`/`web_fetch` tool schemas). Web is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A search-provider swap does not change how the model asks for a query, and a fetch-implementation swap does not change how the model asks for a URL. Source: [`packages/web/web/src/types.ts`](../../packages/web/web/src/types.ts) diff --git a/docs/core-data-structures/workflow.md b/docs/core-data-structures/workflow.md index 40b17a3112..26e337c4c6 100644 --- a/docs/core-data-structures/workflow.md +++ b/docs/core-data-structures/workflow.md @@ -2,7 +2,7 @@ The workflow seam — an agent running a model-written orchestration SCRIPT that fans out subagents. Like [subagent](subagent.md) it is **one optional capability**, not part of the agent-loop spine, so its vocabulary lives here rather than in [core.md](core.md). Unlike the subagent registry it takes the bash shape: ONE engine implementation per context provides `ctx.workflows`; there is no named-provider registry (a second engine is a plugin swap, not a co-resident). -Interface: [dsh-workflow](../../packages/workflow/workflow) (`ctx.workflows` + the vocabulary below). The implementation is [dsh-workflow-workerthread](../../packages/workflow/workflow-workerthread) (a `node:worker_threads` engine — one worker per run, the script's vm context inside it); the model-facing consumer is [dsh-tool-workflow](../../packages/workflow/tool-workflow). The proposal and rationale: [the dynamic-workflows RFC](../rfc/implemented/feature/2026-07-05-dynamic-workflows.md). +Interface: [dsh-workflow](../../packages/workflow/workflow) (`ctx.workflows` + the vocabulary below). The implementation is [dsh-workflow-workerthread](../../packages/workflow/workflow-workerthread) (a `node:worker_threads` engine — one worker per run, the script's vm context inside it); the model-facing consumer is [dsh-tool-workflow](../../packages/workflow/tool-workflow). The proposal and rationale: [the dynamic-workflows Agent Note](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md). Source: [`packages/workflow/workflow/src/types.ts`](../../packages/workflow/workflow/src/types.ts) 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/development.i18n.yaml b/docs/development.i18n.yaml index d7f82d7e92..2ca3f14fbc 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.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 -development.md: a3268164cd06fb8bf66f52391bc42c2dc3ca9396 -development.zh.md: f8f29c64aa6e49e3ed6d7ef12df2a4911c9182b0 +development.md: 94eb4f03329b574862a1ac1de2f8c1d4db4f4a0a +development.zh.md: b533aff43a66ff7cfc5dc61e5b9b224a01c51f12 diff --git a/docs/development.md b/docs/development.md index a3268164cd..94eb4f0332 100644 --- a/docs/development.md +++ b/docs/development.md @@ -2,11 +2,11 @@ English | [中文](development.zh.md) -This onboarding guide helps project contributors get started with the local environment, daily workflow, and CI flow; see the RFCs for design rationale and technical trade-offs. +This onboarding guide helps project contributors get started with the local environment, daily workflow, and CI flow; see the Agent Notes for design rationale and technical trade-offs. ## Prerequisites -- Node.js supports 22.19+ and 24+. CI covers 22.19, 24, and 26; see the [Node engine floor RFC](rfc/implemented/process/2026-07-06-node-engine-floor.md). +- Node.js supports 22.19+ and 24+. CI covers 22.19, 24, and 26; see the [Node engine floor Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md). - Corepack-enabled pnpm. The repo pins `pnpm@11.7.0` in `package.json`; run `corepack enable` if `pnpm --version` does not resolve through Corepack. - Git. - Optional: a DeepSeek API key for the REPL/ACP agent demos and real-API e2e tests. @@ -86,7 +86,6 @@ pnpm run verify-cordis-catalog # fail if either cordis catalog is stale pnpm run verify-export-jsdoc # fail if a module-level package export lacks complete JSDoc pnpm run gen-doc-graphs # regenerate generated relationship docs from source and curated graph definitions pnpm run verify-doc-graphs # fail if generated relationship docs are stale -pnpm run gen-rfc-index # regenerate the docs/rfc/README.md index tables from the RFC tree pnpm run verify-md-wrap # fail on hard-wrapped prose paragraphs in docs/README markdown pnpm run verify-mermaid # fail if a ```mermaid diagram has invalid Mermaid syntax pnpm run verify-type-equiv # fail if a ```ts type-equiv doc block drifts from its source type diff --git a/docs/development.zh.md b/docs/development.zh.md index f8f29c64aa..b533aff43a 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -2,11 +2,11 @@ [English](development.md) | 中文 -本指南覆盖参与 DeepSeek Harness 开发所需的本地环境搭建、日常工作流与 CI 流程;设计动机与技术权衡请查阅相应 RFC。 +本指南覆盖参与 DeepSeek Harness 开发所需的本地环境搭建、日常工作流与 CI 流程;设计动机与技术权衡请查阅相应 Agent Note。 ## 前置条件 -- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 RFC](rfc/implemented/process/2026-07-06-node-engine-floor.md)。 +- Node.js 支持 22.19+ 与 24+。CI 覆盖 22.19、24 和 26;见 [Node 引擎下限 Agent Note](../.agents/notes/implemented/process/2026-07-06-node-engine-floor.md)。 - 启用了 Corepack 的 pnpm。仓库在 `package.json` 中固定使用 `pnpm@11.7.0`;如果 `pnpm --version` 无法通过 Corepack 解析,请先运行 `corepack enable`。 - Git。 - 可选:一个 DeepSeek API key,用于 REPL/ACP(Agent Client Protocol) agent(智能体)演示和真实 API 的 e2e 测试。 @@ -86,7 +86,6 @@ pnpm run verify-cordis-catalog # fail if either cordis catalog is stale pnpm run verify-export-jsdoc # fail if a module-level package export lacks complete JSDoc pnpm run gen-doc-graphs # regenerate generated relationship docs from source and curated graph definitions pnpm run verify-doc-graphs # fail if generated relationship docs are stale -pnpm run gen-rfc-index # regenerate the docs/rfc/README.md index tables from the RFC tree pnpm run verify-md-wrap # fail on hard-wrapped prose paragraphs in docs/README markdown pnpm run verify-mermaid # fail if a ```mermaid diagram has invalid Mermaid syntax pnpm run verify-type-equiv # fail if a ```ts type-equiv doc block drifts from its source type diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 4f6e6daff0..b678c4b7e2 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -8,25 +8,25 @@ 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`) | [`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`) | [`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) | | `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) | diff --git a/docs/glossary.md b/docs/glossary.md index 81b9b4f84a..5392773dc9 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -1,6 +1,6 @@ # Glossary -Domain vocabulary for the DeepSeek Harness SDK uses one canonical term per concept. Terms link to their entries with standard Markdown anchors; implementation detail stays in package READMEs and RFCs. +Domain vocabulary for the DeepSeek Harness SDK uses one canonical term per concept. Terms link to their entries with standard Markdown anchors; implementation detail stays in package READMEs and Agent Notes. FIXME(glossary-completeness): Expand this glossary before the first release so it covers the SDK's other core and capability subsystems, not only agent scope. diff --git a/docs/graph-atlas.md b/docs/graph-atlas.md index 0e6401fdc0..d477bd6d62 100644 --- a/docs/graph-atlas.md +++ b/docs/graph-atlas.md @@ -5,7 +5,7 @@ These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the generated [events](cordis-catalog/events.md) / [services](cordis-catalog/services.md) catalogs, [tool-catalog.md](tool-catalog.md), and [core-data-structures/](core-data-structures/core.md). -The process decision behind this index is recorded in [the documentation graph RFC](rfc/implemented/process/2026-07-03-documentation-graph-atlas.md). +The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md). | Graph | Mode | | --- | --- | diff --git a/docs/i18n/README.i18n.yaml b/docs/i18n/README.i18n.yaml index ab1c9024ad..401813b088 100644 --- a/docs/i18n/README.i18n.yaml +++ b/docs/i18n/README.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 -README.md: 17bb1eeb67b4f5119a698fca23f12490c9378a7f -README.zh.md: c957a82bf420a942e2249942a2d9afc54ad950cf +README.md: bd5d8c08a4c474a13342b6b60800cfe0d31e110b +README.zh.md: a53ab8d9d6053b39def34505038504fefc80a3f9 diff --git a/docs/i18n/README.md b/docs/i18n/README.md index 17bb1eeb67..bd5d8c08a4 100644 --- a/docs/i18n/README.md +++ b/docs/i18n/README.md @@ -2,11 +2,11 @@ English | [中文](README.zh.md) -This repo's documentation is read by people and agents both inside and outside the company, so the README and the docs tree are maintained in English and Simplified Chinese. This page defines the pairing contract, the enforcement gate, and the rollout policy; [translation-rules.md](translation-rules.md) defines how to translate; [terminology.md](terminology.md) is the terminology source of truth. The committed agent workflow lives in [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md). +This repo's documentation is read by people and agents both inside and outside the company, so the README, Agent Notes, and docs tree are maintained in English and Simplified Chinese. This page defines the pairing contract, the enforcement gate, and the rollout policy; [translation-rules.md](translation-rules.md) defines how to translate; [terminology.md](terminology.md) is the terminology source of truth. The committed agent workflow lives in [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md). ## The pairing contract -- **Both languages carry equal authority.** A document may be authored and reviewed in either language first — a Chinese-first RFC is as legitimate as an English-first one — and the counterpart is translated from it. Neither file outranks the other; what binds them is that they must say the same thing. +- **Both languages carry equal authority.** A document may be authored and reviewed in either language first — a Chinese-first Agent Note is as legitimate as an English-first one — and the counterpart is translated from it. Neither file outranks the other; what binds them is that they must say the same thing. - **A pair is three sibling files.** The English `foo.md`, the Chinese `foo.zh.md`, and a consistency record `foo.i18n.yaml`, all in the same directory. No locale directories, no separate translation repo, no interleaved bilingual files. Pairs merge whole: a PR never lands one language without the other two files. - **The consistency record.** `foo.i18n.yaml` holds the full git blob hash of each side as of the last time the two were confirmed to say the same thing: @@ -26,7 +26,7 @@ This repo's documentation is read by people and agents both inside and outside t 1. Every file listed as `required` in [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) has a complete pair. 2. Every pair that exists at all — required or not — is complete and consistent: all three files present, each side's current blob hash equals the recorded one (editing either side without re-confirming the pair goes red), both sides carry the language switcher, and the structural signatures match in order — heading depths, verbatim code blocks (info string and content), table row and column counts, list kinds, ordered-list starts, item counts, and every link target apart from the switcher. 3. Files listed as `excluded` have no `.zh.md` and no `.i18n.yaml` at all. -4. Every date-named document (`yyyy-mm-dd-*.md`) dated on or after the manifest's `requiredSince` cutoff has a complete pair — new date-named RFCs merge bilingual from birth. +4. Every date-named document (`yyyy-mm-dd-*.md`) dated on or after the manifest's `requiredSince` cutoff has a complete pair — new date-named Agent Notes merge bilingual from birth. `pnpm run verify-translation-pairing --list` prints the current pairing state of every document in scope — missing, out-of-sync, or ok — and is the work list for translation batches. It never fails; it reports. @@ -36,16 +36,16 @@ The gate's limit, stated plainly: **a green gate means the pair was confirmed co ## Scope, exclusions, and rollout -**Scope**: the root `README.md`, everything under `docs/**`, and everything under `python/**`. Package READMEs (`packages/**`) join the scope in a later batch. +**Scope**: the root `README.md`, everything under `.agents/notes/**`, `docs/**`, and `python/**`. Package READMEs (`packages/**`) join the scope in a later batch. **Excluded** (never paired, and the gate rejects a `.zh.md` or `.i18n.yaml` for them): - `docs/cordis-catalog/`, `docs/tool-catalog/`, `docs/config-catalog.md`, `docs/persistence-catalog.md`, and `docs/module-graph.md` — generated files; their generators emit English only today, so a hand-written translation would go stale on every regeneration. The planned follow-up is to teach the generators to emit Chinese alongside English, at which point these leave the exclusion list. -- `docs/AGENTS.md` — agent instructions, maintained in English only like the root `AGENTS.md`. +- `docs/AGENTS.md` and `.agents/notes/**/AGENTS.md` — agent instructions, maintained in English only like the root `AGENTS.md`. - `docs/i18n/terminology.md` and [style-samples.md](style-samples.md) — both are bilingual by construction. - [translation-prompt.md](translation-prompt.md) — the automated pipeline's prompt template; its body is machine-consumed verbatim, so a paired translation would change pipeline behavior. -**Rollout**: a date-named document (`yyyy-mm-dd-*.md`, i.e. an RFC) dated on or after the manifest's `requiredSince` cutoff must merge with its pair. Earlier dates are backlog, including files created on the cutoff's eve. An RFC filename records its first-proposed date, so backdating past the cutoff is a review-visible violation. The manifest's `required` list is the current enforcement frontier, not the goal of full coverage. Translation batches add paths to `required`, ratcheting the gate forward. Unlisted documents remain visible in `--list`, while every existing pair is governed by the full contract. Because later edits must update both sides, expand `required` only as fast as translation review can support. +**Rollout**: a date-named document (`yyyy-mm-dd-*.md`, i.e. an Agent Note) dated on or after the manifest's `requiredSince` cutoff must merge with its pair. Earlier dates are backlog, including files created on the cutoff's eve. An Agent Note filename records its first-proposed date, so backdating past the cutoff is a review-visible violation. The manifest's `required` list is the current enforcement frontier, not the goal of full coverage. Translation batches add paths to `required`, ratcheting the gate forward. Unlisted documents remain visible in `--list`, while every existing pair is governed by the full contract. Because later edits must update both sides, expand `required` only as fast as translation review can support. ## Division of labor diff --git a/docs/i18n/README.zh.md b/docs/i18n/README.zh.md index c957a82bf4..a53ab8d9d6 100644 --- a/docs/i18n/README.zh.md +++ b/docs/i18n/README.zh.md @@ -2,11 +2,11 @@ [English](README.md) | 中文 -本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此 README 与 docs 目录树以英文和简体中文双语维护。本页定义配对契约、强制门禁与推进策略;[translation-rules.md](translation-rules.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。仓库内置的 agent 工作流见 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md)。 +本仓库的文档会被公司内外的人和 agent(智能体)阅读,因此 README、Agent Note 与 docs 目录树以英文和简体中文双语维护。本页定义配对契约、强制门禁与推进策略;[translation-rules.md](translation-rules.md) 定义如何翻译;[terminology.md](terminology.md) 是术语真源。仓库内置的 agent 工作流见 [.agents/skills/dsh-translate-docs](../../.agents/skills/dsh-translate-docs/SKILL.md)。 ## 配对契约 -- **两种语言同权。**一篇文档可以先用任一语言撰写和评审——先写中文的 RFC 与先写英文的一样正当——另一侧由它翻译而来。两个文件谁也不高于谁;约束它们的是二者必须说同样的话。 +- **两种语言同权。**一篇文档可以先用任一语言撰写和评审——先写中文的 Agent Note 与先写英文的一样正当——另一侧由它翻译而来。两个文件谁也不高于谁;约束它们的是二者必须说同样的话。 - **一对文档是三个同目录文件。**英文 `foo.md`、中文 `foo.zh.md`,加一份一致性记录 `foo.i18n.yaml`,都在同一目录。不用语言目录,不用独立翻译仓库,不用中英混排的单文件。配对整体合入:PR(Pull Request)永远不会只带一种语言而缺其余两个文件。 - **一致性记录。**`foo.i18n.yaml` 保存两侧文件在上一次被确认「说同样的话」时各自的完整 git blob hash: @@ -26,7 +26,7 @@ 1. [scripts/translation-pairing.manifest.json](../../scripts/translation-pairing.manifest.json) 中 `required` 列出的每个文件都有完整配对。 2. 任何已存在的配对——无论是否 required——都完整且一致:三个文件齐全、每一侧的当前 blob hash 等于记录值(改了任一侧而没重新确认配对就变红)、双方都带语言切换行、结构签名按序一致——标题深度、逐字节一致的代码块(信息字符串与内容)、表格行列数、列表类型、有序列表起始编号、列表项数量,以及除切换行之外的每个链接目标。 3. 列为 `excluded` 的文件完全没有 `.zh.md`,也没有 `.i18n.yaml`。 -4. 凡文件名符合 `yyyy-mm-dd-*.md` 且日期不早于 manifest(元数据清单)中 `requiredSince` 分界日期的文档,都必须有完整配对——新建的日期命名 RFC 从创建起便须配齐中英文。 +4. 凡文件名符合 `yyyy-mm-dd-*.md` 且日期不早于 manifest(元数据清单)中 `requiredSince` 分界日期的文档,都必须有完整配对——新建的日期命名 Agent Note 从创建起便须配齐中英文。 `pnpm run verify-translation-pairing --list` 打印范围内每篇文档的当前配对状态——missing、out-of-sync 或 ok——是翻译批次的工作清单。它从不失败;它只报告。 @@ -36,16 +36,16 @@ ## 范围、排除与推进 -**范围**:根 `README.md`、`docs/**` 下的全部内容,以及 `python/**` 下的全部内容。package README(`packages/**`)在后续批次加入范围。 +**范围**:根 `README.md`,以及 `.agents/notes/**`、`docs/**` 与 `python/**` 下的全部内容。package README(`packages/**`)在后续批次加入范围。 **排除**(永不配对,门禁拒绝为它们建 `.zh.md` 或 `.i18n.yaml`): - `docs/cordis-catalog/`、`docs/tool-catalog/`、`docs/config-catalog.md`、`docs/persistence-catalog.md` 与 `docs/module-graph.md`——生成文件;生成器目前只输出英文,手写译文在每次重新生成时必然陈旧。计划中的后续工作是让生成器同时输出中文,届时这些文件移出排除清单。 -- `docs/AGENTS.md`——agent 指令,与根 `AGENTS.md` 一样只以英文维护。 +- `docs/AGENTS.md` 与 `.agents/notes/**/AGENTS.md`——agent 指令,与根 `AGENTS.md` 一样只以英文维护。 - `docs/i18n/terminology.md` 与 [style-samples.md](style-samples.md)——二者本身即为中英对照文档。 - [translation-prompt.md](translation-prompt.md)——自动翻译流水线的 prompt 模板;正文逐字进入模型请求,配对翻译会改变流水线行为。 -**推进**:以日期命名的文档(`yyyy-mm-dd-*.md`,即 RFC),只要标注日期等于或晚于 manifest 的 `requiredSince` 分界日期,合入时就必须配齐双语文件。更早日期的文件属于 backlog(待翻清单),包括分界前夜创建的文件。RFC 文件名记录首次提出日期,因此倒填日期绕过分界属于评审可见的违规。manifest 中的 `required` 列表是当前执行红线,并非全量覆盖这一最终目标。翻译批次将路径加入 `required`,使门禁只向前收紧。未列入的文档仍可通过 `--list` 查看,而任何已存在的配对都受完整契约约束。后续修改必须同步更新两侧,因此 `required` 的扩展速度不能超过翻译评审的承载能力。 +**推进**:以日期命名的文档(`yyyy-mm-dd-*.md`,即 Agent Note),只要标注日期等于或晚于 manifest 的 `requiredSince` 分界日期,合入时就必须配齐双语文件。更早日期的文件属于 backlog(待翻清单),包括分界前夜创建的文件。Agent Note 文件名记录首次提出日期,因此倒填日期绕过分界属于评审可见的违规。manifest 中的 `required` 列表是当前执行红线,并非全量覆盖这一最终目标。翻译批次将路径加入 `required`,使门禁只向前收紧。未列入的文档仍可通过 `--list` 查看,而任何已存在的配对都受完整契约约束。后续修改必须同步更新两侧,因此 `required` 的扩展速度不能超过翻译评审的承载能力。 ## 分工 diff --git a/docs/i18n/style-samples.md b/docs/i18n/style-samples.md index 786ebea0df..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` 区间;取消或资源释放还可能丢弃尚未启动的队列项。 ## ③ 测试政策清单 @@ -62,7 +62,7 @@ 门禁的边界很明确:通过门禁只说明两侧文件当前的 blob hash 与伴随记录吻合,并且结构签名一致,也就是说,这组内容曾被确认一致;它不代表这次确认可靠。门禁无法判断两种语言是否真正表达了相同的意思;这部分契约要由评审人把关。即使译文粗糙、表意有误,重新记录配对后仍能通过门禁,但绝不能通过人工评审。 -## ⑥ RFC 论证 +## ⑥ Agent Note 论证 > Comparing git timestamps of the pair (no record) — rejected: formatting-only edits would false-positive, and a counterpart committed after an unrelated edit would false-negative; content identity is the only signal that means what the gate claims. @@ -70,9 +70,9 @@ ## ⑦ 推进策略(长段拆分示范) -> **Rollout**: date-named RFCs don't wait for a batch — one dated on or after the manifest's `requiredSince` cutoff must merge with its pair, so each new date-named RFC is bilingual from birth. For the back-catalog, the `required` list in the manifest is the enforcement frontier, not the goal. […] Pairing a document is a commitment: every later edit to either side must carry the counterpart along, so grow the frontier at the pace translation review is actually resourced, not ahead of it. +> **Rollout**: date-named Agent Notes don't wait for a batch — one dated on or after the manifest's `requiredSince` cutoff must merge with its pair, so each new date-named Agent Note is bilingual from birth. For the back-catalog, the `required` list in the manifest is the enforcement frontier, not the goal. […] Pairing a document is a commitment: every later edit to either side must carry the counterpart along, so grow the frontier at the pace translation review is actually resourced, not ahead of it. -**推进**:日期命名的 RFC 无需等待批量翻译。只要文件名中的日期不早于 manifest(元数据清单)的 `requiredSince` 分界日期,合入时就必须配齐中英文,因此此类 RFC 从创建起就要求双语齐备。对于存量文档,manifest 中的 `required` 列表只是当前的执行红线,并非最终目标。(……)一旦文档完成配对,后续修改任一侧都必须同步更新另一侧。因此,应根据实际可投入的翻译评审能力逐步扩展执行红线,不能超前。 +**推进**:日期命名的 Agent Note 无需等待批量翻译。只要文件名中的日期不早于 manifest(元数据清单)的 `requiredSince` 分界日期,合入时就必须配齐中英文,因此此类 Agent Note 从创建起就要求双语齐备。对于存量文档,manifest 中的 `required` 列表只是当前的执行红线,并非最终目标。(……)一旦文档完成配对,后续修改任一侧都必须同步更新另一侧。因此,应根据实际可投入的翻译评审能力逐步扩展执行红线,不能超前。 ## 从样例提炼的要点 diff --git a/docs/i18n/terminology.md b/docs/i18n/terminology.md index f11a1ff19e..3e7527429b 100644 --- a/docs/i18n/terminology.md +++ b/docs/i18n/terminology.md @@ -33,6 +33,7 @@ | English | 中文 | 首次出现 | 不要译作 | 备注 | |---|---|---|---|---| | agent | agent | agent(智能体) | | | +| Agent Note | Agent Note | Agent Note(agent 决策记录) | 智能体注记、智能体笔记 | 本仓库中由 agent 撰写的提案与决策记录 | | agent harness | agent harness | agent harness(智能体框架) | | agent 组合词(agent harness/workflow/loop/skill 等)整体保留英文;未括注过 agent 时首现按对应组合词或 agent 行处理 | | agent loop | agent loop | agent loop(智能体循环) | | | | backlog | backlog | backlog(待翻清单) | | 仅在双语翻译语境里括注`待翻清单` | diff --git a/docs/i18n/translation-prompt.md b/docs/i18n/translation-prompt.md index cd87c161de..bfbb583303 100644 --- a/docs/i18n/translation-prompt.md +++ b/docs/i18n/translation-prompt.md @@ -27,7 +27,7 @@ - `docs/development.md` ↔ `docs/development.zh.md` - `docs/i18n/README.md` ↔ `docs/i18n/README.zh.md` - `docs/i18n/translation-rules.md` ↔ `docs/i18n/translation-rules.zh.md` -- `docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md` ↔ 对应 `.zh.md` +- `.agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md` ↔ 对应 `.zh.md` 注入时按当前翻译方向选择每组的源侧与目标侧:user 消息包含源文档全文,assistant 消息采用模板正文规定的 XML 协议;`translation` 与 `final` 都放入目标文档全文,`review` 填 `- [None] No corrections.`。CDATA 遵循上文的 `]]>` 拆分规则。上下文不足时,按上列顺序从后往前删减示例组数。这 5 组也是评审校准锚点;改动任何一组都会改变流水线行为。 diff --git a/docs/module-graph.md b/docs/module-graph.md index e99a53c030..0ed79d32f8 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -38,6 +38,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 @@ -49,6 +50,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"] @@ -136,6 +138,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"] @@ -163,8 +166,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 @@ -175,14 +176,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 @@ -196,10 +196,18 @@ 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_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 @@ -244,8 +252,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 @@ -260,6 +274,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 @@ -267,9 +282,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 @@ -470,16 +488,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) | @@ -488,8 +504,12 @@ 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) | | [`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) | @@ -502,11 +522,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 3bc15253a5..6ab6f5f1d4 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -5,7 +5,7 @@ Every event type that can appear in a session's durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](core-data-structures/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](core-data-structures/persistence.md) (how the log is made durable), and the [cordis events catalog](cordis-catalog/events.md) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit). -This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog RFC](rfc/implemented/process/2026-07-04-persistence-log-catalog.md). +This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md). The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](core-data-structures/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction. @@ -79,7 +79,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = { }[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:256`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:293`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:325`](../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:220`](../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:227`](../packages/core/session/src/types.ts) ### `compact/*` @@ -224,7 +209,7 @@ Source: [`packages/compact/compact/src/types.ts:15`](../packages/compact/compact * The model that wrote the summary — the summarize call's envelope, * reported by the backend that made the call, logged so the one-shot * request is reconstructable from log + code and "which model wrote - * this summary" has a durable answer (the reconstructability RFC). + * this summary" has a durable answer (the reconstructability Agent Note). */ model: string /** The generation cap the summarize call sent, when one applied. */ @@ -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:214`](../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:202`](../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:252`](../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:245`](../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:195`](../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:193`](../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:247`](../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:233`](../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:243`](../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:191`](../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:184`](../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:197`](../packages/core/session/src/types.ts) diff --git a/docs/postmortem/README.md b/docs/postmortem/README.md index 743433a827..7114f65916 100644 --- a/docs/postmortem/README.md +++ b/docs/postmortem/README.md @@ -2,7 +2,7 @@ Incident write-ups: a bug reached a place it shouldn't have (a real user, a merged PR, a release), and the interesting part is *why our process let it through*, not just the one-line fix. -A post-mortem is NOT an [RFC](../rfc/README.md) (which records a deliberate design decision and its rejected alternatives, or proposes future work). It is a backward-looking record of a failure: what broke, the mechanism, why every safety net missed it, and the concrete guardrails added so the same class of bug fails loudly next time. +A post-mortem is NOT an [Agent Note](../../.agents/notes/README.md) (which records a deliberate design decision and its rejected alternatives, or proposes future work). It is a backward-looking record of a failure: what broke, the mechanism, why every safety net missed it, and the concrete guardrails added so the same class of bug fails loudly next time. Write one when a bug is **subtle** (the mechanism is non-obvious and a careful engineer would re-derive it the hard way), **systemic** (the reason it escaped is a gap in tests/tooling/conventions, not a one-off typo), and **costly to rediscover** (it cost real debugging time, and would cost it again). Link the guardrails (tests, AGENTS.md rules, ADRs) the post-mortem motivated. diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md deleted file mode 100644 index 65afa473fc..0000000000 --- a/docs/rfc/INDEX.md +++ /dev/null @@ -1,248 +0,0 @@ -# RFC index - -Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; `verify-rfc-classification` fails when this file is stale. The curated front door — layout, classification, when to write one, and the in-file format — is [README.md](README.md). - -## Proposed - -### Feature - -| Title | First proposed | -|---|---| -| [Pre-tool input rewrite — a consistent design](proposed/feature/2026-06-30-pre-tool-input-rewrite.md) | 2026-06-30 | -| [Recallable compaction — index checkpoints, a state checkpoint, and in-session history recall](proposed/feature/2026-07-06-recallable-compaction.md) | 2026-07-06 | -| [Claude Code and Codex subagent backends (out-of-process delegation to external coding agents)](proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md) | 2026-07-07 | -| [Interactive side sessions and merge-back](proposed/feature/2026-07-08-interactive-side-sessions.md) | 2026-07-08 | -| [SQLite FTS5 session search](proposed/feature/2026-07-10-sqlite-session-query-provider.md) | 2026-07-10 | -| [Stream workflow progress through tool calls](proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md) | 2026-07-13 | -| [Developer-owned SDK projects](proposed/feature/2026-07-14-sdk-developer-projects.md) | 2026-07-14 | -| [SDK follow-up capabilities](proposed/feature/2026-07-17-sdk-follow-up-capabilities.md) | 2026-07-17 | - -### Simplification - -| Title | First proposed | -|---|---| -| [Prune dead public and result surface](proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md) | 2026-07-04 | -| [Make JSON-RPC completion and transport directional](proposed/simplification/2026-07-19-make-jsonrpc-directional.md) | 2026-07-19 | - -### Architecture - -| Title | First proposed | -|---|---| -| [Runtime schemas for the event vocabulary (Zod vs the merge-extensible-map pattern)](proposed/architecture/2026-06-16-typed-event-schemas.md) | 2026-06-16 | -| [SDK project editing architecture](proposed/architecture/2026-07-15-sdk-project-editing-architecture.md) | 2026-07-15 | - -### Process - -| Title | First proposed | -|---|---| -| [API extractor reports](proposed/process/2026-06-11-api-extractor-reports.md) | 2026-06-11 | -| [Architectural conformance — dependency rules and the adapter kit](proposed/process/2026-06-11-architectural-conformance.md) | 2026-06-11 | -| [Supply chain checks and vendor drift verification](proposed/process/2026-06-11-supply-chain-and-vendor-drift.md) | 2026-06-11 | -| [Discover package inventories instead of maintaining static lists](proposed/process/2026-06-20-discover-package-inventory.md) | 2026-06-20 | -| [Periodic human-review maintenance for dsh-code-review](proposed/process/2026-07-13-human-review-skill-maintenance.md) | 2026-07-13 | - -### Testing - -| Title | First proposed | -|---|---| -| [Deterministic tests, the replay invariant fixture, and race stress](proposed/testing/2026-06-11-deterministic-and-stress-testing.md) | 2026-06-11 | -| [Mutation testing as the coverage counterweight](proposed/testing/2026-06-11-mutation-testing.md) | 2026-06-11 | - -## Implemented - -### Feature - -| Title | First proposed | -|---|---| -| [Agent Client Protocol (ACP) support — drive the coding agent from external editors](implemented/feature/2026-06-14-acp-agent-client-protocol.md) | 2026-06-14 | -| [Multiplex concurrent ACP sessions over one connection](implemented/feature/2026-06-14-acp-multi-session.md) | 2026-06-14 | -| [Code Mode — the model writes TypeScript against the tool registry](implemented/feature/2026-06-15-code-mode.md) | 2026-06-15 | -| [Filesystem tool schemas — model-facing read/write/edit shapes](implemented/feature/2026-06-17-filesystem-tool-schemas.md) | 2026-06-17 | -| [Rich ACP bash rendering — the terminal card via the `_meta` convention](implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) | 2026-06-18 | -| [Compaction as a capability seam (abstract contract + basic backend)](implemented/feature/2026-06-18-compaction-capability-seam.md) | 2026-06-18 | -| [Subagent capability seam](implemented/feature/2026-06-21-subagent-capability-seam.md) | 2026-06-21 | -| [ACP subagent backend (out-of-process delegation)](implemented/feature/2026-06-22-acp-subagent-backend.md) | 2026-06-22 | -| [Workspace context instruction files](implemented/feature/2026-06-24-workspace-context.md) | 2026-06-24 | -| [Ask-user question capability](implemented/feature/2026-06-25-ask-user-question.md) | 2026-06-25 | -| [The `todo_write` tool — model task list as event-sourced session state](implemented/feature/2026-06-29-todo-write-tool.md) | 2026-06-29 | -| [dsh-hooks-claude + dsh-hooks-codex — the Claude Code / Codex hook bridges](implemented/feature/2026-06-30-hook-bridges.md) | 2026-06-30 | -| [dsh-hook-protocol — the shared Claude Code / Codex hook wire-protocol core](implemented/feature/2026-06-30-hook-protocol-lib.md) | 2026-06-30 | -| [Interception seams — the typed-Decision surface a hook programs against](implemented/feature/2026-06-30-interception-seams.md) | 2026-06-30 | -| [SessionStore fork API](implemented/feature/2026-06-30-session-store-fork-api.md) | 2026-06-30 | -| [Subagent lifecycle enrichment — lastAssistantMessage (observe-only)](implemented/feature/2026-06-30-subagent-observe-enrich.md) | 2026-06-30 | -| [Dynamic workflows — a script-driven multi-agent orchestration seam](implemented/feature/2026-07-05-dynamic-workflows.md) | 2026-07-05 | -| [Skill system — progressive disclosure instructions for agents](implemented/feature/2026-07-05-skill-system.md) | 2026-07-05 | -| [The approval seam — one-shot permission decisions over a waterfall of answerers](implemented/feature/2026-07-06-approval-seam.md) | 2026-07-06 | -| [Explicit model-facing tool order](implemented/feature/2026-07-06-explicit-tool-order.md) | 2026-07-06 | -| [The subprocess sandbox — confinement seam, native runners, escalation, and per-session modes](implemented/feature/2026-07-06-sandbox.md) | 2026-07-06 | -| [MCP client plugin — connect to external MCP servers and bridge their tools](implemented/feature/2026-07-07-mcp-client-plugin.md) | 2026-07-07 | -| [The session prefix — request-only messages in front of the derived history](implemented/feature/2026-07-07-session-prefix.md) | 2026-07-07 | -| [Background subagent tasks](implemented/feature/2026-07-08-background-subagent-tasks.md) | 2026-07-08 | -| [Repeat-tool-call guard plugin](implemented/feature/2026-07-08-repeat-tool-guard.md) | 2026-07-08 | -| [The self-referential cordis toolset](implemented/feature/2026-07-08-self-referential-cordis-toolset.md) | 2026-07-08 | -| [Bash-backed grep and glob discovery tools](implemented/feature/2026-07-09-bash-backed-grep-glob-discovery.md) | 2026-07-09 | -| [Expose agent session identity and JSONL location to tools and hooks](implemented/feature/2026-07-10-agent-session-identity-and-log-location.md) | 2026-07-10 | -| [Parallel tool-call execution by per-call safety](implemented/feature/2026-07-10-parallel-tool-call-execution.md) | 2026-07-10 | -| [Exact session query service](implemented/feature/2026-07-10-session-query-service.md) | 2026-07-10 | -| [Configure subagent persona, tool visibility, and depth](implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) | 2026-07-12 | -| [Session query relationship tracing](implemented/feature/2026-07-13-session-query-tracing.md) | 2026-07-13 | -| [Optional time-context plugin](implemented/feature/2026-07-14-time-context-plugin.md) | 2026-07-14 | -| [Durable per-step time context](implemented/feature/2026-07-16-durable-per-step-time-context.md) | 2026-07-16 | -| [Dedicated full-screen TUI front door](implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md) | 2026-07-17 | - -### Simplification - -| Title | First proposed | -|---|---| -| [Drop the mutable session summary](implemented/simplification/2026-06-19-drop-mutable-session-summary.md) | 2026-06-19 | -| [Fold trace-only session facts into load-bearing events](implemented/simplification/2026-06-20-collapse-trace-only-session-events.md) | 2026-06-20 | -| [Drop the unconsumed `llm/adapter-change` event](implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md) | 2026-06-20 | -| [Drop unconsumed assembled LLM convenience surfaces](implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md) | 2026-06-20 | -| [Prune dead methods from the persistence seam](implemented/simplification/2026-06-20-prune-dead-seam-methods.md) | 2026-06-20 | -| [Keep one public stop primitive](implemented/simplification/2026-06-20-public-agent-stop-surface.md) | 2026-06-20 | -| [Stop mirroring durable boundaries as agent events](implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md) | 2026-06-20 | -| [Unify the agent id and the session id](implemented/simplification/2026-06-20-unify-agent-and-session-id.md) | 2026-06-20 | -| [Split the filesystem seam — provider text mutations plus the `dsh-fs-policy` plugin](implemented/simplification/2026-06-26-fsspec-style-fs-seam.md) | 2026-06-26 | -| [Stop mirroring the token stream as an agent event](implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md) | 2026-07-02 | -| [Drop the `image` content block until a path can honor it](implemented/simplification/2026-07-04-drop-image-content-block.md) | 2026-07-04 | -| [Drop `GenerateOptions.prefill` and `ToolSchema.strict` — request knobs with no working end-to-end path](implemented/simplification/2026-07-04-drop-inert-request-knobs.md) | 2026-07-04 | -| [Drop the unconsumed web observation surface — the `providers-change` event and the status methods](implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) | 2026-07-04 | -| [Fold the stdio UI helper into the stdio app](implemented/simplification/2026-07-04-fold-stdio-ui-helper.md) | 2026-07-04 | -| [Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger)](implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) | 2026-07-04 | -| [Prune write-only fields and a dead routing knob from the fs seam](implemented/simplification/2026-07-04-prune-write-only-fs-surface.md) | 2026-07-04 | -| [Remove the `agent/steering` mirror emit](implemented/simplification/2026-07-04-remove-agent-steering-mirror.md) | 2026-07-04 | -| [Share the app bins' boot glue instead of maintaining twin copies](implemented/simplification/2026-07-04-share-app-bin-boot-glue.md) | 2026-07-04 | -| [Tighten the hook-protocol contract — dialect, discarded fields, double defaults, and lib-owned `hook/result` semantics](implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md) | 2026-07-04 | -| [Trim unreachable ACP bridge surface — the branding knobs and the kind-sniffing fallback](implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md) | 2026-07-04 | -| [Drop unconsumed skill provider events](implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md) | 2026-07-12 | -| [Prune unused web seam fields](implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md) | 2026-07-12 | -| [Simplify session-log representation](implemented/simplification/2026-07-12-simplify-session-log-representation.md) | 2026-07-12 | -| [Retire the standalone subagent mock package](implemented/simplification/2026-07-19-retire-subagent-mock-package.md) | 2026-07-19 | -| [Use one surface manager per session](implemented/simplification/2026-07-19-use-one-session-surface-manager.md) | 2026-07-19 | - -### Architecture - -| Title | First proposed | -|---|---| -| [Provider-neutral content-block vocabulary owned by dsh-llm](implemented/architecture/2026-06-11-content-block-vocabulary.md) | 2026-06-11 | -| [Custom typed tool-schema DSL instead of schemastery](implemented/architecture/2026-06-11-custom-schema-dsl.md) | 2026-06-11 | -| [Source-owned session immutability and dev-mode invariants](implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md) | 2026-06-11 | -| [Event-sourced sessions with derived message history](implemented/architecture/2026-06-11-event-sourced-sessions.md) | 2026-06-11 | -| [Microkernel — extension via Cordis event taxonomy, one concrete loop](implemented/architecture/2026-06-11-microkernel-event-taxonomy.md) | 2026-06-11 | -| [Runtime arg validation at the model boundary](implemented/architecture/2026-06-11-runtime-arg-validation.md) | 2026-06-11 | -| [Structured error taxonomy](implemented/architecture/2026-06-11-structured-error-taxonomy.md) | 2026-06-11 | -| [Tool schemas are part of the system-prompt assembly](implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md) | 2026-06-11 | -| [Capability seams — interface / implementation / consumer split](implemented/architecture/2026-06-13-capability-seams.md) | 2026-06-13 | -| [Two LLM adapters as a design-verification twin](implemented/architecture/2026-06-13-twin-llm-adapters.md) | 2026-06-13 | -| [Session persistence as an abstract service over the existing `SessionEvent`](implemented/architecture/2026-06-14-session-persistence.md) | 2026-06-14 | -| [Every session event is enclosed in a turn](implemented/architecture/2026-06-15-turn-enclosure-invariant.md) | 2026-06-15 | -| [Filesystem capability seam — ctx.fs, local backend, and model-facing filesystem tools](implemented/architecture/2026-06-17-filesystem-capability-seam.md) | 2026-06-17 | -| [Agent lifecycle and ownership seams](implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md) | 2026-06-18 | -| [Session surface — an ordered projection over the event log](implemented/architecture/2026-06-18-session-surface.md) | 2026-06-18 | -| [Shared persistence write coordinator](implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) | 2026-06-18 | -| [Branded IDs everywhere they belong](implemented/architecture/2026-06-20-branded-ids.md) | 2026-06-20 | -| [Extract example apps into packages](implemented/architecture/2026-06-20-extract-example-app-packages.md) | 2026-06-20 | -| [The background task runtime (`ctx.tasks`) and generic task control tools](implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) | 2026-06-20 | -| [Reorganize packages into a modular hierarchy](implemented/architecture/2026-06-20-package-hierarchy.md) | 2026-06-20 | -| [Mandatory `User-Agent` attribution for provider requests](implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md) | 2026-06-21 | -| [Web capability seam - stable tools over multiple providers](implemented/architecture/2026-06-24-web-capability-seam.md) | 2026-06-24 | -| [Make `dsh-fs-policy` an event-gate plugin, not a method interface](implemented/architecture/2026-06-26-file-context-as-event-gate.md) | 2026-06-26 | -| [stdin + extra env on the bash seam](implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) | 2026-06-30 | -| [Event-domain semantics — session is the fact log, agent is the live surface](implemented/architecture/2026-06-30-event-domain-semantics.md) | 2026-06-30 | -| [Resolve filesystem paths against the caller's session cwd](implemented/architecture/2026-07-02-fs-per-session-cwd.md) | 2026-07-02 | -| [Result-time applied-hunk diffs for file mutations](implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md) | 2026-07-02 | -| [Tagged render-intent union for tool-call presentation](implemented/architecture/2026-07-02-tool-render-intent-union.md) | 2026-07-02 | -| [Add direct directory listing to the filesystem seam](implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md) | 2026-07-03 | -| [Prompt variables and tool-guidance ownership](implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md) | 2026-07-05 | -| [Every LLM request is reconstructable from the session log](implemented/architecture/2026-07-05-reconstructable-requests.md) | 2026-07-05 | -| [Subagent provider-lifecycle events — `subagent/provider-added` / `subagent/provider-removed`](implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md) | 2026-07-05 | -| [A shared timeout/deadline primitive, with hard-kill left to each capability](implemented/architecture/2026-07-06-timeout-deadline-library.md) | 2026-07-06 | -| [Tool result retention library](implemented/architecture/2026-07-06-tool-result-retention-library.md) | 2026-07-06 | -| [Tool-call timeout policy as a plugin](implemented/architecture/2026-07-07-tool-call-timeout-policy.md) | 2026-07-07 | -| [The agent is a registration scope](implemented/architecture/2026-07-08-agent-scope-contexts.md) | 2026-07-08 | -| [Tool output spill policy](implemented/architecture/2026-07-08-tool-output-spill-files.md) | 2026-07-08 | -| [After-call compaction pressure and context-overflow recovery](implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md) | 2026-07-10 | -| [Single-file executable SDK runtime distribution (single-exe)](implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) | 2026-07-10 | -| [Agent-scope runtime design and correctness](implemented/architecture/2026-07-12-agent-scope-runtime-design.md) | 2026-07-12 | -| [Provider-routed LLM adapters and a generic pi-ai backend](implemented/architecture/2026-07-14-provider-routed-llm-adapters.md) | 2026-07-14 | -| [Initiating Agent scope over AsyncLocalStorage](implemented/architecture/2026-07-15-agent-initiator-scope.md) | 2026-07-15 | -| [Advisory LLM catalogs and per-session ACP model selection](implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md) | 2026-07-15 | -| [Replay token meter service](implemented/architecture/2026-07-15-replay-token-meter-service.md) | 2026-07-15 | - -### Process - -| Title | First proposed | -|---|---| -| [Doc-sync enforcement](implemented/process/2026-06-11-doc-sync-enforcement.md) | 2026-06-11 | -| [Mechanical quality gates over prose guidelines](implemented/process/2026-06-11-quality-gates.md) | 2026-06-11 | -| [tsdown for JS bundling instead of dumble](implemented/process/2026-06-11-tsdown-over-dumble.md) | 2026-06-11 | -| [Vendor Cordis as source, not npm dependencies](implemented/process/2026-06-11-vendor-cordis-as-source.md) | 2026-06-11 | -| [pnpm as the package manager instead of Yarn 4](implemented/process/2026-06-16-pnpm-over-yarn.md) | 2026-06-16 | -| [TSC-first build and one tsconfig](implemented/process/2026-06-17-ts-build-config.md) | 2026-06-17 | -| [Markdown cross-link validity linting](implemented/process/2026-06-18-markdown-cross-link-lint.md) | 2026-06-18 | -| [Core-data-structures catalog and the `ts type-equiv` drift gate](implemented/process/2026-06-20-core-data-structures-catalog.md) | 2026-06-20 | -| [Generated cordis events + services catalog](implemented/process/2026-06-20-generated-cordis-catalog.md) | 2026-06-20 | -| [Classify RFCs by kind via path-encoded subdirectories](implemented/process/2026-06-20-rfc-classification.md) | 2026-06-20 | -| [Bilingual documentation via paired sibling files and a pairing gate](implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md) | 2026-07-02 | -| [Generated tool-schema catalog (boot-and-harvest)](implemented/process/2026-07-02-tool-schema-catalog.md) | 2026-07-02 | -| [Documentation graph index for maintainers and SDK users](implemented/process/2026-07-03-documentation-graph-atlas.md) | 2026-07-03 | -| [JSDoc completeness gate for the cordis surface](implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md) | 2026-07-04 | -| [Documentation tiers, budgets, and the ceiling gate](implemented/process/2026-07-04-doc-tiers-and-budgets.md) | 2026-07-04 | -| [Generate the RFC index tables](implemented/process/2026-07-04-generate-rfc-index-tables.md) | 2026-07-04 | -| [Generated persistence log event catalog](implemented/process/2026-07-04-persistence-log-catalog.md) | 2026-07-04 | -| [One gated in-file format for RFCs](implemented/process/2026-07-05-uniform-rfc-format.md) | 2026-07-05 | -| [Export-surface JSDoc gate](implemented/process/2026-07-06-export-surface-jsdoc-gate.md) | 2026-07-06 | -| [Generated plugin config catalog](implemented/process/2026-07-06-generated-config-catalog.md) | 2026-07-06 | -| [Raise the Node LTS engine floor to 22.19](implemented/process/2026-07-06-node-engine-floor.md) | 2026-07-06 | -| [Parallel GitHub CI gates](implemented/process/2026-07-06-parallel-github-ci-gates.md) | 2026-07-06 | -| [Parallel pre-push gates](implemented/process/2026-07-06-parallel-pre-push-gates.md) | 2026-07-06 | -| [A gated Known-Limitations section in every package README](implemented/process/2026-07-10-readme-known-limitations-gate.md) | 2026-07-10 | -| [Package Model Experience contract](implemented/process/2026-07-12-package-model-experience-contract.md) | 2026-07-12 | -| [TypeScript Program-backed semantic gates](implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md) | 2026-07-14 | -| [Run CI examples from built lib](implemented/process/2026-07-17-run-ci-examples-from-built-lib.md) | 2026-07-17 | - -### Testing - -| Title | First proposed | -|---|---| -| [Property-based testing for protocol-shaped code](implemented/testing/2026-06-11-property-based-testing.md) | 2026-06-11 | -| [ACP snapshot tests — record-once / replay-deterministic](implemented/testing/2026-06-19-acp-snapshot-tests.md) | 2026-06-19 | -| [Real-API e2e in CI against the external DeepSeek API](implemented/testing/2026-06-19-real-api-e2e-ci.md) | 2026-06-19 | -| [Use `session.jsonl` as the only snapshot session-log artifact](implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md) | 2026-06-20 | -| [Persist the seed boundary so fork-child replay routes correctly](implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md) | 2026-06-22 | -| [Record fork and mixed spawn+fork snapshot scenarios](implemented/testing/2026-06-22-fork-snapshot-scenarios.md) | 2026-06-22 | -| [Per-session snapshot replay for nested agents](implemented/testing/2026-06-22-subagent-snapshot-replay.md) | 2026-06-22 | -| [Hook snapshot matrix — end-to-end expected outputs for both bridges](implemented/testing/2026-07-04-hook-snapshot-matrix.md) | 2026-07-04 | -| [Single-source the acp-agent replay config](implemented/testing/2026-07-04-single-source-acp-replay-config.md) | 2026-07-04 | -| [Pin request-header content in one snapshot scenario](implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md) | 2026-07-06 | -| [Extract the ACP snapshot suite into a support package](implemented/testing/2026-07-08-shared-acp-snapshot-package.md) | 2026-07-08 | -| [Snapshot semantic terminal state for the TUI](implemented/testing/2026-07-18-tui-terminal-state-snapshots.md) | 2026-07-18 | - -## Rejected - -### Simplification - -| Title | First proposed | -|---|---| -| [Persist assembled assistant messages, not stream chunks](rejected/simplification/2026-06-20-assembled-assistant-messages-only.md) | 2026-06-20 | -| [Drop ACP session/load until resume has a product shape](rejected/simplification/2026-06-20-drop-acp-session-load.md) | 2026-06-20 | -| [Drop ACP terminal `_meta` rendering](rejected/simplification/2026-06-20-drop-acp-terminal-meta.md) | 2026-06-20 | -| [Drop bash full-output spill files](rejected/simplification/2026-06-20-drop-bash-output-spill-files.md) | 2026-06-20 | -| [Drop durable step boundary events](rejected/simplification/2026-06-20-drop-durable-step-boundaries.md) | 2026-06-20 | -| [Drop unused session lineage metadata](rejected/simplification/2026-06-20-drop-unused-session-lineage.md) | 2026-06-20 | -| [Fold the persistence interface into dsh-session](rejected/simplification/2026-06-20-fold-session-persistence-interface.md) | 2026-06-20 | -| [Collapse tool-owned UI presentation](rejected/simplification/2026-06-20-generic-tool-rendering.md) | 2026-06-20 | -| [Retire mid-turn steering](rejected/simplification/2026-06-20-retire-mid-turn-steering.md) | 2026-06-20 | -| [Return the ACP bridge to one live session per connection](rejected/simplification/2026-06-20-single-session-acp-bridge.md) | 2026-06-20 | -| [Truncate interrupted final turns on load](rejected/simplification/2026-06-20-truncate-interrupted-turns.md) | 2026-06-20 | -| [Prune the unimplemented subagent seam vocabulary](rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md) | 2026-07-04 | -| [Collapse workflows to the exercised foreground core](rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md) | 2026-07-12 | -| [Prune unused skill registry surface](rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md) | 2026-07-12 | -| [Fold the single compaction backend into its service package](rejected/simplification/2026-07-19-fold-compaction-package-split.md) | 2026-07-19 | - -### Architecture - -| Title | First proposed | -|---|---| -| [Deep-readonly public surfaces](rejected/architecture/2026-06-11-immutable-public-surfaces.md) | 2026-06-11 | -| [Make the shared example base providerless](rejected/architecture/2026-06-20-providerless-example-base.md) | 2026-06-20 | diff --git a/docs/rfc/README.md b/docs/rfc/README.md deleted file mode 100644 index a6fe4dbfa2..0000000000 --- a/docs/rfc/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# RFCs - -One kind of design doc lives here. An **RFC** records a decision or proposal that shapes this codebase — the *why* and *what we gave up*, the parts code and docs can't carry. The full list is the generated [INDEX.md](INDEX.md); this file is the contract — where RFCs live, when to write one, and [the in-file format](#the-file-format). - -## Layout and naming - -Every RFC has two axes, both encoded in its **path** — `{lifecycle}/{class}/yyyy-mm-dd-topic-title.md`: - -- **Lifecycle** (the top-level folder) is the RFC's status, and an RFC moves between folders as that status changes: - - **`proposed/`** — proposals reviewed before implementation; not yet built (or only partly). - - **`implemented/`** — the decision shipped. The file records what was decided and what was rejected, and is **kept current with what actually shipped**: when the code later moves a file, renames a package, or changes a key/default, the RFC is updated in the same change to match (facts only — paths, names, structure — not the decision itself). See [implemented/AGENTS.md](implemented/AGENTS.md). - - **`rejected/`** — the proposal was considered and declined. Kept for the record so the rejection isn't re-litigated. -- **Class** (the nested folder) is the *kind* of decision — see [Classification](#classification) below. - -The date in the filename is when the topic was **first proposed** (per git history). Cross-references between RFCs use relative markdown links (`[topic](../../implemented/architecture/2026-…-….md)`) — never bare prose or numbers — so they are mechanically checkable and survive moves between folders. - -## Classification - -Each RFC belongs to one path-encoded class from the closed set in `scripts/rfc-index.ts`; the classification gate rejects other folders. [INDEX.md](INDEX.md) is generated from paths, titles, and filename dates, and its freshness is gated. Adding a class requires updating the canonical set and this section. See the [classification](implemented/process/2026-06-20-rfc-classification.md) and [index-generation](implemented/process/2026-07-04-generate-rfc-index-tables.md) RFCs. - -| Class | What it covers | -|---|---| -| `feature` | A new user- or model-facing capability. | -| `bug-fix` | Corrects a defect or closes a gap a postmortem surfaced. | -| `simplification` | Removes code, behavior, or surface area without adding a capability. | -| `architecture` | A structural decision about the **shipped source** — how packages relate, what the runtime vocabulary is. | -| `process` | Tooling, policy, or workflow **around** the code — gates, the package manager, vendoring — not runtime behavior. | -| `testing` | Test infrastructure and strategy. | - -The `architecture` / `process` line: **architecture** is about the source we ship; **process** is the surrounding tooling and workflow. (`refactor` is deliberately absent — it overlaps `simplification`, whose discriminator, "does observable behavior change?", already covers it.) - -## When to write one - -Write an RFC when a decision is **durable** (it shapes the codebase beyond a single function or package), **contested** (there was a real alternative a reasonable engineer might have chosen), and **surprising** (a future reader would otherwise ask "why on earth is it done this way?"). A proposal for substantial future work starts in `proposed/`; a decision already made starts in `implemented/`. Pick the class folder that matches the decision (see [Classification](#classification)). - -Do NOT write one for a mechanical or local choice (a variable name, a one-file refactor), for anything already enforced and explained by a gate or a convention in AGENTS.md, or for a still-provisional decision tagged `TODO(...)` in the code — record those as TODOs and promote to an RFC only once they settle. An RFC is never edited into a *different decision*: supersede it with a new one and cross-link. (Editing an `implemented/` RFC to track where its already-made decision now *lives* — a moved file, a renamed package — is not a different decision and is required, not forbidden; see [implemented/AGENTS.md](implemented/AGENTS.md).) - -## The file format - -Every RFC follows one in-file format, enforced by `pnpm run verify-rfc-format` ([scripts/verify-rfc-format.ts](../../scripts/verify-rfc-format.ts), part of `doc-sync`); the rationale for the format — and the alternatives it rejected — is [the uniform-format RFC](implemented/process/2026-07-05-uniform-rfc-format.md). - -### The header block - -The first three lines of every RFC are exactly: - -```markdown -# RFC: <title> - -Status: <status> -``` - -followed by a blank line. The `Status:` value is one of three forms, and must agree with the lifecycle folder the file sits in — the gate cross-checks them: - -- `Status: proposed` -- `Status: implemented` -- `Status: rejected — <why, in one line>` - -The status carries no dates and no parentheticals: the filename holds the first-proposed date, git holds everything else, and an "accepted in amended form" note is body content (state the amendment where the decision is stated). The rejection reason is the one status with content, because a rejected RFC's verdict is the fact readers come for. - -### The body skeleton - -Every RFC opens its body with `## Problem` — the motivation, written to stand without the solution. What follows depends on the lifecycle; recurring sections use these canonical names and nothing else, while genuinely bespoke technical sections (package topology, wire contracts, schemas) remain free-form between the required ones. - -#### `proposed/` - -```markdown -## Problem -## Proposal -…bespoke sections… -## Alternatives considered -## Acceptance criteria -## Risks -``` - -`## Proposal` is the intended change and may legitimately speak in the future tense — plans, migration steps, and open questions belong here while the work is unbuilt. `## Acceptance criteria` says what observable state means done. `## Risks` covers both what could go wrong and what the change knowingly gives up. - -#### `implemented/` - -```markdown -## Problem -## Decision -…bespoke sections… -## Alternatives considered -## Consequences -``` - -`## Decision` describes shipped reality in the present tense, and the whole file is kept current with it per [implemented/AGENTS.md](implemented/AGENTS.md). `## Consequences` records what the trade-off cost **and** bought. Proposal-era headings are spec-speak here and the gate rejects them: `## Proposal`, `## Plan`, `## Migration plan`, and `## Acceptance criteria` may not appear in an implemented RFC (the [slop checklist](../AGENTS.md) names why). A `## Testing`, `## Deferred`, or `## Related` section is fine where it states present-tense fact. - -#### `rejected/` - -A rejected RFC is the proposal, frozen: it keeps whatever proposal-time sections it had (including `## Acceptance criteria` or `## Plan`), and the verdict lives on the `Status:` line. Only the header block, the `## Problem` opener, a `## Proposal` section, and the Alternatives-considered mandate below apply. - -### Alternatives considered — mandatory - -Every RFC carries an `## Alternatives considered` section: each genuine alternative and why it lost, one bold-led paragraph per alternative or a `### Why not <X>?` subsection per contested one. A decision recorded without what it beat invites re-litigation — the failure RFCs exist to prevent. - -Alternatives are recorded, never invented. An RFC dated before 2026-07-05 whose alternatives are not reconstructible from the record carries this exact comment in place of the section, which the gate accepts for pre-format files only: - -```markdown -<!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> -``` - -### Moving between lifecycles - -Moving a file between lifecycle folders means updating the `Status:` line and re-satisfying that folder's skeleton in the same change — the gate fails the move otherwise. Concretely, `proposed/` → `implemented/` rewrites `## Proposal` into a present-tense `## Decision`, folds `## Acceptance criteria` and `## Risks` into `## Consequences` (or a present-tense `## Testing`/`## Verification` section for what now pins the behavior), and drops plans in favor of what shipped — the rewrite [implemented/AGENTS.md](implemented/AGENTS.md) requires, made mechanical. `proposed/` → `rejected/` only adds the reason to the `Status:` line and freezes the file. - -### Chinese counterparts - -A `.zh.md` counterpart mirrors its English sibling's structure section-for-section under the [i18n contract](../i18n/README.md); the machine-checked header tokens (`# RFC: ` and the `Status:` line) stay in English verbatim. The format gate skips `.zh.md` files — the pairing gate owns their consistency. diff --git a/docs/rfc/implemented/AGENTS.md b/docs/rfc/implemented/AGENTS.md deleted file mode 100644 index 639415ba70..0000000000 --- a/docs/rfc/implemented/AGENTS.md +++ /dev/null @@ -1,11 +0,0 @@ -# AGENTS.md — Implemented RFCs - -These RFCs describe shipped decisions. Follow the [root instructions](../../../AGENTS.md), [documentation standard](../../AGENTS.md), and [RFC format](../README.md#the-file-format); `verify-rfc-format` gates the lifecycle-specific structure. - -## Keep an implemented RFC current with what actually shipped - -Keep paths, symbols, defaults, and mechanisms current in the same change that alters them. Rewrite stale facts in place; do not append change history. - -### This is not a license to rewrite the *decision* - -Update factual realization in place. A reversal of the decision or its rationale requires a new RFC and cross-link; see [rfc/README.md](../README.md). diff --git a/docs/rfc/implemented/process/2026-06-20-rfc-classification.md b/docs/rfc/implemented/process/2026-06-20-rfc-classification.md deleted file mode 100644 index 2162687225..0000000000 --- a/docs/rfc/implemented/process/2026-06-20-rfc-classification.md +++ /dev/null @@ -1,46 +0,0 @@ -# RFC: Classify RFCs by kind via path-encoded subdirectories - -Status: implemented - -## Problem - -`docs/rfc/` grouped RFCs by **lifecycle** only — `proposed/` / `implemented/` / `rejected/`. Nothing recorded what *kind* of decision each RFC was. The index was one flat list per lifecycle, with no way to scan "show me every simplification" or "every testing-strategy decision." A wave of simplification RFCs landing on the same day made the gap concrete: a reader skimming `proposed/` could not tell a new capability from a removal from a tooling-policy change without opening each file. - -The repo's standing bias is [mechanical quality gates over prose guidelines](2026-06-11-quality-gates.md): a convention that isn't machine-checked rots. So a classification scheme here had to be enforceable, not an honor-system header. - -## Decision - -Add a second axis — the RFC's **class** — and encode it in the path: `{lifecycle}/{class}/yyyy-mm-dd-topic.md`. The folder *is* the label. A file's location declares its class, the closed set is "these folders and no others," and the existing [verify-md-links](2026-06-18-markdown-cross-link-lint.md) gate already protects the path rewrites the move required. - -### The closed set of six classes - -| Class | Covers | -|---|---| -| `feature` | A new user- or model-facing capability. | -| `bug-fix` | Corrects a defect or closes a gap a postmortem surfaced. | -| `simplification` | Removes code, behavior, or surface area without adding a capability. | -| `architecture` | A structural decision about the **shipped source** — how packages relate, what the runtime vocabulary is. | -| `process` | Tooling, policy, or workflow **around** the code, not runtime behavior. | -| `testing` | Test infrastructure and strategy. | - -The `architecture` / `process` line: **architecture** is about the source we ship; **process** is the surrounding tooling and workflow. This RFC is itself a `process` decision — it changes how the repo is organized and gated, not what the harness does at runtime — so it lives under `implemented/process/`. - -### Two gates - -Both are `doc-sync` members, in the `verify-md-wrap` style (tsx ESM, verify-don't-generate, exit non-zero on the first violation): - -- **`scripts/verify-rfc-classification.ts`** — the closed set and index freshness. It asserts every file under a lifecycle folder lives in a class folder from the canonical set (a loose `.md` at a lifecycle root, or an unknown class folder, fails), and that the generated [INDEX.md](../../INDEX.md) byte-matches a fresh render from the tree (see [generate the RFC index tables](2026-07-04-generate-rfc-index-tables.md)). The canonical class set lives as a `const` in `scripts/rfc-index.ts` — the machine source of truth shared with the generator — and [the README](../../README.md) documents it in prose; the class *descriptions* stay hand-written, the index is generated. -- **`scripts/verify-doc-refs.ts`** — source comments that cite docs. RFC paths are referenced not only from Markdown but from TypeScript doc comments (root-relative prose like `docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md`). `verify-md-links` never saw those, so the reorg could have silently orphaned them. This gate scans repo-authored `.ts` under `packages/**` and `examples/**` (excluding built `lib/` and `vendor/`) for `docs/….md` tokens, resolves each root-relative, and asserts it exists. It requires the `.md` extension so extensionless prose (`docs/postmortem/0001`, `docs/architecture.md § Extending The Harness`) is left alone. - -## Alternatives considered - -- **A `Classification:` prose line** in each file (next to `Status:`), parsed by the gate. Workable, but it duplicates into the file a fact the path can already carry, and a line can disagree with its folder. Path-encoding makes the label and its storage the same thing — there is nothing to keep in sync. -- **A `refactor` class.** It overlaps `simplification` almost entirely; the only discriminator anyone reached for was "does observable behavior change?", which `simplification` already encodes (it does not). One class, not two. -- **Auto-generating the index** from the filesystem. Rejected here to keep the index hand-written; superseded by [generate the RFC index tables](2026-07-04-generate-rfc-index-tables.md) once stacked proposal waves made the hand-written tables the repo's most conflict-prone docs region — the list is now the fully generated [INDEX.md](../../INDEX.md) while the README prose stays curated. - -## Consequences - -- Every RFC now sits under a class folder, and the index groups by class within each lifecycle. A reader scans one heading to see all simplifications, or all testing decisions. -- Two more fast tsx scripts in the `doc-sync` chain; no new dependency (the mdast/GFM stack was already present for `verify-md-wrap`/`verify-md-links`). -- Adding a class is a deliberate act: amend the `const` in `scripts/rfc-index.ts` and the [Classification section](../../README.md#classification), not just `mkdir` a folder. The gate rejects an unknown folder, so an ad-hoc class can't slip in. -- Source-comment doc references are now gated too — a moved or renamed doc that a `.ts` comment cites fails the pre-push hook, closing a drift class `verify-md-links` structurally could not see. diff --git a/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.md b/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.md deleted file mode 100644 index d854991ad9..0000000000 --- a/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.md +++ /dev/null @@ -1,32 +0,0 @@ -# RFC: Generate the RFC index tables - -Status: implemented - -## Problem - -The RFC index's per-lifecycle/per-class tables list facts that are fully derivable: an RFC's path encodes lifecycle and class, its filename encodes the first-proposed date, and its H1 carries the title. A hand-maintained copy of those facts is also the repo's highest-contention docs hotspot: every proposal wave appends rows to the same few lines, so concurrent RFC branches conflict precisely there while agreeing everywhere else, and each conflict is resolved by hand-merging rows whose content the filesystem already knows. [The classification RFC](2026-06-20-rfc-classification.md) originally kept the index hand-written for curation's sake — but the curated part of the README is the prose, and the prose never conflicts; only the mechanical tables do. - -## Decision - -Keep the curated prose; generate the list. The tables live in [`docs/rfc/INDEX.md`](../../INDEX.md), a **fully generated file** — the curated prose stays in README.md, which carries no index rows at all. [`scripts/rfc-index.ts`](../../../../scripts/rfc-index.ts) is the shared source of truth — the tree walker (owning the closed lifecycle/class sets and the structure rules, including a parseable-H1 requirement) and the renderer (rows from H1 title with any `RFC: ` prefix stripped, plus the filename date, sorted by date then filename, grouped as `### {Class}` sections in canonical class order). Two thin consumers share it: - -- [`scripts/gen-rfc-index.ts`](../../../../scripts/gen-rfc-index.ts) (`pnpm run gen-rfc-index`) rewrites INDEX.md in full from the tree. -- [`scripts/verify-rfc-classification.ts`](../../../../scripts/verify-rfc-classification.ts) (a `doc-sync` member) checks structure, asserts the committed INDEX.md byte-matches a fresh render — the `gen-cordis-catalog`/`verify-cordis-catalog` pattern — and rejects an index-shaped row in the curated README. Freshness subsumes the index-completeness check: a generated-from-disk table is definitionally complete and correctly headed. - -Adding, moving, or deleting an RFC means editing only the RFC file and running the generator; the classification RFC's rejected-alternatives record carries the supersession cross-link. - -## Alternatives considered - -### Why not marker-delimited regions inside README.md? - -The first landed shape: the generator spliced the tables into README.md between `gen-rfc-index` marker comments, under each `## {Lifecycle}` heading. Superseded by the whole-file INDEX.md once the README also absorbed the in-file format contract ([the uniform-format RFC](2026-07-05-uniform-rfc-format.md)): a front-door README hosting hundreds of generated rows dwarfed its curated prose, and splice mechanics (marker pairs, heading checks, outside-region row detection) exist only to protect curated text that a dedicated generated file simply doesn't contain. - -### Why not the verifier-only model? - -It catches mistakes but still makes every proposal edit a shared hotspot in a hand-maintained table, and a failed verifier is strictly more annoying than a generator for a purely mechanical row: the author has already named and placed the file; the index copy adds no information. This is the same hand-list-versus-derivation judgment the [package-inventory proposal](../../proposed/process/2026-06-20-discover-package-inventory.md) applies to tsconfig references and knip stanzas — applied to the one list that demonstrably conflicts. - -## Consequences - -- The generated file is explicit: its banner names the generator, there is no curated region to protect inside it, and the generator refuses to run on a structurally invalid tree. -- A malformed or missing H1 is a hard error in both the generator and the gate — the H1 is now load-bearing as the index title source. -- Concurrent RFC branches resolve index conflicts by rerunning the generator, never by hand-merging rows. diff --git a/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md b/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md deleted file mode 100644 index d5ce28066d..0000000000 --- a/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md +++ /dev/null @@ -1,28 +0,0 @@ -# RFC: One gated in-file format for RFCs - -Status: implemented - -## Problem - -RFC paths encoded lifecycle and class, but file contents still mixed headings, status formats, ADR and proposal templates, and proposal-era sections in implemented records. Authors copied whichever neighbor they found, and lifecycle moves could skip the required rewrite because no gate enforced an in-file contract. - -## Decision - -[README.md § The file format](../../README.md#the-file-format) is the in-file contract — the header block (`# RFC: <title>` plus a dateless, folder-agreeing `Status:` enum whose only content is the rejection reason), the per-lifecycle body skeleton (`Problem` opener everywhere; `Proposal`/`Acceptance criteria`/`Risks` in `proposed/`; present-tense `Decision`/`Consequences` with proposal-era headings banned in `implemented/`; frozen proposal shape in `rejected/`), a mandatory `Alternatives considered` section, and the canonical section vocabulary between which bespoke technical sections stay free-form. `pnpm run verify-rfc-format` ([scripts/verify-rfc-format.ts](../../../../scripts/verify-rfc-format.ts)) enforces every mechanical clause as part of `doc-sync`, so a lifecycle move that skips its rewrite now fails CI instead of review memory. - -The whole corpus was normalized in the same change that defined the format — the pre-release stance: no transition period, no dual-format tolerance. The one grandfather is content, not format: alternatives are recorded, never invented, so a pre-format RFC whose alternatives are not reconstructible from the record carries the exact `rfc-format: alternatives-not-recorded` comment, which the gate accepts only for files dated before this RFC. - -## Alternatives considered - -- **A full rigid template** (one fixed section sequence per lifecycle, every RFC restructured to fit) — rejected: the big design RFCs carry eight to fifteen bespoke technical sections (package topology, wire contracts, schemas) that are load-bearing content, not drift; a rigid sequence would force destructive rewrites now and template-fighting forever. -- **Header-only normalization** (H1 and Status, bodies untouched) — rejected: the debt markers flagged the *body* genre split, and leaving `Context`/`Decision` beside `Problem`/`Proposal` indefinitely resolves nothing. -- **No Status line** (the folder already is the status; the three newest pre-format RFCs (and the zh counterpart of one) omitted the line) — rejected in favor of keeping a self-describing file: the drift risk that motivated dropping it is neutralized by gating the line against the folder instead. -- **Dated status** (`Status: implemented (accepted YYYY-MM-DD)`) — rejected: the acceptance date is narrated history the writing rules keep out of docs; the filename carries first-proposed, git carries the rest, and the gate could check a date's format but never its truth. -- **A bare `# <title>` H1** — rejected: the `RFC: ` prefix is the corpus-majority form and self-describes the genre when a file is read outside its tree; the index generator strips it, so index rows are identical either way. -- **`## What we give up` as the implemented closer** (the README's own phrase for what an RFC records) — rejected: it names only costs, and an honest consequences section records what the trade-off bought as well. -- **Convention without a gate** (write the contract down, enforce by review) — rejected: the slop checklist already outlawed spec-speak in `implemented/` by convention, and nineteen files show what convention alone achieves here. -- **A standalone `FORMAT.md` contract file** — the first landed home; folded into README.md once the generated index moved out to [INDEX.md](../../INDEX.md): with the tables gone the README regained the room, and one front door carrying layout, classification, and format beats splitting the contract across two files. - -## Consequences - -Every RFC now costs slightly more structure, and the mandatory `Alternatives considered` section is deliberate friction: a decision recorded without what it beat invites the re-litigation RFCs exist to prevent. Pre-format RFCs whose alternatives were not reconstructible carry the grandfather comment permanently — an honest gap on the record rather than fabricated rationale. `doc-sync` gains one gate, and moving an RFC between lifecycle folders is now real work at move time (the body rewrite the move always owed) instead of deferred cleanup nothing tracked. The thirty-nine debt markers are gone, resolved by the template they were waiting for. diff --git a/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md b/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md deleted file mode 100644 index 5d40b166bd..0000000000 --- a/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md +++ /dev/null @@ -1,31 +0,0 @@ -# RFC: Prune producer-less vocabulary variants (block cache hints, the `agent` message source, the `continuation` turn trigger) - -Status: implemented - -## Problem - -The merge-extensible vocabulary maps are designed to grow by declaration merging, and the codebase already states the admission policy on `TurnEndReasonMap` (`packages/core/session/src/types.ts`): a variant like `refusal` is "deliberately omitted until" an adapter or loop first emits it. Three declared vocabulary items violated that policy — each had no producer and no consumer, and two had not even a test: - -- **`CacheHint` and its `cache?: CacheHint` block fields** on `TextBlock`/`ToolResultBlock` (`packages/llm/llm/src/types.ts`; the image block carried a third such field, which left with it — see [the drop-image RFC](2026-07-04-drop-image-content-block.md)). Nothing constructed a block with `cache:` anywhere — src, tests, and doc pastes all came up empty — and neither adapter read `.cache`: DeepSeek prompt caching is automatic, so the adapters map `prompt_cache_hit_tokens` OUT of responses without ever sending a hint IN. This was Anthropic-style `cache_control` surface with no provider that could honor it. -- **`MessageSourceMap.agent`** (`{ kind: 'agent'; agentId: string }`, same file). Zero constructors, tests included. Its intended producer shipped without it: the subagent backends send the parent's prompt to the child with no `source`, so it logs as `{ kind: 'user' }`, and the generic envelope renderer interpolates `source.kind` without ever routing on it. -- **`TurnTriggerMap.continuation`** (`packages/core/session/src/types.ts`). The loop structurally cannot emit it — continuation happens *within* a turn as further steps, never as a new turn — and it constructs only `message` and `injection` triggers. The only writer was one hand-built test fixture needing an arbitrary non-message trigger (`packages/support/llm-replay/tests/llm-replay.spec.ts`), which an `injection` trigger serves equally; the only production trigger reader, the ACP bridge, filters on `kind === 'message'`. - -## Decision - -`CacheHint`, its `cache?` block fields, the `agent` message-source variant, and the `continuation` turn-trigger variant are deleted: the shipped vocabulary carries none of them. The llm-replay fixture uses an `injection` trigger (any non-`message` trigger serves its purpose). The type-equiv pastes in [core.md](../../../core-data-structures/core.md) and [session.md](../../../core-data-structures/session.md) match the pruned maps — both symbols keep their rows in `scripts/type-equiv.manifest.json`, since each map survives minus a member — and the [content-block vocabulary RFC](../architecture/2026-06-11-content-block-vocabulary.md)'s consequences record cache hints as producer-gated rather than as having a home, per [implemented/AGENTS.md](../AGENTS.md). - -Each variant returns the day it gains a real producer, exactly as the maps are designed to grow: a caching feature re-adds `cache` together with the adapter that transmits it; subagent attribution re-adds `agent` together with the backend that stamps it and a consumer that routes on it; an auto-continue feature that genuinely starts new turns re-adds `continuation` with the plugin that emits it. - -## Alternatives considered - -### Why not keep them? - -The [content-block vocabulary RFC](../architecture/2026-06-11-content-block-vocabulary.md) listed "cache hints … have a home" as a design consequence, and reserved slots do advertise intent. But an empty slot is contract surface every implementation and consumer must consider (must my adapter honor `cache`? must my renderer route `agent` sources?), and the sibling map's own JSDoc already rejects reservation-without-emitter — `refusal` and `max_turn_requests` are named as variants to add *when something first emits them*, not declared in advance. Holding already-declared dead variants to the same standard makes the vocabulary mean something: if it is in the map, something produces it. - -## Verification - -`rg` for `CacheHint`, the `agent` message-source spelling, and the `continuation` trigger spelling returns only RFC records (this one, and [the drop-image RFC](2026-07-04-drop-image-content-block.md)'s account of the image block's own `cache` field); the llm-replay fixture asserts the same replay behavior with an `injection` trigger; the core-data-structures pastes and the type-equiv manifest are in sync. - -## Consequences - -Nothing operational changed — nothing could construct these values. The mirror-event removals ([the boundary-mirror RFC](2026-06-20-remove-agent-boundary-mirror-events.md), [the stream-chunk RFC](2026-07-02-remove-stream-chunk-mirror.md)) touch only transient `agent/*` events, never the durable vocabulary, so there is no collision. Elsewhere the admission policy already holds: `rejected`, `prompt/blocked`, and `hook/invoked`/`hook/result` each have live producers — this RFC extends the same bar to the three variants that lacked one. The image block's own `cache?` field belongs to [the drop-image RFC](2026-07-04-drop-image-content-block.md), which removed it together with the block; this RFC covers the two fields on the block types that remain. diff --git a/docs/testing.md b/docs/testing.md index 1a2d6cb192..84629aae45 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,13 +1,13 @@ # Testing policy -How this repo tests, tier by tier, and the rules that keep a green suite meaningful. Commands live in root [AGENTS.md](../AGENTS.md); linked RFCs carry the rationale. +How this repo tests, tier by tier, and the rules that keep a green suite meaningful. Commands live in root [AGENTS.md](../AGENTS.md); linked Agent Notes carry the rationale. ## Tiers - **Unit** (`pnpm run test`): vitest over `packages|examples/*/tests/**/*.spec.ts`, colocated with what they test. Every registry gets an HMR-safety test (dispose the contributing fiber, assert cleanup). Prefer edge cases, error paths, event ordering, concurrency races, and permanent contract regressions (see `packages/core/agent-loop/tests/contract-regressions.spec.ts`). - **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate is correctly flagging for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped. -- **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e RFC](rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md)). -- **Snapshot** (`pnpm run test:snapshot`): transport-specific keyless expected outputs cover external presentation. ACP suites boot the real example subprocess, replay a recorded session, and diff normalized JSON-RPC plus the re-persisted log ([ACP snapshot RFC](rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md)); the headless suite independently pins `stream-json` through its real one-shot subprocess. TUI completed journeys replay recorded primary/child JSONL through the real agent loop and tools before projecting ANSI into semantic terminal-state expected outputs; package-local snapshots retain transient renderer states, and a real PTY conversation covers the process boundary ([TUI snapshot RFC](rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md)). Use `pnpm run test:snapshot:record` when a model transcript must change and `pnpm run test:snapshot:refresh` when committed replay input remains correct; review every JSONL and expected-output diff. System-prompt/tool-schema content is pinned by one ACP scenario (`text-turn`) and tokenized in every other fixture, so a prompt or schema edit churns one committed line ([pinned-header RFC](rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)). +- **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)). +- **Snapshot** (`pnpm run test:snapshot`): transport-specific keyless expected outputs cover external presentation. ACP suites boot the real example subprocess, replay a recorded session, and diff normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); the headless suite independently pins `stream-json` through its real one-shot subprocess. TUI completed journeys replay recorded primary/child JSONL through the real agent loop and tools before projecting ANSI into semantic terminal-state expected outputs; package-local snapshots retain transient renderer states, and a real PTY conversation covers the process boundary ([TUI snapshot Agent Note](../.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md)). Use `pnpm run test:snapshot:record` when a model transcript must change and `pnpm run test:snapshot:refresh` when committed replay input remains correct; review every JSONL and expected-output diff. System-prompt/tool-schema content is pinned by one ACP scenario (`text-turn`) and tokenized in every other fixture, so a prompt or schema edit churns one committed line ([pinned-header Agent Note](../.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)). ## The with-key policy: inference is cheap here diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index dfb97deef3..d2339d35a7 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -5,7 +5,7 @@ Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the cordis [events](cordis-catalog/events.md) & [services](cordis-catalog/services.md) catalogs (the wiring a plugin listens to and calls) and [core-data-structures/](core-data-structures/core.md) (the types those signatures move) — this page is the *tools* the agent is offered. -This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator's boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog RFC](rfc/implemented/process/2026-07-02-tool-schema-catalog.md). +This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator's boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog Agent Note](../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md). Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config. The registered tool NAME can be a load-time config (e.g. `tool-subagent`'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog's packages-only scope. @@ -16,11 +16,11 @@ This table connects model-visible tool names to the plugin package and service s | Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Deployment note | | --- | --- | --- | --- | --- | --- | | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userInteraction` | `tool/call`, `tool/result after a UI/provider answers the question` | - | ask_user_question pauses the tool call until the active UI provider returns a human answer. | -| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/code-dispatch per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. | +| `@deepseek-ai/dsh-tools` | `run_code` | `ctx.tools`, `ctx.codeRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/code-dispatch per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. | | `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.tools`, `ctx.bash`, `ctx.tasks at call time for run_in_background` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.tasks` runtime and is collected/stopped through the `task_*` tools from `@deepseek-ai/dsh-tool-tasks`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled. | -| `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect`, `cordis_mount`, `cordis_unmount` | `ctx.tools` | `tool/call`, `tool/result`, `live plugin-tree mutations (mount/unmount)` | - | Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; a full changed request header logs those tool-set changes. | +| `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect`, `cordis_mount`, `cordis_unmount` | `ctx.tools` | `tool/call`, `tool/result`, `live plugin-tree mutations (mount/unmount)` | - | Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; a full changed request header logs those tool-set changes. | | `@deepseek-ai/dsh-tool-fs` | `edit`, `read`, `write` | `ctx.tools`, `ctx.fs`, `ctx.systemPrompt` | `tool/call`, `fs/write-intent or fs/edit-intent for mutations`, `fs/observed after successful file operations`, `tool/result` | - | The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. The tool schemas above are identical with or without the policy plugin. | -| `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.tools`, `ctx.bash`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are bash-backed discovery tools: they run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. | +| `@deepseek-ai/dsh-tool-fs-search` | `glob`, `grep` | `ctx.tools`, `ctx.bash`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. | | `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.tools`, `ctx.skills` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `examples/repl-agent/cordis.yml` and `examples/acp-agent/cordis.yml`. | | `@deepseek-ai/dsh-tool-tasks` | `task_kill`, `task_list`, `task_output` | `ctx.tools`, `ctx.tasks`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `context/message via agent.inject() for background completion notices` | - | The kind-agnostic background-task control surface: a background bash command and a background subagent are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers' `ctx.tasks.start()`. | @@ -121,7 +121,7 @@ Execute a TypeScript program against the available tools. Write the BODY of an a Source: [`packages/core/tools/src/code-mode.ts`](../packages/core/tools/src/code-mode.ts) -Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. +Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. ## `@deepseek-ai/dsh-tool-bash` @@ -239,7 +239,7 @@ Dispose a plugin previously mounted with cordis_mount, by id. All its registrati Source: [`packages/cordis/tool-cordis/src/index.ts`](../packages/cordis/tool-cordis/src/index.ts) -Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; a full changed request header logs those tool-set changes. +Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; a full changed request header logs those tool-set changes. ## `@deepseek-ai/dsh-tool-fs` @@ -391,7 +391,7 @@ Search file contents with a ripgrep regular expression. Returns matching lines w Source: [`packages/fs/tool-fs-search/src/index.ts`](../packages/fs/tool-fs-search/src/index.ts) -glob and grep are bash-backed discovery tools: they run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. +glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. ## `@deepseek-ai/dsh-tool-skill` 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<Config> = 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<Config> = 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<Config> = 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 6b482f1401..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 d9e6f10b80..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<string>) => Promise<string> + } +} + +// 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<string>) => Promise<string> + } +} + +// 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 d9c6def99a..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 b935c905df..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<MyCapResult> +} + +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<MyCapResult> { + // 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 2a077aa22e..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<MyCapResult> } @@ -101,7 +103,7 @@ import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/ class MyCapLocal extends MyCapService { async execute(request: MyCapRequest): Promise<MyCapResult> { - // 具体实现 + // 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<StreamChunk> { + // 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<Config> = 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<StreamChunk> { + // 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<StreamChunk> { + 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<StreamChunk> { - // 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<Config> = 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<StreamChunk> { - // 1. 每个内容块以 block-start 开始 +async function* exampleChunks(): AsyncIterable<StreamChunk> { + // 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<StreamChunk> { }, } - // 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<StreamChunk> { ## 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<StreamChunk> { - 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 02b8fe0465..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 <text>" 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 27725fc6b9..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 189b762185..03dcf7edac 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,7 +12,9 @@ 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', '**/*.mjs', '*.config.ts', // root tool configs (vitest, tsdown) — no project service @@ -21,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, ], @@ -108,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. @@ -125,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/README.md b/examples/README.md index abeee1d97e..6524a36f7f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -33,11 +33,15 @@ The full-screen terminal sibling of `repl-agent`: it reuses the same coding back Run with: `pnpm run demo:tui` (needs `DEEPSEEK_API_KEY`). See [tui-agent/README.md](tui-agent/README.md) for controls and composition. +## jsonrpc-agent + +An unattended coding agent driven through the Python SDK: JSON-RPC stdio, foreground-only `bash`, `read` / `write` / `edit`, one foreground `subagent`, `todo_write`, JSONL persistence, and compaction. It excludes terminal UI, stdout logging, approvals, skills, and background task controls. See [jsonrpc-agent/README.md](jsonrpc-agent/README.md). + ## cordis-agent The **self-referential** demo: the coding spine plus [`@deepseek-ai/dsh-tool-cordis`](../packages/cordis/tool-cordis), whose three tools (`cordis_inspect` / `cordis_mount` / `cordis_unmount`) let the agent inspect the live cordis runtime it runs inside, mount model-written plugins into it (an event listener, a brand-new tool for itself, or a service another mount injects), and dispose them again — all dynamic mounts grouped under one `cordis-dynamic` fiber subtree. The `ctx.fs`/`ctx.web` services ride along provider-only, as the capabilities those plugins build on. -Run with: `pnpm run demo:cordis` (needs `DEEPSEEK_API_KEY`). See [cordis-agent/README.md](cordis-agent/README.md) for the staged demo script and [the toolset RFC](../docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md) for the design and sandbox caveats. +Run with: `pnpm run demo:cordis` (needs `DEEPSEEK_API_KEY`). See [cordis-agent/README.md](cordis-agent/README.md) for the staged demo script and [the toolset Agent Note](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md) for the design and sandbox caveats. ## acp-agent diff --git a/examples/acp-agent/README.md b/examples/acp-agent/README.md index 000017dd90..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,19 +29,19 @@ 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 RFC](../../docs/rfc/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) -This example hosts the ACP snapshot suite. It replays through `dsh-llm-replay`, which reconstructs model streams from `assistant/chunk` events in each scenario's session JSONL. Recording runs the real ACP agent and harvests its logs; refresh keeps the committed transcript as mock input and rewrites current replay outputs. `replay.override.json` covers throw and hang cases that chunks cannot express, and an optional `workspace/` seeds files. The [snapshot RFC](../../docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md) owns the ACP harness design. +This example hosts the ACP snapshot suite. It replays through `dsh-llm-replay`, which reconstructs model streams from `assistant/chunk` events in each scenario's session JSONL. Recording runs the real ACP agent and harvests its logs; refresh keeps the committed transcript as mock input and rewrites current replay outputs. `replay.override.json` covers throw and hang cases that chunks cannot express, and an optional `workspace/` seeds files. The [snapshot Agent Note](../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md) owns the ACP harness design. ## 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 RFC § Escalation](../../docs/rfc/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 194ff3dee0..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<br/>@deepseek-ai/dsh-sandbox-local"] cfg --> plugin_acp_sandbox + plugin_acp_sandbox_policy["sandbox-policy<br/>@deepseek-ai/dsh-sandbox-policy"] + cfg --> plugin_acp_sandbox_policy plugin_acp_bash["bash<br/>@deepseek-ai/dsh-bash-sandbox"] cfg --> plugin_acp_bash plugin_acp_approval["approval<br/>@deepseek-ai/dsh-user-approval"] @@ -27,6 +29,10 @@ flowchart LR bundle_agent_core --> spine_sessions["ctx.sessions"] bundle_agent_core --> spine_tools["ctx.tools + tool-bash"] bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"] + plugin_acp_token_meter["token-meter<br/>@deepseek-ai/dsh-token-meter"] + cfg --> plugin_acp_token_meter + plugin_acp_compact_basic["compact-basic<br/>@deepseek-ai/dsh-compact-basic"] + cfg --> plugin_acp_compact_basic plugin_acp_subagent["subagent<br/>@deepseek-ai/dsh-subagent"] cfg --> plugin_acp_subagent plugin_acp_subagent_spawn["subagent-spawn<br/>@deepseek-ai/dsh-subagent-spawn"] @@ -45,6 +51,12 @@ flowchart LR cfg --> plugin_acp_tool_todo plugin_acp_repeat_tool_guard["repeat-tool-guard<br/>@deepseek-ai/dsh-repeat-tool-guard"] cfg --> plugin_acp_repeat_tool_guard + plugin_acp_fs_sandbox["fs-sandbox<br/>@deepseek-ai/dsh-fs-sandbox"] + cfg --> plugin_acp_fs_sandbox + plugin_acp_fs_policy["fs-policy<br/>@deepseek-ai/dsh-fs-policy"] + cfg --> plugin_acp_fs_policy + plugin_acp_tool_fs["tool-fs<br/>@deepseek-ai/dsh-tool-fs"] + cfg --> plugin_acp_tool_fs plugin_acp_hooks_claude["hooks-claude<br/>@deepseek-ai/dsh-hooks-claude"] cfg --> plugin_acp_hooks_claude plugin_acp_hooks_codex["hooks-codex<br/>@deepseek-ai/dsh-hooks-codex"] @@ -55,10 +67,13 @@ 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` | | `acp-agent` | `@deepseek-ai/dsh-acp-demo` | +| `token-meter` | `@deepseek-ai/dsh-token-meter` | +| `compact-basic` | `@deepseek-ai/dsh-compact-basic` | | `subagent` | `@deepseek-ai/dsh-subagent` | | `subagent-spawn` | `@deepseek-ai/dsh-subagent-spawn` | | `subagent-fork` | `@deepseek-ai/dsh-subagent-fork` | @@ -68,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 cb8890dae7..bcdcb75f73 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' @@ -48,6 +56,23 @@ Verify your work by running the code or tests. Keep answers brief and factual. +# Replay-aware request pressure with one service-wide context window. +- id: token-meter + name: '@deepseek-ai/dsh-token-meter' + config: + # FIXME: Resolve compaction config per model; this capacity assumes a 256k context window. + contextWindow: 256000 + +# 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 + name: '@deepseek-ai/dsh-compact-basic' + config: + thresholdRatio: 0.8 + retainTokens: 20480 + maxTokens: 8192 + compactionRetries: 1 + # Expose fresh-child `spawn` and completed-prefix `fork` through separate tool # names so multi-child scenarios exercise both transports. These leaves follow # the app because it provides `ctx.agents` and `ctx.tools`. @@ -95,6 +120,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/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 37b420378c..792788245d 100644 --- a/examples/acp-agent/tests/acp.snapshot.ts +++ b/examples/acp-agent/tests/acp.snapshot.ts @@ -9,8 +9,8 @@ import { defineAcpSnapshotSuite, type Scenario, type SnapshotSuiteOptions } from * uniformity guard, the fixture guards). Fixtures live under `snapshots/<name>/`; * `pnpm run test:snapshot:record` re-records model transcripts against the real * API; `pnpm run test:snapshot:refresh` rewrites current replay expected outputs keyless. - * See the package README (packages/support/acp-snapshot) and the snapshot RFC, - * docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md. + * See the package README (packages/support/acp-snapshot) and the snapshot Agent Note, + * .agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md. */ // The dsh-acp-demo bin (the demo:acp entry), this example's cordis.yml, and @@ -53,24 +53,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 @@ -138,7 +139,7 @@ const SCENARIOS: Scenario[] = [ // (the model's reaction to a deny/block/force-continue is part of the captured transcript). // SessionStart/SubagentStart are excluded because detached injection races log // order; SubagentStop writes no transcript, so an expected output could not prove it ran. - // Unit tests cover those points; the hook-snapshot-matrix RFC owns the rationale. + // Unit tests cover those points; the hook-snapshot-matrix Agent Note owns the rationale. { name: 'hook-cc-promptsubmit-context', hasModelTurn: true, recorded: true }, { name: 'hook-cc-pretool-deny', hasModelTurn: true, recorded: true }, { name: 'hook-cc-pretool-ask', hasModelTurn: true, recorded: true }, @@ -175,6 +176,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..0852ef7513 --- /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"} +{"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> 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/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<string>; + /** 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<string>; + /** 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<string>; /** 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<string, unknown>; }): Promise<string>; + /** 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<string>; } ``` 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..853b446f81 100644 --- a/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl +++ b/examples/acp-agent/tests/snapshots/bash-spill/session.jsonl @@ -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/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<string>; + /** 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<string>; + /** 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<string>; /** 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<string, unknown>; }): Promise<string>; + /** 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<string>; } ``` 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/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<string>; + /** 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<string>; + /** 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<string>; /** 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<string, unknown>; }): Promise<string>; + /** 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<string>; } ``` 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..a20dbcd999 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 @@ -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":"<path>./nested/task.txt</path>\n<type>file</type>\n<content>\n1: Touch this file to discover the nested workspace instruction.\n\n(End of file - total 1 lines)\n</content>"}} {"type":"tool/result","seq":86,"time":1783921767271,"data":{"turn":1,"step":1,"callId":"call_00_6APApmaKLRDlXKMdIcWL5139","content":[{"type":"text","text":"<path>/var/folders/_g/59jgff8x2gqd39f5vy1wnbfc0000gn/T/acp-snap-cwd-uorU26/nested/task.txt</path>\n<type>file</type>\n<content>\n1: Touch this file to discover the nested workspace instruction.\n\n(End of file - total 1 lines)\n</content>"}],"isError":false,"meta":{"logs":[]}},"sourceEventSeqs":[84],"surfaceOp":"append"} -{"type":"context/message","seq":87,"time":1783921767272,"data":{"content":[{"type":"text","text":"<system-reminder>\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</system-reminder>"}],"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":"<system-reminder>\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</system-reminder>"}],"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<string>; /** 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<string>; } ``` 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..dc9e4a844a 100644 --- a/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl +++ b/examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl @@ -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<ToolExecutionResult>\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<void>;\n }\n export interface AgentOptions {\n provider?: string;\n model?: string;\n }\n export type AgentStatus = 'idle' | 'running' | 'disposed';\n export type Branded<B extends string> = 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<ToolExecuteReturn>;\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<ToolExecution>) => 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<string, unknown>;\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<ToolExecutionResult>\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<void>;\n }\n export interface AgentOptions {\n provider?: string;\n model?: string;\n }\n export type AgentStatus = 'idle' | 'running' | 'disposed';\n export type Branded<B extends string> = 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<ToolExecuteReturn>;\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<ToolExecution>) => 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<string, unknown>;\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<ToolExecutionResult>\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<void>;\n }\n export interface AgentOptions {\n provider?: string;\n model?: string;\n }\n export type AgentStatus = 'idle' | 'running' | 'disposed';\n export type Branded<B extends string> = 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<ToolExecuteReturn>;\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<ToolExecution>) => 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<string, unknown>;\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<ToolExecutionResult>\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<void>;\n }\n export interface AgentOptions {\n provider?: string;\n model?: string;\n }\n export type AgentStatus = 'idle' | 'running' | 'disposed';\n export type Branded<B extends string> = 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<ToolExecuteReturn>;\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<ToolExecution>) => 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<string, unknown>;\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<ToolRegistry>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision>\nwaterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() vetoes the chain."}}]}}} {"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"{{sessionId}}","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"CORDIS_INSPECT_JSDOC_OK"}}}} diff --git a/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl b/examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl index 2a33a4e15e..b3931b079b 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":"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..af1afd3c4a 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":"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-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..f796b0b473 --- /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"} +{"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":"<path>/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-vmEGzd/escalated.md</path>\n<type>file</type>\n<content>\nCreated file\n</content>"}],"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/hook-cc-pretool-ask/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl index dcd496f61c..a1f46a781b 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 @@ -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-stop-continue/session.jsonl b/examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl index 5f6b0e6e9e..baa6a92e9a 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"} +{"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-stop-continue/session.jsonl b/examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl index bba879203d..cee758076c 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"} +{"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/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/permission-switching/session.jsonl b/examples/acp-agent/tests/snapshots/permission-switching/session.jsonl index 4a62de6642..43fc43d979 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":"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/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/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/workspace-context/session.jsonl b/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl index b133708b9a..f50e124b32 100644 --- a/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl +++ b/examples/acp-agent/tests/snapshots/workspace-context/session.jsonl @@ -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":"<path>{{cwd}}/nested/task.txt</path>\n<type>file</type>\n<content>\n1: snapshot task\n\n(End of file - total 1 lines)\n</content>"}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"} -{"type":"context/message","seq":12,"time":1783778297072,"data":{"content":[{"type":"text","text":"<system-reminder>\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</system-reminder>"}],"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":"<system-reminder>\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</system-reminder>"}],"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/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`). -<!-- dsh-user-approval-policy:never --> - -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> 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 <value>` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise<any>` — 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<any[]>` — 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<any[]>` — 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 <json-value>`)." - }, - "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/cordis-agent/README.md b/examples/cordis-agent/README.md index 5900d6a0f0..310fbb30af 100644 --- a/examples/cordis-agent/README.md +++ b/examples/cordis-agent/README.md @@ -1,6 +1,6 @@ # cordis-agent -The self-referential harness demo: the coding spine (DeepSeek V4 + local bash on the stdio chat app) plus [`@deepseek-ai/dsh-tool-cordis`](../../packages/cordis/tool-cordis/README.md), which hands the model three tools over the **live cordis runtime it is running inside** — inspect it, mount new plugins into it, and dispose them again. The `ctx.fs` and `ctx.web` services are mounted (provider-only, no model-facing file/web tools) so the plugins the agent writes have real capabilities to build on; Node built-ins are trapped in the sandbox and redirect to those services. The design (sandbox semantics, mount lifecycle, cross-mount composition, caveats) lives in [the toolset RFC](../../docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). +The self-referential harness demo: the coding spine (DeepSeek V4 + local bash on the stdio chat app) plus [`@deepseek-ai/dsh-tool-cordis`](../../packages/cordis/tool-cordis/README.md), which hands the model three tools over the **live cordis runtime it is running inside** — inspect it, mount new plugins into it, and dispose them again. The `ctx.fs` and `ctx.web` services are mounted (provider-only, no model-facing file/web tools) so the plugins the agent writes have real capabilities to build on; Node built-ins are trapped in the sandbox and redirect to those services. The design (sandbox semantics, mount lifecycle, cross-mount composition, caveats) lives in [the toolset Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). ## Run it diff --git a/examples/cordis-agent/cordis.yml b/examples/cordis-agent/cordis.yml index a567705cbd..7c7c010c3a 100644 --- a/examples/cordis-agent/cordis.yml +++ b/examples/cordis-agent/cordis.yml @@ -5,7 +5,7 @@ # Trust stance: the vm and context façade limit accidental global/framework # access but are not a security boundary; mounted code can reach live capabilities # such as `ctx.bash`. Grant this toolset like bash access. See -# ../../docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md. +# ../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md. # Hot-module reload for the dev/demo loop (needs `node --expose-internals`). - id: hmr diff --git a/examples/jsonrpc-agent/README.md b/examples/jsonrpc-agent/README.md new file mode 100644 index 0000000000..83c6e98fa5 --- /dev/null +++ b/examples/jsonrpc-agent/README.md @@ -0,0 +1,25 @@ +# jsonrpc-agent + +The unattended coding-agent composition for the Python SDK's bundled JSON-RPC runtime. It intentionally loads no terminal UI, console logger, approval surface, or user-interaction tool because stdout belongs to the SDK protocol and turns are driven by the SDK. + +The model-facing tools are: + +- `bash`, foreground only +- `read`, `write`, and `edit` +- `subagent`, using one foreground in-process spawn provider +- `todo_write` + +The surrounding runtime also loads JSONL session persistence and automatic context compaction. `maxTokensAsSuccess` keeps a token-limited model turn as an accepted evaluation result while preserving its `max-tokens` reason. + +## Runtime environment + +| Variable | Purpose | +|---|---| +| `DEEPSEEK_API_KEY` | Credential passed to the OpenAI-compatible host endpoint | +| `DEEPSEEK_BASE_URL` | Host endpoint used by `dsh-llm-deepseek` | +| `DSH_CWD` | Agent workspace for bash and filesystem tools | +| `DSH_MAX_TOKENS_AS_SUCCESS` | `true` (default) accepts token-limited results; `false` reports them as errors | +| `DSH_SESSION_ROOT` | JSONL trajectory directory | +| `DSH_SYSTEM_PROMPT` | Deployment-provided coding persona | + +Pass the config path through the Python SDK's `cordis` option or `DSH_CORDIS_CONFIG`. The bundled executable already carries every plugin named by this file; the target machine does not need Node.js. diff --git a/examples/jsonrpc-agent/cordis.yml b/examples/jsonrpc-agent/cordis.yml new file mode 100644 index 0000000000..00ef48f4ed --- /dev/null +++ b/examples/jsonrpc-agent/cordis.yml @@ -0,0 +1,74 @@ +# Unattended coding-agent deployment for the bundled dsh-jsonrpc-agent runtime. +# stdout is reserved for JSON-RPC; do not add a console logger or terminal UI. + +- id: jsonrpc + name: '@deepseek-ai/dsh-jsonrpc' + config: + maxTokensAsSuccess: !!js "process.env.DSH_MAX_TOKENS_AS_SUCCESS === undefined ? true : JSON.parse(process.env.DSH_MAX_TOKENS_AS_SUCCESS)" + +- id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + config: + apiKey: !!js process.env.DEEPSEEK_API_KEY + baseURL: !!js process.env.DEEPSEEK_BASE_URL + +- id: bash + name: '@deepseek-ai/dsh-bash-local' + config: + cwd: !!js process.env.DSH_CWD ?? process.cwd() + timeoutMs: 60000 + +- id: agent-spine + name: '@deepseek-ai/dsh-agent-spine-demo' + config: + persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a coding agent.' + workspaceContext: false + skills: + enabled: false + toolBash: + enableRunInBackground: false + toolTasks: false + +- id: sessions + name: '@deepseek-ai/dsh-session-persistence-jsonl' + config: + root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions' + +- id: subagent + name: '@deepseek-ai/dsh-subagent' + +- id: subagent-spawn + name: '@deepseek-ai/dsh-subagent-spawn' + config: + providerName: spawn + +- id: tool-subagent + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: spawn + toolName: subagent + enableRunInBackground: false + +- id: tool-todo + name: '@deepseek-ai/dsh-tool-todo' + +- id: fs-local + name: '@deepseek-ai/dsh-fs-local' + config: + cwd: !!js process.env.DSH_CWD ?? process.cwd() + +- id: fs-policy + name: '@deepseek-ai/dsh-fs-policy' + +- id: tool-fs + name: '@deepseek-ai/dsh-tool-fs' + +- id: compact-basic + name: '@deepseek-ai/dsh-compact-basic' + config: + contextWindow: 128000 + thresholdRatio: 0.8 + retainTokens: 20480 + summarizationModel: '' + maxTokens: 8192 + compactionRetries: 1 diff --git a/examples/jsonrpc-agent/package.json b/examples/jsonrpc-agent/package.json new file mode 100644 index 0000000000..080b0649a6 --- /dev/null +++ b/examples/jsonrpc-agent/package.json @@ -0,0 +1,7 @@ +{ + "name": "jsonrpc-agent-example", + "private": true, + "version": "0.0.1", + "type": "module", + "description": "Unattended JSON-RPC coding-agent composition" +} diff --git a/examples/jsonrpc-agent/tests/keyless-smoke.e2e.ts b/examples/jsonrpc-agent/tests/keyless-smoke.e2e.ts new file mode 100644 index 0000000000..c99b2c3c50 --- /dev/null +++ b/examples/jsonrpc-agent/tests/keyless-smoke.e2e.ts @@ -0,0 +1,194 @@ +import { spawn } from 'node:child_process' +import { createServer } from 'node:http' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +const binScript = fileURLToPath(new URL('../../../packages/examples/jsonrpc-demo/src/bin.ts', import.meta.url)) +const configPath = fileURLToPath(new URL('../cordis.yml', import.meta.url)) +const repoRoot = fileURLToPath(new URL('../../..', import.meta.url)) + +function waitForLine( + lines: string[], + predicate: (value: Record<string, unknown>) => boolean, + stderr: () => string, +): Promise<Record<string, unknown>> { + return new Promise((resolve, reject) => { + const deadline = Date.now() + 30_000 + const poll = (): void => { + while (lines.length > 0) { + const line = lines.shift()! + if (!line.trim()) continue + try { + const value = JSON.parse(line) as Record<string, unknown> + if (predicate(value)) { + resolve(value) + return + } + } catch { + reject(new Error(`non-JSON stdout from JSON-RPC agent runtime: ${line}`)) + return + } + } + if (Date.now() >= deadline) { + reject(new Error(`timed out waiting for JSON-RPC response; stderr=${stderr()}`)) + return + } + setTimeout(poll, 10) + } + poll() + }) +} + +describe('jsonrpc-agent keyless smoke', () => { + it.each([ + { label: 'accepts max-token results by default', envValue: undefined, expectedStatus: 'ok' }, + { label: 'accepts max-token results when enabled through env', envValue: 'true', expectedStatus: 'ok' }, + { label: 'reports max-token results as errors when disabled through env', envValue: 'false', expectedStatus: 'error' }, + ])('$label', async ({ envValue, expectedStatus }) => { + const root = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-agent-smoke-')) + const modelRequests: Record<string, unknown>[] = [] + const modelServer = createServer((request, response) => { + let body = '' + request.setEncoding('utf8') + request.on('data', (chunk: string) => { body += chunk }) + request.on('end', () => { + modelRequests.push(JSON.parse(body) as Record<string, unknown>) + response.writeHead(200, { 'content-type': 'text/event-stream' }) + response.write('data: {"choices":[{"delta":{"role":"assistant","content":null}}]}\n\n') + response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n') + response.write('data: {"choices":[{"delta":{},"finish_reason":"length"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n') + response.end('data: [DONE]\n\n') + }) + }) + await new Promise<void>(resolve => modelServer.listen(0, '127.0.0.1', resolve)) + const address = modelServer.address() + if (address === null || typeof address === 'string') throw new Error('model server did not bind a TCP port') + const child = spawn(process.execPath, [ + '--expose-internals', + '--import', + 'tsx', + binScript, + configPath, + ], { + cwd: repoRoot, + env: { + ...process.env, + DEEPSEEK_API_KEY: 'keyless-smoke-no-call', + DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`, + DSH_CWD: root, + DSH_SESSION_ROOT: join(root, '.sessions'), + ...(envValue === undefined ? {} : { DSH_MAX_TOKENS_AS_SUCCESS: envValue }), + }, + stdio: ['pipe', 'pipe', 'pipe'], + }) + const lines: string[] = [] + let stdoutBuffer = '' + let stderr = '' + child.stdout.setEncoding('utf8') + child.stdout.on('data', (chunk: string) => { + stdoutBuffer += chunk + const parts = stdoutBuffer.split('\n') + stdoutBuffer = parts.pop() ?? '' + lines.push(...parts) + }) + child.stderr.setEncoding('utf8') + child.stderr.on('data', (chunk: string) => { stderr += chunk }) + + try { + child.stdin.write(`${JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { cwd: root, provider: 'deepseek', model: 'deepseek-v4-pro' }, + })}\n`) + const initialized = await waitForLine(lines, value => value.id === 1, () => stderr) + expect(initialized).toMatchObject({ + jsonrpc: '2.0', + id: 1, + result: { serverInfo: { name: 'deepseek-harness-sdk-runtime' } }, + }) + + child.stdin.write(`${JSON.stringify({ + jsonrpc: '2.0', + id: 2, + method: 'session/prompt', + params: { sessionId: 'main', contentBlocks: [{ type: 'text', text: 'inspect tools' }] }, + })}\n`) + const finished = await waitForLine(lines, value => value.method === 'session.finished', () => stderr) + expect(finished).toMatchObject({ + jsonrpc: '2.0', + method: 'session.finished', + params: { + sessionId: 'main', + status: expectedStatus, + reason: { kind: 'max-tokens' }, + }, + }) + const prompt = await waitForLine(lines, value => value.id === 2, () => stderr) + expect(prompt).toMatchObject({ jsonrpc: '2.0', id: 2, result: { accepted: true } }) + const tools = modelRequests[0]?.tools as { function?: { name?: string } }[] + expect(tools.map(tool => tool.function?.name).sort()).toEqual([ + 'bash', + 'edit', + 'read', + 'subagent', + 'todo_write', + 'write', + ]) + + child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`) + const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr) + expect(shutdown).toMatchObject({ jsonrpc: '2.0', id: 3, result: {} }) + if (child.exitCode === null) { + await new Promise<void>((resolve, reject) => { + child.once('exit', (code) => { + if (code === 0) resolve() + else reject(new Error(`runtime exited ${code}; stderr=${stderr}`)) + }) + }) + } else { + expect(child.exitCode, stderr).toBe(0) + } + } finally { + if (child.exitCode === null) child.kill('SIGKILL') + await new Promise<void>(resolve => modelServer.close(() => { resolve() })) + await rm(root, { recursive: true, force: true }) + } + }, 40_000) + + it('rejects an invalid max-token success env value', async () => { + const child = spawn(process.execPath, [ + '--expose-internals', + '--import', + 'tsx', + binScript, + configPath, + ], { + cwd: repoRoot, + env: { + ...process.env, + DEEPSEEK_API_KEY: 'keyless-smoke-no-call', + DSH_MAX_TOKENS_AS_SUCCESS: 'sometimes', + }, + stdio: ['ignore', 'pipe', 'pipe'], + }) + let stdout = '' + let stderr = '' + child.stdout.setEncoding('utf8') + child.stdout.on('data', (chunk: string) => { stdout += chunk }) + child.stderr.setEncoding('utf8') + child.stderr.on('data', (chunk: string) => { stderr += chunk }) + + const exitCode = await new Promise<number | null>((resolve, reject) => { + child.once('error', reject) + child.once('exit', resolve) + }) + + expect(exitCode, stderr).toBe(1) + expect(stdout).toBe('') + expect(stderr).toContain('plugin(s) failed to load: @deepseek-ai/dsh-jsonrpc') + }, 10_000) +}) diff --git a/examples/package.json b/examples/package.json index 76349b2ea4..687c624536 100644 --- a/examples/package.json +++ b/examples/package.json @@ -3,26 +3,32 @@ "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:*", "@deepseek-ai/dsh-acp-demo": "workspace:*", + "@deepseek-ai/dsh-agent-spine-demo": "workspace:*", "@deepseek-ai/dsh-bash-local": "workspace:*", "@deepseek-ai/dsh-bash-sandbox": "workspace:*", "@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-fs-sandbox": "workspace:^", "@deepseek-ai/dsh-hooks-claude": "workspace:*", "@deepseek-ai/dsh-hooks-codex": "workspace:*", + "@deepseek-ai/dsh-jsonrpc": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-llm-deepseek": "workspace:*", "@deepseek-ai/dsh-llm-replay": "workspace:*", "@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:*", "@deepseek-ai/dsh-stdio-demo": "workspace:*", diff --git a/examples/repl-agent/README.md b/examples/repl-agent/README.md index 7145873e9a..f89e24618c 100644 --- a/examples/repl-agent/README.md +++ b/examples/repl-agent/README.md @@ -27,7 +27,7 @@ The id is wired through `cordis.yml` (`resumeSessionId: !!js process.env.RESUME_ ## Code Mode -[`code-mode.cordis.yml`](code-mode.cordis.yml) overlays the same tree with the worker-thread runtime and `tools: { mode: code }`. The model receives one `run_code` transport plus a generated TypeScript SDK for the visible tools; only program output returns to model context. Use `mode: both` to expose native calls alongside `run_code`. See the [Code Mode RFC](../../docs/rfc/implemented/feature/2026-06-15-code-mode.md) for the execution contract. +[`code-mode.cordis.yml`](code-mode.cordis.yml) overlays the same tree with the worker-thread runtime and `tools: { mode: code }`. The model receives one `run_code` transport plus a generated TypeScript SDK for the visible tools; only program output returns to model context. Use `mode: both` to expose native calls alongside `run_code`. See the [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) for the execution contract. ```sh pnpm run demo:code-mode # this overlay under the REPL (default UI) @@ -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<br/>@deepseek-ai/dsh-token-meter"] cfg --> plugin_repl_token_meter + plugin_repl_tool_result_prune["tool-result-prune<br/>@deepseek-ai/dsh-compact-tool-result-prune"] + cfg --> plugin_repl_tool_result_prune plugin_repl_compact_basic["compact-basic<br/>@deepseek-ai/dsh-compact-basic"] cfg --> plugin_repl_compact_basic plugin_repl_subagent["subagent<br/>@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 4428d5f3a8..6e78f7af98 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 @@ -111,9 +115,10 @@ - id: tool-fs name: '@deepseek-ai/dsh-tool-fs' -# Bash-backed discovery tools (glob/grep): fixed ripgrep commands through the -# local bash executor above — not ctx.fs. Capped results save the complete -# formatted list through the spill backend below (ctx.spillStore, optional). +# Bash-backed discovery tools (glob/grep): if the local bash executor above +# can find rg, register fixed ripgrep commands — not ctx.fs. Capped results +# save the complete formatted list through the spill backend below +# (ctx.spillStore, optional). - id: tool-fs-search name: '@deepseek-ai/dsh-tool-fs-search' 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/README.md b/examples/tui-agent/README.md index 6646649b1b..fb8b10e7f4 100644 --- a/examples/tui-agent/README.md +++ b/examples/tui-agent/README.md @@ -20,4 +20,4 @@ Run `pnpm run demo:code-mode tui` for the sibling Code Mode overlay. ## Snapshot tests -`tests/snapshots/<scenario>/session.jsonl` supplies recorded user prompts and model chunks; sibling child logs drive subagents and workflows. The keyless suite executes those scripts through the real loop and tool implementations, then compares readable expected terminal cell/style output. Use `pnpm run test:snapshot:refresh` for presentation-only changes and `pnpm run test:snapshot:record` with a DeepSeek key when a recorded model journey changes. The implemented [TUI snapshot RFC](../../docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md) owns the scenario matrix and the split between recorded journeys, transient package snapshots, and PTY coverage. +`tests/snapshots/<scenario>/session.jsonl` supplies recorded user prompts and model chunks; sibling child logs drive subagents and workflows. The keyless suite executes those scripts through the real loop and tool implementations, then compares readable expected terminal cell/style output. Use `pnpm run test:snapshot:refresh` for presentation-only changes and `pnpm run test:snapshot:record` with a DeepSeek key when a recorded model journey changes. The implemented [TUI snapshot Agent Note](../../.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md) owns the scenario matrix and the split between recorded journeys, transient package snapshots, and PTY coverage. diff --git a/knip.json b/knip.json index 2c3465ab38..d8e543e75c 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"] @@ -18,6 +18,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/native/README.md b/native/README.md new file mode 100644 index 0000000000..983e67f740 --- /dev/null +++ b/native/README.md @@ -0,0 +1,20 @@ +# native/ + +Source of record for `node-addon-landlock-run`, the Landlock self-restrict-then-exec launcher the harness consumes from npm (`packages/sandbox/sandbox-local`, `packages/bash/bash-sandbox`). Launcher development happens HERE, next to the consumers; the standalone repository is the release mirror that packs and publishes the npm package family. + +## Release mirror + +| Directory | Mirror repo | Last exported release | Commit | +|---|---|---|---| +| `landlock-run/` | https://github.com/deepseek-harness/node-addon-landlock-run | `v0.0.1` | `614f7fd7dc11e6eaceefba9e7ff1fbe28b51ba22` | + +The subtree is a self-contained pnpm workspace with its own `AGENTS.md`, docs, gates, and lockfile; it is NOT part of the harness workspace (`pnpm-workspace.yaml` does not include it), so harness installs, builds, and CI gates never touch it. The mirror's `.github/` stays out of the subtree — [.github/workflows/landlock-run.yml](../.github/workflows/landlock-run.yml) (manual dispatch) runs the subtree's CI legs here, and a change to those legs is mirrored into the mirror's `ci.yml` at the next export. + +## Export procedure (cutting a release) + +1. Land the launcher change here through a normal harness PR; dispatch the `Landlock Run` workflow and get its legs green. +2. In the mirror checkout, replace everything except `.github/`: `git -C <mirror> rm -rq -- . ':!.github'`, then `git -C <harness> archive HEAD:native/landlock-run | tar -x -C <mirror>`, then `git -C <mirror> add -A` and commit. +3. In the mirror, follow its release checklist (`docs/release.md`): `pnpm release:commit <version>` → merge → tag `vX.Y.Z` → two-phase `Release` workflow (`publish=false` rehearsal, then `publish=true` from the tag). +4. Update the manifest table above with the released tag/commit, and bump the harness consumers' dependency range in the same change. + +The mirror must not diverge: a change committed there directly (hotfix during a release) is ported back here before the next export. diff --git a/native/landlock-run/.gitignore b/native/landlock-run/.gitignore new file mode 100644 index 0000000000..0d7597f2db --- /dev/null +++ b/native/landlock-run/.gitignore @@ -0,0 +1,13 @@ +# Built native binaries ride npm tarballs via each package's `files` list, +# never git. Root-level rules on purpose: a package-nested ignore file would +# also steer `pnpm pack` and has silently dropped payload from tarballs before. +packages/*/bin/ +packages/*/lib/ + +/.claude/ +/.release/ +dist/ +node_modules/ +/package-lock.json +*.log +*.tsbuildinfo diff --git a/native/landlock-run/AGENTS.md b/native/landlock-run/AGENTS.md new file mode 100644 index 0000000000..31e12e177c --- /dev/null +++ b/native/landlock-run/AGENTS.md @@ -0,0 +1,50 @@ +# AGENTS.md + +This workspace builds `landlock-run`, a Landlock self-restrict-then-exec launcher: a small, auditable confinement binary distributed as prebuilt per-platform npm packages, plus the thin JS entry package that resolves it and speaks its CLI contract. The source of record is the `deepseek-harness` repository's `native/landlock-run/`; the `node-addon-landlock-run` repository is the release mirror this tree is exported to for packing and publishing (procedure: `native/README.md` in the harness repo). Make changes in the source of record, never only in the mirror. + +## Pre-release stance + +The project is pre-1.0. Prefer the correct public shape over compatibility shims: if a package name, exported field, layout, or contract detail is wrong, rename it and update all references in the same change. Do not add deprecated aliases unless a stable release already needs them. + +## Runtime safety rules + +- Every tool must fail closed. If a ruleset cannot be created or the kernel does not enforce it, exit non-zero WITHOUT exec'ing the wrapped command. Never run unconfined as a fallback. +- Runtime binaries and the entry packages take NO environment-variable overrides: which binary confines a process must never be decidable by the ambient environment. Test injection is by function parameter; the `NALR_*` prefix is for build/test orchestration only. +- Kernel UAPI is self-defined in the C source (verbatim from the kernel headers), keeping builds independent of toolchain header vintage and making the definitions part of the audit record. +- No libraries beyond libc, linked statically against musl. The audit surface of a tool is its C source plus the kernel's stable syscall contract. +- The CLI contract of each tool ([docs/cli-contract.md](docs/cli-contract.md)) is the cross-repo compatibility surface: argv grammar, exit codes, and report lines change only with a version bump and a changelog entry, and consumers parse them only through the entry package. +- There is deliberately NO install-time build fallback: a host without a matching platform package gets a nonexistent launcher path, the consumer's probe fails, and the consumer falls closed — that degradation is part of the design, not a gap to fill with node-gyp. + +## Repository layout + +```text +packages/entry/ Published entry package: JS seam (resolve/probe/grants) + the C source. +packages/linux-*/ Published per-platform packages: one prebuilt static binary, no JavaScript. +scripts/ Build, matrix derivation, prepack gates, and release orchestration. +test/ Plain-node behavioral tests (entry seam + real-kernel launcher proofs). +docs/ Architecture, packaging, CLI contract, release, support matrix, naming. +``` + +## Commands + +```sh +pnpm install +pnpm build:ts # entry packages → lib/ +pnpm build:native # this Linux architecture's binaries (needs musl-tools); fails fast elsewhere +pnpm typecheck +pnpm test # entry tests everywhere; launcher tests need linux + built binary +``` + +## Packaging invariants + +- The package matrix is explicit, checked-in metadata: `packages/<name>/package.json` (`os`, `cpu`), `packages/<name>/prebuilds.json` (the binaries that may exist there), and [docs/support-matrix.md](docs/support-matrix.md) stay synchronized when the matrix changes. `scripts/github-matrix.mjs` derives CI and release matrices from it; nothing else enumerates platforms. +- Platform package names contain platform only (`-linux-x64`), never tool variants — those stay inside `prebuilds.json`. Static musl linking is why there is no libc suffix: one binary serves glibc and musl distros. +- Platform packages ship no JavaScript; the entry package resolves them to file paths. Backends prove themselves at runtime through the functional probe, never through metadata trust. +- Builds are native-only: each architecture compiles its own binary on its own runner (CI is the builder of record); no cross toolchain enters the repo. +- Every tarball is gated at pack time: platform packages refuse to pack without their declared binaries present, executable, and in the right ELF architecture (`verify-launcher-binary.mjs`), entry packages without built `lib/` (`verify-entry-lib.mjs`), and the release pipeline byte-pins installed binaries against the workspace builds (`verify-packed-install.mjs`). +- Platform tarballs are packed with `npm pack`, never `pnpm pack`: pnpm's pack path strips the executable bit (observed on 11.7.0), shipping a launcher no consumer can spawn. `pack-release.mjs` encodes the split; the rehearsal asserts executability of the installed copy so a regression fails loudly instead of masquerading as a non-enforcing kernel. +- Generated artifacts stay out of git: `packages/*/bin/`, `packages/*/lib/`, `dist/`, `.release/`, `*.tsbuildinfo`. Ignore rules live in the ROOT `.gitignore` only — a package-nested ignore file can silently drop payload from tarballs. + +## Documentation + +User-facing docs are English. Keep the README focused on install, usage, and support status; durable design decisions belong in docs/ alongside the code, and the current implemented shape belongs in [docs/architecture.md](docs/architecture.md). diff --git a/native/landlock-run/LICENSE b/native/landlock-run/LICENSE new file mode 100644 index 0000000000..8187059c9a --- /dev/null +++ b/native/landlock-run/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2026, node-addon-landlock-run contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/native/landlock-run/README.md b/native/landlock-run/README.md new file mode 100644 index 0000000000..2bb92843e6 --- /dev/null +++ b/native/landlock-run/README.md @@ -0,0 +1,58 @@ +# node-addon-landlock-run + +A [Landlock](https://landlock.io/) self-restrict-then-exec launcher for confining subprocesses on Linux, distributed as prebuilt per-platform npm packages plus a thin JS entry package that resolves the binary and speaks its CLI contract. Built for agent harnesses and other hosts that need to run untrusted commands under a filesystem allow-list without confining themselves. + +The first tool is **`landlock-run`** — a self-restrict-then-exec [Landlock](https://landlock.io/) launcher (~300 lines of C11 over the raw kernel UAPI, statically linked against musl). It installs a Landlock ruleset on itself and `exec`s the wrapped command; the ruleset is inherited across `execve`, so the command and every process it spawns run confined while the invoking process stays unrestricted. Fail-closed: if the kernel cannot enforce, it exits without running the command. + +## Install + +```sh +npm install node-addon-landlock-run +``` + +Published packages use an entry package plus platform optional packages: + +```text +node-addon-landlock-run +node-addon-landlock-run-linux-x64 +node-addon-landlock-run-linux-arm64 +``` + +npm's `os`/`cpu` fields make installers fetch only the matching platform package. There is no install-time build fallback on purpose: on a host without a platform package the resolved path never exists, the probe reports `unusable`, and the consumer falls closed. + +## Usage + +```js +import { grantArgs, launcherPath, probe } from 'node-addon-landlock-run'; + +const launcher = launcherPath(); +if (probe(launcher) !== 'unusable') { + const argv = [launcher, ...grantArgs({ readOnly: ['/'], readWrite: ['/tmp/work'] }), '--', 'bash', '-c', command]; + // spawn argv with your process runner of choice +} +``` + +The public API is intentionally small: + +- `launcherPath()`: absolute path of this host's launcher (existence deliberately unchecked — the probe is the availability signal). +- `probe(launcher?, { timeoutMs? })`: functional enforcement probe — `'full' | 'partial' | 'unusable'`. +- `grantArgs({ readOnly?, readWrite? })`: the launcher's grant argv; everything not granted is denied. +- `LAUNCHER_BIN`, `LAUNCHER_FAILURE_EXIT` (125): contract constants. + +The full binary contract (argv grammar, exit codes, report lines) is pinned in [docs/cli-contract.md](docs/cli-contract.md). + +## Support + +linux-x64 and linux-arm64, kernel with Landlock enabled (5.13+; ABI level determines `full` vs `partial` enforcement — see [docs/support-matrix.md](docs/support-matrix.md)). Other platforms deliberately have no package: consumers run different confinement backends there. + +## Development + +```sh +corepack enable +pnpm install +pnpm build:ts # entry packages → lib/ +pnpm build:native # this Linux architecture's binaries (apt-get install musl-tools) +pnpm test +``` + +Binaries are git-ignored and built natively per architecture — locally for your own machine, by CI's per-arch runners as the builders of record. Release flow: [docs/release.md](docs/release.md). diff --git a/native/landlock-run/docs/architecture.md b/native/landlock-run/docs/architecture.md new file mode 100644 index 0000000000..e6974f4e50 --- /dev/null +++ b/native/landlock-run/docs/architecture.md @@ -0,0 +1,34 @@ +# Architecture + +This repository owns confinement *mechanism*, not policy: consumers (agent harnesses, sandbox seams) decide which paths a run may read or write; this package family provides the launcher that enforces those grants and the JS seam that resolves and speaks to it. The packaging follows the per-platform-package model of [`node-addon-require-builtin`](https://www.npmjs.com/package/@esplus/node-addon-require-builtin) (and esbuild), adapted from Node addons to standalone static executables. + +## Two-layer package family + +The family is one entry package plus per-platform binary packages: + +- **Entry package** (`node-addon-landlock-run`): ESM JavaScript. Owns the tool's CLI contract — path resolution (`launcherPath`), the functional probe (`probe`), grant-argv construction (`grantArgs`), and the contract constants. Ships the C source in its tarball for auditability. Lists every platform package as an `optionalDependency`. +- **Platform packages** (`node-addon-landlock-run-linux-{x64,arm64}`): one prebuilt static binary under `bin/`, a `prebuilds.json` declaring it, and no JavaScript at all. npm's `os`/`cpu` fields select the matching one at install time; the entry package resolves it to a file path — there is nothing to import. + +Because the contract parser and the binary version together in one family, probe-parsing drift against the binary is structurally impossible — the failure mode the split exists to prevent. + +There is no shared loader package: platform packages have nothing to load. If a second tool ever needs shared JS, extract it then, not preemptively. + +## Resolution and availability + +`launcherPath()` resolves `node-addon-landlock-run-<platform>-<arch>` and returns `<package>/bin/landlock-run`. When the package is not resolvable it returns a deterministic fallback path inside the entry package's own `node_modules` that simply never exists. Existence is deliberately unchecked either way: `probe()` is the single availability signal, and a missing binary probes `unusable` exactly like an unenforcing kernel. Consumers get one degradation path, not two. + +The probe is functional — the launcher builds and enforces a real maximal ruleset in a short-lived child — because version checks would miss a kernel that has the syscalls but refuses enforcement. + +## Fail-closed everywhere + +The launcher exits `125` without exec'ing the command on any launcher-level failure: usage error, unenforcing kernel, unopenable grant root, failed exec. Partial enforcement (an older Landlock ABI governing only a subset of accesses) is accepted, reported on stderr, and surfaced by the probe as `partial` — the consumer decides what its mode vocabulary promises at each level. Neither the binary nor the entry package reads environment variables: which binary confines a process is never decidable by the ambient environment. + +## Build and release model + +Builds are native-only. `scripts/build.ts` compiles the running architecture's binaries with the distro `musl-gcc` (static: no loader or libc expectations on consumers, one binary for glibc and musl distros); CI's per-architecture runners are the builders of record, and no cross toolchain exists in the repo. The audit surface of a tool is its reviewed C source plus CI provenance, enforced by three gates: platform prepack refuses missing/wrong-ELF binaries, entry prepack refuses unbuilt `lib/`, and the release pipeline byte-pins installed binaries against the workspace builds they were packed from. + +The package matrix is checked-in metadata (`prebuilds.json` + `os`/`cpu` fields); `scripts/github-matrix.mjs` derives the CI and Release matrices from it, so adding a platform extends automation without editing workflows. + +## Adding a platform + +A new platform adds one `packages/<platform>/` package (`package.json` with `os`/`cpu`, `prebuilds.json`, README, LICENSE), a runner entry in `scripts/github-matrix.mjs`, and a row in [support-matrix.md](support-matrix.md) — added only together with a native GitHub runner that builds and proves it (the no-cross-toolchain rule). Sibling launchers for other confinement mechanisms belong in their own repositories on this same template, not as second tools here. diff --git a/native/landlock-run/docs/cli-contract.md b/native/landlock-run/docs/cli-contract.md new file mode 100644 index 0000000000..57ab0f604c --- /dev/null +++ b/native/landlock-run/docs/cli-contract.md @@ -0,0 +1,34 @@ +# CLI contract: landlock-run + +This file pins the launcher's externally observable behavior — the cross-repo compatibility surface between the binaries and every consumer. Consumers interact with it only through the entry package (`launcherPath`/`probe`/`grantArgs`); changing anything below requires a version bump for the whole package family and a note in the release notes. + +## Invocation grammar + +```text +landlock-run [--ro <path>]... [--rw <path>]... -- <argv>... +landlock-run --probe +``` + +- `--ro <path>`: grant read + execute beneath `<path>`. +- `--rw <path>`: grant full filesystem access beneath `<path>` (every access the negotiated kernel ABI can govern). +- Everything not granted is denied — Landlock rulesets are allow-lists. +- A grant on a non-directory keeps only its file-compatible access bits (this is how a `--rw /dev/null` grant works). +- `--`: mandatory separator; everything after it is the command argv, exec'd via `execvp` with the launcher's environment unchanged. +- `--probe`: mutually exclusive with grants and a command. +- No other flags, no environment-variable inputs. + +## Exit codes + +- `125` (`LAUNCHER_FAILURE_EXIT`): every launcher-level failure — usage error, kernel that cannot enforce Landlock, unopenable grant root, failed `exec`. The wrapped command was NOT run (fail-closed; the one exception is `exec` itself failing after restriction, which by definition never ran the command either). +- Any other status: the wrapped command's own exit status, passed through unchanged. +- `--probe`: `0` when the kernel enforces (fully or partially), `125` otherwise. + +## Report lines + +- Probe success prints exactly one stdout line: `landlock: fully enforced` or `landlock: partially enforced (older ABI)`. The entry package's `probe()` maps these to `full`/`partial`; a non-zero probe exit maps to `unusable`. +- A confined run under a partial-ABI kernel prints one stderr line `landlock-run: partial enforcement (older Landlock ABI)` and proceeds — still confined for everything the kernel supports. +- Every fatal error prints one stderr line prefixed `landlock-run: ` before exiting `125`. + +## Confinement semantics + +The launcher sets `no_new_privs`, installs the ruleset on itself, and `exec`s the command; the ruleset is inherited across `execve`, so every descendant process is equally confined. The ruleset governs the filesystem accesses of the kernel's negotiated Landlock ABI (up to ABI 5); accesses newer than the running ABI are not governed and are the difference between `full` and `partial`. diff --git a/native/landlock-run/docs/naming.md b/native/landlock-run/docs/naming.md new file mode 100644 index 0000000000..9de9f0b95f --- /dev/null +++ b/native/landlock-run/docs/naming.md @@ -0,0 +1,30 @@ +# Naming + +## npm packages + +The public package family is unscoped, using the `node-addon-landlock-run` package prefix; platform packages append platform information only: + +```text +node-addon-landlock-run +node-addon-landlock-run-<platform> +``` + +Platform suffixes carry no libc component (binaries are static musl) and no variant component — variants stay inside `prebuilds.json` and binary filenames. + +## Binaries + +The launcher executable is `landlock-run`, shipped at `bin/landlock-run` inside each platform package. + +## Environment variables + +The `NALR_` prefix (Node Addon Landlock Run) is reserved for build/test orchestration: + +```text +NALR_REQUIRE_LANDLOCK test-only: an unenforcing kernel fails instead of skipping +``` + +Runtime binaries and entry packages read NO environment variables — a runtime safety rule ([AGENTS.md](../AGENTS.md)), not a naming convention. Do not include the npm scope in environment variable names. + +## C symbols + +The launcher is a single C file with static linkage; there is no exported symbol namespace. Kernel UAPI constants keep their kernel names prefixed `LL_` where locally defined. diff --git a/native/landlock-run/docs/packaging.md b/native/landlock-run/docs/packaging.md new file mode 100644 index 0000000000..9a1be47b2a --- /dev/null +++ b/native/landlock-run/docs/packaging.md @@ -0,0 +1,45 @@ +# Packaging + +The package family uses the same broad shape as native packages such as esbuild: one JS entry package plus platform optional packages. Unlike Node addons there is no ABI or backend dimension — each platform package carries exactly the static executables its `prebuilds.json` declares. + +## Published packages + +```text +node-addon-landlock-run +node-addon-landlock-run-linux-x64 +node-addon-landlock-run-linux-arm64 +``` + +Unsupported platforms are intentionally absent from `optionalDependencies` — see [support-matrix.md](support-matrix.md). + +## Package matrix + +The matrix is explicit in checked-in metadata: + +- `packages/entry/package.json` lists the platform packages as `optionalDependencies`. +- `packages/<name>/package.json` declares `os` and `cpu`. There is no `libc` field on purpose: the binaries are statically linked against musl and run on glibc and musl distros alike. +- `packages/<name>/prebuilds.json` declares the binaries that may exist in that package (`tool`, `kind`, `path`). +- [support-matrix.md](support-matrix.md) explains why unsupported platform packages are not published. + +`scripts/github-matrix.mjs` derives the CI and Release matrices from these files. `scripts/build.ts` builds only the current host's targets, into `packages/<name>/bin/`; it is not a matrix generator. When changing the matrix, update package metadata, `prebuilds.json`, the lockfile, and the support/release docs in the same change. + +## Runtime selection + +1. npm's `os`/`cpu` fields make installers fetch only the matching platform package. +2. The entry package's `launcherPath()` resolves it to `<package>/bin/landlock-run`; unresolvable packages yield a deterministic, never-existing fallback path. +3. `probe()` is the single availability signal: missing binary and unenforcing kernel are deliberately indistinguishable (`unusable`), so consumers have one fail-closed path. + +## No install fallback + +The entry package has NO install script and never compiles on the consumer host. A compile fallback would require a musl toolchain everywhere and turn a clean fail-closed degradation into an environment-dependent maybe. The packed-manifest check in `verify-packed-install.mjs` enforces the absence of install lifecycle scripts. + +## Pack gates + +Platform tarballs are produced by `npm pack`, entry tarballs by `pnpm pack` — deliberately split: `pnpm pack` (observed on 11.7.0) normalizes file modes and strips the executable bit, which would ship a launcher no consumer can spawn, while platform packages have no dependencies and so need none of pnpm's workspace-protocol conversion; entry packages need that conversion and carry no executables. `scripts/pack-release.mjs` encodes the split — never hand-pack a platform package with pnpm. + +Both pack paths produce the exact publish bytes behind a `prepack` gate: + +- Platform packages: `scripts/verify-launcher-binary.mjs` — every declared binary present, executable, ELF `e_machine` matching the declared `cpu`, nothing undeclared in `bin/`. +- Entry packages: `scripts/verify-entry-lib.mjs` — built `lib/` present. + +`scripts/verify-packed-install.mjs` then rehearses the consumer path from the packed tarballs: payload checks, a throwaway install, a byte-pin of the installed binary against the workspace build, an executability check on the installed copy, and a real confinement world-proof through the installed launcher. A non-executable or missing binary fails loudly here instead of masquerading as a non-enforcing kernel. diff --git a/native/landlock-run/docs/release.md b/native/landlock-run/docs/release.md new file mode 100644 index 0000000000..e1ea65c411 --- /dev/null +++ b/native/landlock-run/docs/release.md @@ -0,0 +1,58 @@ +# Release + +Pre-1.0: treat this as a release checklist, not a stability policy. + +## Versioning + +One version across every package in the repo. Use the bump helper: + +```sh +pnpm release:bump patch # or minor / major / x.y.z +``` + +It updates the root and every `packages/*` manifest, refreshes the lockfile (`--ignore-scripts --lockfile-only`), and runs `release:verify`. Explicit versions accept full semver including prereleases (`pnpm release:bump 0.0.0-test.0`); the publish workflow puts prerelease versions under the `next` dist-tag, so `latest` never points at a test build. Keep `workspace:*` dependencies in source; pnpm converts them to concrete versions during pack. + +Version bumps are normal source changes: open a release PR (or commit) with the manifests and lockfile, merge it, then create the matching `vX.Y.Z` tag from that commit. The publish workflow validates that the tag matches every package version. + +```sh +pnpm release:commit patch # bump + stage + commit in one command +git tag v0.0.2 +``` + +## Preflight + +```sh +pnpm install --frozen-lockfile +pnpm build:ts +pnpm typecheck +pnpm test:entry +``` + +On a Linux host, also rehearse the pack path locally: + +```sh +pnpm build:native +pnpm test:launcher +node ./scripts/pack-release.mjs .release/npm --current-platform-only +node ./scripts/verify-packed-install.mjs .release/npm --current-platform-only +``` + +## Publish + +Use the `Release` workflow so every binary is built on its matching native runner: + +1. Run it with `publish=false` (from the release commit) to build all platform binaries, assemble and verify the payloads, pack the tarballs in publish order, rehearse the packed install, and upload the `npm-tarballs` artifact for inspection. +2. Create and push the `vX.Y.Z` tag matching the package versions. +3. Run the same workflow from that tag with `publish=true`. + +The workflow publishes only from the final packed tarballs, in `publish-order.txt` order (platform packages before the entry that optionally depends on them). It supports npm trusted publishing through GitHub OIDC; without it, provide an `NPM_TOKEN` secret in the `npm-publish` environment. Packages publish with `--access public`. + +Manual local fallback (current platform's packages only) — always through `pack-release.mjs`, never `pnpm publish` directly (pnpm's pack path strips the launcher's executable bit; see [packaging.md](packaging.md)): + +```sh +node ./scripts/pack-release.mjs dist/npm --current-platform-only +node ./scripts/verify-packed-install.mjs dist/npm --current-platform-only +while IFS= read -r tarball; do npm publish "dist/npm/${tarball}" --access public; done < dist/npm/publish-order.txt +``` + +Do not commit `.npmrc` files with tokens or registry overrides. diff --git a/native/landlock-run/docs/support-matrix.md b/native/landlock-run/docs/support-matrix.md new file mode 100644 index 0000000000..96d02b3cf6 --- /dev/null +++ b/native/landlock-run/docs/support-matrix.md @@ -0,0 +1,18 @@ +# Support matrix + +## Supported + +| Platform package | GitHub runner (builder of record) | Notes | +|---|---|---| +| `node-addon-landlock-run-linux-x64` | `ubuntu-24.04` | static musl — glibc and musl distros alike | +| `node-addon-landlock-run-linux-arm64` | `ubuntu-24.04-arm` | static musl — glibc and musl distros alike | + +Enforcement additionally requires a kernel with Landlock enabled (5.13+). The negotiated ABI level decides the probe verdict: every access this build knows governed → `full`; an older ABI governing a subset → `partial` (still confined for everything it supports); Landlock absent or disabled → `unusable`, and the launcher refuses to run commands at all. The probe — not the kernel version — is the authority: a kernel built without Landlock, or with the LSM disabled, probes `unusable` regardless of its version. + +## Deliberately unsupported + +- **darwin**: macOS consumers typically confine through `sandbox-exec`/Seatbelt, which ships with the OS — there is no binary to distribute. +- **win32**: a Windows confinement launcher would be a different mechanism in its own repository, not a port of this one. +- **Other Linux architectures** (riscv64, s390x, …): no native CI builder of record yet. The no-cross-toolchain rule means a platform package is added only together with a native runner that builds and proves it. + +A consumer on an unsupported platform resolves a nonexistent launcher path, probes `unusable`, and falls closed — the documented degradation, exercised by CI's darwin leg. diff --git a/native/landlock-run/package.json b/native/landlock-run/package.json new file mode 100644 index 0000000000..f516588f17 --- /dev/null +++ b/native/landlock-run/package.json @@ -0,0 +1,30 @@ +{ + "name": "node-addon-landlock-run-workspace", + "version": "0.0.1", + "private": true, + "type": "module", + "license": "BSD-3-Clause", + "packageManager": "pnpm@11.7.0", + "scripts": { + "build": "pnpm build:ts", + "build:ts": "tsc -b", + "build:native": "tsx ./scripts/build.ts", + "typecheck": "tsc --noEmit && tsc -b --dry", + "test": "node ./test/entry.test.js && node ./test/launcher.test.js", + "test:entry": "node ./test/entry.test.js", + "test:launcher": "node ./test/launcher.test.js", + "gha:matrix": "node ./scripts/github-matrix.mjs", + "release:bump": "node ./scripts/bump-release.mjs", + "release:commit": "node ./scripts/commit-release.mjs", + "release:assemble-prebuilds": "node ./scripts/assemble-prebuilds.mjs", + "release:verify": "node ./scripts/verify-release.mjs", + "release:pack": "node ./scripts/pack-release.mjs", + "release:verify-packed-install": "node ./scripts/verify-packed-install.mjs" + }, + "devDependencies": { + "node-addon-landlock-run": "workspace:*", + "@types/node": "^24.10.0", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + } +} diff --git a/native/landlock-run/packages/entry/README.md b/native/landlock-run/packages/entry/README.md new file mode 100644 index 0000000000..789b1ddf6b --- /dev/null +++ b/native/landlock-run/packages/entry/README.md @@ -0,0 +1,16 @@ +# node-addon-landlock-run + +Landlock self-restrict-then-exec launcher for confining subprocesses on Linux: this entry package resolves the per-platform prebuilt binary, runs its functional enforcement probe, and builds its grant argv — consumers never spell launcher flags or parse launcher output themselves. + +```js +import { grantArgs, launcherPath, probe } from 'node-addon-landlock-run'; + +const launcher = launcherPath(); +if (probe(launcher) !== 'unusable') { + const argv = [launcher, ...grantArgs({ readOnly: ['/'], readWrite: ['/tmp/work'] }), '--', 'bash', '-c', command]; +} +``` + +The launcher installs a Landlock ruleset on itself and `exec`s the wrapped command; the ruleset is inherited across `execve`, so the whole process tree runs confined. Everything not granted is denied, and launcher failures exit `125` without running the command — fail-closed, never fail-open. The binary contract is pinned in the repo's `docs/cli-contract.md`; the C source rides this tarball (`src/main.c`) for audit. + +Platform packages (`os`/`cpu`-selected optional dependencies, no JavaScript inside): `node-addon-landlock-run-linux-x64`, `node-addon-landlock-run-linux-arm64`. On hosts without one, `launcherPath()` returns a deterministic nonexistent path and `probe()` reports `'unusable'` — there is deliberately no install-time compile fallback. diff --git a/native/landlock-run/packages/entry/package.json b/native/landlock-run/packages/entry/package.json new file mode 100644 index 0000000000..f05e81f06b --- /dev/null +++ b/native/landlock-run/packages/entry/package.json @@ -0,0 +1,36 @@ +{ + "name": "node-addon-landlock-run", + "version": "0.0.1", + "type": "module", + "description": "Landlock self-restrict-then-exec launcher for sandboxing subprocesses on Linux: per-platform prebuilt static binaries plus the JS seam that resolves, probes, and speaks their CLI contract", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "README.md", + "lib/", + "!lib/*.tsbuildinfo", + "src/main.c" + ], + "scripts": { + "build:js": "tsc -b", + "prepack": "node ../../scripts/verify-entry-lib.mjs" + }, + "engines": { + "node": ">=20" + }, + "license": "BSD-3-Clause", + "publishConfig": { + "access": "public" + }, + "optionalDependencies": { + "node-addon-landlock-run-linux-arm64": "workspace:*", + "node-addon-landlock-run-linux-x64": "workspace:*" + } +} diff --git a/native/landlock-run/packages/entry/src/index.ts b/native/landlock-run/packages/entry/src/index.ts new file mode 100644 index 0000000000..53de86122f --- /dev/null +++ b/native/landlock-run/packages/entry/src/index.ts @@ -0,0 +1,126 @@ +/** + * The JS seam over the prebuilt `landlock-run` launcher: resolve the + * binary for this host, build its grant argv, and run its functional probe. + * + * This module owns the launcher's CLI contract (`docs/cli-contract.md`) so + * consumers never parse launcher output or spell launcher flags themselves — + * the contract and the binaries version together in one package family, + * which makes probe-parsing drift against the binary structurally + * impossible. Policy stays with the consumer: this package does not know + * what a "sandbox mode" is, only which paths are granted read or write. + * + * Deliberately no environment-variable overrides anywhere in this module: + * which binary confines a process must never be decidable by the ambient + * environment. Test injection is by function parameter. + */ +import { spawnSync } from 'node:child_process' +import { createRequire } from 'node:module' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +/** The launcher binary's file name inside each platform package's `bin/`. */ +export const LAUNCHER_BIN = 'landlock-run' + +/** + * The exit code for every launcher-level failure (usage error, unenforcing + * kernel, unopenable grant root, failed exec) — chosen because the wrapped + * command itself is unlikely to use it, so a consumer can tell launcher + * failures from command failures. Part of the CLI contract. + */ +export const LAUNCHER_FAILURE_EXIT = 125 + +/** + * The probe's verdict on this host: `full` when the running kernel enforces + * every access the launcher can govern, `partial` when an older Landlock ABI + * governs only a subset (still confined for everything it supports), and + * `unusable` when nothing can be enforced — a kernel without Landlock, a + * disabled LSM, or a missing binary, all indistinguishable on purpose + * because the consumer's answer is the same: do not trust this launcher. + */ +export type LandlockEnforcement = 'full' | 'partial' | 'unusable' + +/** + * Filesystem grants for one confined run. Everything not granted is denied — + * Landlock rulesets are allow-lists. + */ +export interface LauncherGrants { + /** Roots granted read + execute beneath (the launcher's `--ro`). */ + readonly readOnly?: readonly string[] + /** Roots granted full filesystem access beneath (the launcher's `--rw`). */ + readonly readWrite?: readonly string[] +} + +/** + * Path of the launcher binary for this host: resolved from the per-platform + * npm package `node-addon-landlock-run-<platform>-<arch>` (npm's + * `os`/`cpu` fields make installers fetch only the matching one). When the + * package is not resolvable — a platform without one, or an install that + * skipped the optional dependency — the returned fallback path points inside + * this package's own `node_modules` and simply never exists. Existence is + * deliberately not checked either way: {@link probe} is the single + * availability signal (a missing binary probes `unusable` the same way an + * unenforcing kernel does). + * @param resolvePackageJson - test seam over `require.resolve` (the default + * covers real installs); receives the platform package's `package.json` + * specifier and returns its absolute path, throwing when unresolvable. + * @returns the absolute launcher path to probe and exec. + */ +export function launcherPath( + resolvePackageJson: (specifier: string) => string = createRequire(import.meta.url).resolve, +): string { + const platformPackage = `node-addon-landlock-run-${process.platform}-${process.arch}` + try { + return join(dirname(resolvePackageJson(`${platformPackage}/package.json`)), 'bin', LAUNCHER_BIN) + } catch { + // Unresolvable platform package: no such package exists for this host, or + // it was not installed. Fall back to the path pnpm's layout WOULD use — + // absolute, inside this package's boundary (never cwd-relative: a + // spawnable relative path here would hand cwd control over which binary + // confines), and nonexistent exactly when the package is absent. + return fileURLToPath(new URL(`../node_modules/${platformPackage}/bin/${LAUNCHER_BIN}`, import.meta.url)) + } +} + +/** + * The launcher grant arguments for one set of filesystem grants — everything + * before the `--` argv separator. A caller spawns + * `[launcherPath(), ...grantArgs(grants), '--', ...command]`; the flag + * spellings stay private to this package. + * @param grants - the read-only and read-write roots to allow. + * @returns the `--ro <path>` / `--rw <path>` argument list, read-only roots + * first, in the caller's order. + */ +export function grantArgs(grants: LauncherGrants): string[] { + return [ + ...(grants.readOnly ?? []).flatMap(root => ['--ro', root]), + ...(grants.readWrite ?? []).flatMap(root => ['--rw', root]), + ] +} + +/** + * Functional probe: `landlock-run --probe` builds and enforces a maximal + * ruleset in a short-lived child and exits 0 only when the running kernel + * actually enforces it — `--version`-style checks would miss a kernel that + * has the syscalls but refuses enforcement. The probe's one report line is + * part of the CLI contract and distinguishes complete from per-ABI-subset + * enforcement; a zero exit without the partial marker reads as `full`. A + * failed or timed-out spawn (missing binary, wrong architecture, unenforcing + * kernel) probes `unusable`. Synchronous by design: consumers run it once + * and cache the verdict. + * @param launcher - the launcher path to probe; defaults to + * {@link launcherPath}'s resolution for this host. + * @param options - `timeoutMs` bounds the probe child (default 2000). + * @returns the enforcement verdict for this host. + */ +export function probe( + launcher: string = launcherPath(), + options: { timeoutMs?: number } = {}, +): LandlockEnforcement { + const result = spawnSync(launcher, ['--probe'], { + timeout: options.timeoutMs ?? 2000, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }) + if (result.status !== 0) return 'unusable' + return /partially enforced/.test(result.stdout) ? 'partial' : 'full' +} diff --git a/native/landlock-run/packages/entry/src/main.c b/native/landlock-run/packages/entry/src/main.c new file mode 100644 index 0000000000..af3c2eb3f0 --- /dev/null +++ b/native/landlock-run/packages/entry/src/main.c @@ -0,0 +1,301 @@ +/* + * landlock-run: self-restrict-then-exec Landlock launcher. + * + * The Landlock rung of a consuming sandbox seam, for Linux hosts where + * `bwrap` is + * unusable (not installed, unprivileged user namespaces disabled, or an LSM + * profile that denies mount — Landlock is an independent syscall family and + * needs none of those). The launcher installs a Landlock + * ruleset on itself and `exec`s the wrapped command; the ruleset is inherited + * across `execve`, so the command (and every process it spawns) runs confined + * while the invoking process stays unrestricted. + * + * CLI contract (mirrors the `bwrap` runner argv shape the executor wraps): + * + * landlock-run [--ro <path>]... [--rw <path>]... -- <argv>... + * landlock-run --probe + * + * `--ro` grants read+execute beneath the path; `--rw` grants full filesystem + * access beneath the path. Everything else is denied (Landlock is an + * allow-list). `--probe` builds a maximal ruleset and reports whether the + * running kernel actually enforces it — the executor's functional probe. + * + * Fail-closed: if the ruleset cannot be created or is NOT enforced by the + * kernel, the launcher exits non-zero WITHOUT exec'ing the command. A partial + * (best-effort) enforcement on an older ABI is accepted and reported on + * stderr; the consumer's mode vocabulary keeps its file-effect promises + * honest per ABI level (surfaced as `full` vs `partial` by the entry + * package's probe). + * + * Plain C11 over the raw Landlock UAPI — no libraries beyond libc (musl, + * linked statically), so the whole audit surface is this file plus the + * kernel's stable syscall contract. Built natively per architecture by + * `scripts/build.ts` into the per-platform npm packages + * (`node-addon-landlock-run-linux-{x64,arm64}`); the argv grammar, + * exit codes, and report lines are pinned in `docs/cli-contract.md`. + */ + +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/prctl.h> +#include <sys/stat.h> +#include <sys/syscall.h> +#include <unistd.h> + +/* + * The Landlock UAPI, defined locally instead of via <linux/landlock.h>: the + * kernel's user-space ABI is stable by contract, self-defining it keeps the + * build independent of the toolchain's header vintage, and the definitions + * double as the audit record of exactly which kernel surface this launcher + * touches. Layouts and values are verbatim from the kernel header (the + * path-beneath struct is packed there, so it must be packed here). + */ +struct landlock_ruleset_attr { + uint64_t handled_access_fs; +}; + +struct landlock_path_beneath_attr { + uint64_t allowed_access; + int32_t parent_fd; +} __attribute__((packed)); + +#define LANDLOCK_CREATE_RULESET_VERSION (1U << 0) +#define LANDLOCK_RULE_PATH_BENEATH 1 + +/* Filesystem access bits, grouped by the Landlock ABI that introduced them. */ +#define LL_FS_EXECUTE (UINT64_C(1) << 0) /* ABI 1 */ +#define LL_FS_WRITE_FILE (UINT64_C(1) << 1) +#define LL_FS_READ_FILE (UINT64_C(1) << 2) +#define LL_FS_READ_DIR (UINT64_C(1) << 3) +#define LL_FS_REMOVE_DIR (UINT64_C(1) << 4) +#define LL_FS_REMOVE_FILE (UINT64_C(1) << 5) +#define LL_FS_MAKE_CHAR (UINT64_C(1) << 6) +#define LL_FS_MAKE_DIR (UINT64_C(1) << 7) +#define LL_FS_MAKE_REG (UINT64_C(1) << 8) +#define LL_FS_MAKE_SOCK (UINT64_C(1) << 9) +#define LL_FS_MAKE_FIFO (UINT64_C(1) << 10) +#define LL_FS_MAKE_BLOCK (UINT64_C(1) << 11) +#define LL_FS_MAKE_SYM (UINT64_C(1) << 12) +#define LL_FS_REFER (UINT64_C(1) << 13) /* ABI 2 */ +#define LL_FS_TRUNCATE (UINT64_C(1) << 14) /* ABI 3 (ABI 4 added TCP bits only) */ +#define LL_FS_IOCTL_DEV (UINT64_C(1) << 15) /* ABI 5 */ + +#define LL_ABI1_MASK (LL_FS_REFER - 1) /* bits 0..12: every ABI-1 access, nothing newer */ + +/* + * Newest ABI this build knows; the negotiation below scales the actual + * ruleset down to what the running kernel supports (the best-effort compat + * stance of the previous Rust launcher, made explicit). + */ +#define MAX_ABI 5L + +/* + * Landlock has no libc wrappers; these are the raw syscalls. The numbers are + * identical on every architecture (the post-2011 unified table) — the + * fallbacks only matter to a libc older than the feature. + */ +#ifndef __NR_landlock_create_ruleset +#define __NR_landlock_create_ruleset 444 +#define __NR_landlock_add_rule 445 +#define __NR_landlock_restrict_self 446 +#endif + +/* + * Every fatal launcher error prints `landlock-run: <message>` to stderr + * and exits 125 — a code the wrapped command itself is unlikely to use, so + * the executor can tell launcher failures from command failures. + */ +#define EXIT_LAUNCHER_FAILURE 125 + +static const char NOT_ENFORCED_MESSAGE[] = + "landlock is not enforced by this kernel (ABI unsupported or disabled)"; + +/* Print one fatal `landlock-run: ...` line; returns the fatal exit code. */ +static int fail(const char *prefix, const char *detail) { + if (detail == NULL) { + fprintf(stderr, "landlock-run: %s\n", prefix); + } else { + fprintf(stderr, "landlock-run: %s: %s\n", prefix, detail); + } + return EXIT_LAUNCHER_FAILURE; +} + +static int fail_usage(const char *message, const char *detail) { + fprintf(stderr, "landlock-run: usage error: %s%s\n", message, detail == NULL ? "" : detail); + return EXIT_LAUNCHER_FAILURE; +} + +/* Parsed CLI: either a probe, or grants plus the command argv after `--`. */ +struct cli { + int probe; + const char **ro; + size_t ro_count; + const char **rw; + size_t rw_count; + char **command; /* NULL-terminated tail of main's argv */ +}; + +/* + * Hand-rolled argv parsing — four flags do not justify a parsing library, + * and the previous Rust launcher made the same call for the same reason. + * Returns 0 on success, else the process exit code (message already printed). + */ +static int parse(int argc, char **argv, struct cli *cli) { + /* argc bounds each grant list; the launcher execs or exits, so no free. */ + cli->ro = calloc(argc > 0 ? (size_t)argc : 1, sizeof *cli->ro); + cli->rw = calloc(argc > 0 ? (size_t)argc : 1, sizeof *cli->rw); + if (cli->ro == NULL || cli->rw == NULL) return fail("out of memory", NULL); + + int index = 1; + while (index < argc) { + const char *arg = argv[index]; + if (strcmp(arg, "--probe") == 0) { + if (argc != 2) { + return fail_usage("--probe takes no other arguments", NULL); + } + cli->probe = 1; + index += 1; + } else if (strcmp(arg, "--ro") == 0 || strcmp(arg, "--rw") == 0) { + if (index + 1 >= argc) { + return fail_usage(arg, " requires a path"); + } + if (strcmp(arg, "--ro") == 0) { + cli->ro[cli->ro_count++] = argv[index + 1]; + } else { + cli->rw[cli->rw_count++] = argv[index + 1]; + } + index += 2; + } else if (strcmp(arg, "--") == 0) { + cli->command = &argv[index + 1]; + break; + } else { + return fail_usage("unknown argument: ", arg); + } + } + if (!cli->probe && (cli->command == NULL || cli->command[0] == NULL)) { + return fail_usage("missing `-- <argv>...` command", NULL); + } + return 0; +} + +/* The filesystem accesses the running kernel's ABI can govern. */ +static uint64_t fs_mask_for_abi(long abi) { + uint64_t mask = LL_ABI1_MASK; + if (abi >= 2) mask |= LL_FS_REFER; + if (abi >= 3) mask |= LL_FS_TRUNCATE; + if (abi >= 5) mask |= LL_FS_IOCTL_DEV; + return mask; +} + +/* Add one path-beneath rule; 0 on success, else the exit code. */ +static int add_rule(int ruleset_fd, const char *path, uint64_t access) { + int path_fd = open(path, O_PATH | O_CLOEXEC); + if (path_fd < 0) { + /* Fail closed on an unopenable grant root: silently narrowing the + * granted set would be safe, but running with a profile the caller did + * not get is not worth the ambiguity. */ + fprintf(stderr, "landlock-run: cannot open rule path: %s: %s\n", path, strerror(errno)); + return EXIT_LAUNCHER_FAILURE; + } + /* The kernel rejects directory-only accesses on a non-directory rule + * (EINVAL), so a file grant keeps only the file-compatible bits — how the + * `--rw /dev/null` grant works. Same clamp the Rust crate's + * path_beneath_rules helper applied. */ + struct stat st; + if (fstat(path_fd, &st) == 0 && !S_ISDIR(st.st_mode)) { + access &= LL_FS_EXECUTE | LL_FS_WRITE_FILE | LL_FS_READ_FILE | LL_FS_TRUNCATE | LL_FS_IOCTL_DEV; + } + struct landlock_path_beneath_attr attr = { .allowed_access = access, .parent_fd = path_fd }; + if (syscall(__NR_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &attr, 0) != 0) { + int saved = errno; + close(path_fd); + return fail("landlock ruleset error", strerror(saved)); + } + close(path_fd); + return 0; +} + +/* + * Install the ruleset on the current thread, negotiating the kernel's ABI + * down from MAX_ABI. `--ro` paths get the read side of the vocabulary (read + * file/dir + execute — the wrapped `bash` and everything it spawns must + * remain executable); `--rw` paths get every filesystem access the + * negotiated ABI can grant. Sets `no_new_privs` first (mandatory for an + * unprivileged restrict, and it neutralizes setuid/setgid escalation inside + * the sandbox). On success `*partial` reports whether the kernel governs + * only a subset of MAX_ABI's accesses. Returns 0, else the exit code. + */ +static int restrict_self(const struct cli *cli, int *partial) { + long abi = syscall(__NR_landlock_create_ruleset, NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); + if (abi < 0) { + /* ENOSYS: kernel built without Landlock; EOPNOTSUPP: built but disabled. + * Either way: not enforceable — fail CLOSED, never exec unconfined. */ + return fail(NOT_ENFORCED_MESSAGE, NULL); + } + *partial = abi < MAX_ABI; + uint64_t handled = fs_mask_for_abi(abi < MAX_ABI ? abi : MAX_ABI); + + struct landlock_ruleset_attr attr = { .handled_access_fs = handled }; + int ruleset_fd = (int)syscall(__NR_landlock_create_ruleset, &attr, sizeof attr, 0); + if (ruleset_fd < 0) return fail("landlock ruleset error", strerror(errno)); + + const uint64_t read_side = LL_FS_EXECUTE | LL_FS_READ_FILE | LL_FS_READ_DIR; + for (size_t i = 0; i < cli->ro_count; i++) { + int code = add_rule(ruleset_fd, cli->ro[i], read_side & handled); + if (code != 0) return code; + } + for (size_t i = 0; i < cli->rw_count; i++) { + int code = add_rule(ruleset_fd, cli->rw[i], handled); + if (code != 0) return code; + } + + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) { + return fail("landlock ruleset error", strerror(errno)); + } + if (syscall(__NR_landlock_restrict_self, ruleset_fd, 0) != 0) { + return fail("landlock ruleset error", strerror(errno)); + } + close(ruleset_fd); + return 0; +} + +int main(int argc, char **argv) { + struct cli cli = { 0 }; + int code = parse(argc, argv, &cli); + if (code != 0) return code; + + if (cli.probe) { + /* The functional probe: build and enforce a maximal ruleset in THIS + * short-lived process (the probe run exits right after). `--version` + * style checks would miss a kernel that has the syscalls but refuses + * enforcement; actually restricting is the only honest signal. The one + * report line is part of the launcher CLI contract — the executor reads + * enforcement completeness from it. */ + static const char *probe_root = "/"; + struct cli probe = { .ro = &probe_root, .ro_count = 1 }; + int partial = 0; + code = restrict_self(&probe, &partial); + if (code != 0) return code; + printf("landlock: %s\n", partial ? "partially enforced (older ABI)" : "fully enforced"); + return 0; + } + + int partial = 0; + code = restrict_self(&cli, &partial); + if (code != 0) return code; + if (partial) { + /* Older ABI: some handled accesses are not governed (e.g. truncate + * before ABI 3). Still confined for everything the kernel supports — + * report, do not refuse. */ + fprintf(stderr, "landlock-run: partial enforcement (older Landlock ABI)\n"); + } + + execvp(cli.command[0], cli.command); + /* exec only returns on failure. */ + return fail("exec failed", strerror(errno)); +} diff --git a/native/landlock-run/packages/entry/tsconfig.json b/native/landlock-run/packages/entry/tsconfig.json new file mode 100644 index 0000000000..bb991d6ceb --- /dev/null +++ b/native/landlock-run/packages/entry/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "declaration": true, + "outDir": "lib", + "rootDir": "src", + "tsBuildInfoFile": "lib/.tsbuildinfo" + }, + "include": ["src/**/*.ts"] +} diff --git a/native/landlock-run/packages/linux-arm64/LICENSE b/native/landlock-run/packages/linux-arm64/LICENSE new file mode 100644 index 0000000000..8187059c9a --- /dev/null +++ b/native/landlock-run/packages/linux-arm64/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2026, node-addon-landlock-run contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/native/landlock-run/packages/linux-arm64/README.md b/native/landlock-run/packages/linux-arm64/README.md new file mode 100644 index 0000000000..1921c8f4b5 --- /dev/null +++ b/native/landlock-run/packages/linux-arm64/README.md @@ -0,0 +1,7 @@ +# node-addon-landlock-run-linux-arm64 + +Prebuilt `bin/landlock-run` Landlock launcher for linux-arm64 — a static musl binary compiled natively (no cross toolchain) from the C source shipped in [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run). npm's `os`/`cpu` fields select this package at install time; the entry package resolves it to a file path — it ships no JavaScript and is never imported. + +The binary is git-ignored and rides the npm tarball via the `files` list; the `prepack` gate refuses to pack when it is missing or has the wrong ELF architecture, and the release pipeline byte-pins the packed binary against the CI build it came from. Static musl linking means one binary for glibc and musl distros alike — hence no libc suffix in the name. + +Sibling: `node-addon-landlock-run-linux-x64`. diff --git a/native/landlock-run/packages/linux-arm64/package.json b/native/landlock-run/packages/linux-arm64/package.json new file mode 100644 index 0000000000..0067f77c8b --- /dev/null +++ b/native/landlock-run/packages/linux-arm64/package.json @@ -0,0 +1,26 @@ +{ + "name": "node-addon-landlock-run-linux-arm64", + "version": "0.0.1", + "description": "Prebuilt landlock-run Landlock launcher binary for linux-arm64 (static musl) — resolved as a file path by node-addon-landlock-run, never imported", + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "files": [ + "README.md", + "bin/", + "prebuilds.json" + ], + "scripts": { + "prepack": "node ../../scripts/verify-launcher-binary.mjs" + }, + "engines": { + "node": ">=20" + }, + "license": "BSD-3-Clause", + "publishConfig": { + "access": "public" + } +} diff --git a/native/landlock-run/packages/linux-arm64/prebuilds.json b/native/landlock-run/packages/linux-arm64/prebuilds.json new file mode 100644 index 0000000000..81e6b429f7 --- /dev/null +++ b/native/landlock-run/packages/linux-arm64/prebuilds.json @@ -0,0 +1,10 @@ +{ + "platform": "linux-arm64", + "binaries": [ + { + "tool": "landlock-run", + "kind": "static-musl", + "path": "bin/landlock-run" + } + ] +} diff --git a/native/landlock-run/packages/linux-x64/LICENSE b/native/landlock-run/packages/linux-x64/LICENSE new file mode 100644 index 0000000000..8187059c9a --- /dev/null +++ b/native/landlock-run/packages/linux-x64/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2026, node-addon-landlock-run contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/native/landlock-run/packages/linux-x64/README.md b/native/landlock-run/packages/linux-x64/README.md new file mode 100644 index 0000000000..ce741eb34c --- /dev/null +++ b/native/landlock-run/packages/linux-x64/README.md @@ -0,0 +1,7 @@ +# node-addon-landlock-run-linux-x64 + +Prebuilt `bin/landlock-run` Landlock launcher for linux-x64 — a static musl binary compiled natively (no cross toolchain) from the C source shipped in [`node-addon-landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run). npm's `os`/`cpu` fields select this package at install time; the entry package resolves it to a file path — it ships no JavaScript and is never imported. + +The binary is git-ignored and rides the npm tarball via the `files` list; the `prepack` gate refuses to pack when it is missing or has the wrong ELF architecture, and the release pipeline byte-pins the packed binary against the CI build it came from. Static musl linking means one binary for glibc and musl distros alike — hence no libc suffix in the name. + +Sibling: `node-addon-landlock-run-linux-arm64`. diff --git a/native/landlock-run/packages/linux-x64/package.json b/native/landlock-run/packages/linux-x64/package.json new file mode 100644 index 0000000000..8ea60b636c --- /dev/null +++ b/native/landlock-run/packages/linux-x64/package.json @@ -0,0 +1,26 @@ +{ + "name": "node-addon-landlock-run-linux-x64", + "version": "0.0.1", + "description": "Prebuilt landlock-run Landlock launcher binary for linux-x64 (static musl) — resolved as a file path by node-addon-landlock-run, never imported", + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "files": [ + "README.md", + "bin/", + "prebuilds.json" + ], + "scripts": { + "prepack": "node ../../scripts/verify-launcher-binary.mjs" + }, + "engines": { + "node": ">=20" + }, + "license": "BSD-3-Clause", + "publishConfig": { + "access": "public" + } +} diff --git a/native/landlock-run/packages/linux-x64/prebuilds.json b/native/landlock-run/packages/linux-x64/prebuilds.json new file mode 100644 index 0000000000..27b0de360c --- /dev/null +++ b/native/landlock-run/packages/linux-x64/prebuilds.json @@ -0,0 +1,10 @@ +{ + "platform": "linux-x64", + "binaries": [ + { + "tool": "landlock-run", + "kind": "static-musl", + "path": "bin/landlock-run" + } + ] +} diff --git a/native/landlock-run/pnpm-lock.yaml b/native/landlock-run/pnpm-lock.yaml new file mode 100644 index 0000000000..88b1b3df00 --- /dev/null +++ b/native/landlock-run/pnpm-lock.yaml @@ -0,0 +1,345 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^24.10.0 + version: 24.13.2 + node-addon-landlock-run: + specifier: workspace:* + version: link:packages/entry + tsx: + specifier: ^4.20.6 + version: 4.23.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + packages/entry: + optionalDependencies: + node-addon-landlock-run-linux-arm64: + specifier: workspace:* + version: link:../linux-arm64 + node-addon-landlock-run-linux-x64: + specifier: workspace:* + version: link:../linux-x64 + + packages/linux-arm64: {} + + packages/linux-x64: {} + +packages: + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/node@24.13.2': + resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + tsx@4.23.0: + resolution: {integrity: sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + +snapshots: + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@types/node@24.13.2': + dependencies: + undici-types: 7.18.2 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + fsevents@2.3.3: + optional: true + + tsx@4.23.0: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@7.18.2: {} diff --git a/native/landlock-run/pnpm-workspace.yaml b/native/landlock-run/pnpm-workspace.yaml new file mode 100644 index 0000000000..22299bfea0 --- /dev/null +++ b/native/landlock-run/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +packages: + - packages/* + +# pnpm 10+ blocks any dependency shipping an install/build script until it is +# explicitly reviewed here. Deny by default; esbuild (tsx's bundled native +# binary) genuinely needs its script. +allowBuilds: + esbuild: true diff --git a/native/landlock-run/scripts/assemble-prebuilds.mjs b/native/landlock-run/scripts/assemble-prebuilds.mjs new file mode 100644 index 0000000000..4dcdb23bed --- /dev/null +++ b/native/landlock-run/scripts/assemble-prebuilds.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +/** + * Assemble downloaded release artifacts into the platform packages and + * verify the result. The Release workflow's build legs upload one + * `prebuild-<package>` artifact per platform package (its `bin/` payload); + * this script copies each into `packages/<package>/bin/` and then checks + * every declared binary for presence and ELF architecture. + * + * Usage: `node scripts/assemble-prebuilds.mjs <artifact-root>`. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { platformDirs, root, verifyPlatformBinaries } from './repo.mjs'; + +const artifactRoot = path.resolve(process.argv[2] || '.release/prebuild-artifacts'); + +if (!fs.existsSync(artifactRoot)) { + throw new Error(`prebuild artifact directory does not exist: ${artifactRoot}`); +} + +const platforms = platformDirs().map((dir) => path.basename(dir)); + +for (const name of platforms) { + const binDir = path.join(root, 'packages', name, 'bin'); + fs.rmSync(binDir, { recursive: true, force: true }); + fs.mkdirSync(binDir, { recursive: true }); +} + +for (const artifactName of fs.readdirSync(artifactRoot)) { + const artifactDir = path.join(artifactRoot, artifactName); + if (!fs.statSync(artifactDir).isDirectory()) continue; + + const name = platforms.find((candidate) => artifactName === `prebuild-${candidate}`); + if (!name) { + throw new Error(`cannot map artifact to a platform package: ${artifactName}`); + } + + for (const file of fs.readdirSync(artifactDir)) { + const source = path.join(artifactDir, file); + const destination = path.join(root, 'packages', name, 'bin', file); + fs.copyFileSync(source, destination); + fs.chmodSync(destination, 0o755); + console.log(`Copied ${path.relative(root, source)} -> ${path.relative(root, destination)}`); + } +} + +for (const dir of platformDirs()) { + const { name, count } = verifyPlatformBinaries(path.join(root, dir)); + console.log(`Verified ${name}: ${count} binaries`); +} diff --git a/native/landlock-run/scripts/build.ts b/native/landlock-run/scripts/build.ts new file mode 100644 index 0000000000..5866cc0dc4 --- /dev/null +++ b/native/landlock-run/scripts/build.ts @@ -0,0 +1,86 @@ +/** + * Build every native tool this host can build, into its per-platform + * package. + * + * Targets are derived from the checked-in matrix: each + * `packages/<name>/prebuilds.json` whose `platform` matches this host names + * the binaries to produce; the TOOLS table below maps each `tool` to its C + * source. Builds are NATIVE-ONLY — each Linux architecture compiles its own + * binary with the distro's `musl-gcc` (static musl: runs on glibc and musl + * distros alike, no loader or libc expectations on the consumer host), and + * CI's per-arch runners are the builders of record. No cross toolchain + * exists here on purpose: native runners replace it, and the audit surface + * is the reviewed C source plus CI provenance. + * + * Binaries land in `packages/<name>/bin/` — git-ignored (root + * `.gitignore`), packed into the platform package's npm tarball behind its + * `prepack` gate (`scripts/verify-launcher-binary.mjs`). + * + * Run: `pnpm run build:native` (Linux with musl-gcc on PATH: + * `apt-get install musl-tools`). Non-Linux hosts fail fast — no platform + * package exists for them to build. + */ +import { spawnSync } from 'node:child_process' +import { existsSync, mkdirSync, readdirSync, readFileSync } from 'node:fs' +import { basename, dirname, join, resolve } from 'node:path' + +/** Each native tool's C source, keyed by the `tool` field in prebuilds.json. */ +const TOOLS: Record<string, { source: string }> = { + 'landlock-run': { source: 'packages/entry/src/main.c' }, +} + +const repoRoot = resolve(import.meta.dirname, '..') + +if (process.platform !== 'linux') { + console.error(`build: native tools are built natively per Linux architecture (no cross toolchain) — nothing to build on ${process.platform}. CI's per-arch runners build and rehearse every platform package.`) + process.exit(1) +} +const hostPlatform = `linux-${process.arch}` + +/** This host's platform packages, from the checked-in matrix. */ +const targets: { packageDir: string; tool: string; binaryPath: string; kind: string }[] = [] +const packagesRoot = join(repoRoot, 'packages') +for (const name of readdirSync(packagesRoot).sort()) { + const prebuildsFile = join(packagesRoot, name, 'prebuilds.json') + if (!existsSync(prebuildsFile)) continue + const prebuilds = JSON.parse(readFileSync(prebuildsFile, 'utf8')) as { + platform: string + binaries: { tool: string; kind: string; path: string }[] + } + if (prebuilds.platform !== hostPlatform) continue + for (const binary of prebuilds.binaries) { + targets.push({ packageDir: join(packagesRoot, name), tool: binary.tool, binaryPath: binary.path, kind: binary.kind }) + } +} +if (targets.length === 0) { + console.error(`build: no platform package declares binaries for ${hostPlatform} — supported platforms are the packages/*/prebuilds.json "platform" values.`) + process.exit(1) +} + +for (const target of targets) { + const tool = TOOLS[target.tool] + if (tool === undefined) { + console.error(`build: prebuilds.json names unknown tool "${target.tool}" — add it to the TOOLS table in scripts/build.ts.`) + process.exit(1) + } + if (target.kind !== 'static-musl') { + console.error(`build: unknown binary kind "${target.kind}" — the only toolchain here is static musl.`) + process.exit(1) + } + const binary = join(target.packageDir, target.binaryPath) + mkdirSync(dirname(binary), { recursive: true }) + + // -static against musl: self-contained, no loader/libc expectations on the + // consumer host. -Werror is safe to keep hard: CI pins the builder images, + // and a new warning on a toolchain bump deserves a look, not a pass. + const result = spawnSync('musl-gcc', [ + '-std=c11', '-Os', '-Wall', '-Wextra', '-Werror', '-static', '-s', + '-o', binary, join(repoRoot, tool.source), + ], { stdio: ['ignore', 'inherit', 'inherit'] }) + if (result.error !== undefined || result.status !== 0) { + console.error('build: musl-gcc failed' + + (result.error ? ` (${result.error.message} — is musl-tools installed?)` : '')) + process.exit(1) + } + console.log(`build: built ${basename(target.packageDir)}/${target.binaryPath}`) +} diff --git a/native/landlock-run/scripts/bump-release.mjs b/native/landlock-run/scripts/bump-release.mjs new file mode 100644 index 0000000000..29a7777379 --- /dev/null +++ b/native/landlock-run/scripts/bump-release.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +/** + * Bump every package (workspace root + packages/*) to one version, refresh + * the lockfile, and verify. Usage: `pnpm release:bump <major|minor|patch|x.y.z>`. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { packageDirs, readJson, root } from './repo.mjs'; + +const bump = process.argv[2]; +const releaseTypes = new Set(['major', 'minor', 'patch']); + +function writeJson(file, value) { + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +function run(command, args) { + const result = spawnSync(command, args, { + cwd: root, + stdio: 'inherit', + env: { ...process.env, CI: 'true' }, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function packageFiles() { + return ['package.json', ...packageDirs().map((dir) => path.join(dir, 'package.json'))]; +} + +function parseVersion(version) { + const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version); + if (!match) { + throw new Error(`increment types need a plain x.y.z current version (current: ${version}) — pass an explicit target version instead`); + } + return match.slice(1).map((part) => Number(part)); +} + +/** Explicit target versions accept full semver, prereleases included (test publishes). */ +const EXPLICIT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/; + +function nextVersion(current, release) { + if (EXPLICIT_VERSION.test(release)) return release; + + if (!releaseTypes.has(release)) { + throw new Error('Usage: pnpm release:bump <major|minor|patch|x.y.z>'); + } + + const [major, minor, patch] = parseVersion(current); + if (release === 'major') return `${major + 1}.0.0`; + if (release === 'minor') return `${major}.${minor + 1}.0`; + return `${major}.${minor}.${patch + 1}`; +} + +function currentPublishedVersion(files) { + const versions = new Set( + files + .filter((file) => file.startsWith('packages/')) + .map((file) => readJson(path.join(root, file)).version), + ); + if (versions.size !== 1) { + throw new Error(`published package versions differ: ${[...versions].join(', ')}`); + } + return [...versions][0]; +} + +if (!bump) { + console.error('Usage: pnpm release:bump <major|minor|patch|x.y.z>'); + process.exit(1); +} + +const files = packageFiles(); +const targetVersion = nextVersion(currentPublishedVersion(files), bump); + +for (const file of files) { + const fullPath = path.join(root, file); + const json = readJson(fullPath); + json.version = targetVersion; + writeJson(fullPath, json); + console.log(`${file}: ${targetVersion}`); +} + +run('pnpm', ['install', '--ignore-scripts', '--lockfile-only']); +run('node', ['./scripts/verify-release.mjs']); + +console.log(`Release version bumped to ${targetVersion}`); diff --git a/native/landlock-run/scripts/commit-release.mjs b/native/landlock-run/scripts/commit-release.mjs new file mode 100644 index 0000000000..b7bf3e513b --- /dev/null +++ b/native/landlock-run/scripts/commit-release.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env node +/** + * Bump, stage, and commit a release in one command: + * `pnpm release:commit <major|minor|patch|x.y.z>`. The tag stays manual — + * create it from the merged release commit. + */ + +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { packageDirs, readJson, root } from './repo.mjs'; + +const bump = process.argv[2]; + +function run(command, args) { + const result = spawnSync(command, args, { + cwd: root, + stdio: 'inherit', + env: { ...process.env, CI: 'true' }, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +if (!bump) { + console.error('Usage: pnpm release:commit <major|minor|patch|x.y.z>'); + process.exit(1); +} + +run('node', ['./scripts/bump-release.mjs', bump]); + +const version = readJson(path.join(root, packageDirs()[0], 'package.json')).version; +run('git', [ + 'add', + 'package.json', + 'packages/*/package.json', + 'pnpm-lock.yaml', +]); +run('git', ['commit', '-m', `release: ${version}`]); + +console.log(`Committed release ${version}. Create the tag manually: git tag v${version}`); diff --git a/native/landlock-run/scripts/github-matrix.mjs b/native/landlock-run/scripts/github-matrix.mjs new file mode 100644 index 0000000000..9566b89c8a --- /dev/null +++ b/native/landlock-run/scripts/github-matrix.mjs @@ -0,0 +1,66 @@ +#!/usr/bin/env node +/** + * Derive the GitHub Actions matrices from the checked-in package matrix + * (`packages/<name>/prebuilds.json`). Single source: adding a platform + * package extends CI and Release without editing a workflow. + * + * node scripts/github-matrix.mjs ci → one leg per distinct platform + * node scripts/github-matrix.mjs release-prebuild → one leg per platform package + */ + +import path from 'node:path'; +import { platformDirs, readJson, root } from './repo.mjs'; + +/** GitHub runner per prebuilds.json `platform` value — native builders only, no cross toolchain. */ +const RUNNERS = { + 'linux-x64': 'ubuntu-24.04', + 'linux-arm64': 'ubuntu-24.04-arm', +}; + +function runnerFor(platform) { + const runner = RUNNERS[platform]; + if (!runner) { + throw new Error(`missing GitHub runner for platform: ${platform}`); + } + return runner; +} + +function platformManifests() { + return platformDirs().map((dir) => ({ + dir, + name: path.basename(dir), + prebuilds: readJson(path.join(root, dir, 'prebuilds.json')), + })); +} + +function ciMatrix() { + const platforms = [...new Set(platformManifests().map(({ prebuilds }) => prebuilds.platform))].sort(); + return { + include: platforms.map((platform) => ({ platform, runner: runnerFor(platform) })), + }; +} + +function releasePrebuildMatrix() { + return { + include: platformManifests().map(({ dir, name, prebuilds }) => ({ + platform: prebuilds.platform, + package: name, + dir, + runner: runnerFor(prebuilds.platform), + artifact: `prebuild-${name}`, + })), + }; +} + +const target = process.argv[2]; +const matrices = { + ci: ciMatrix, + 'release-prebuild': releasePrebuildMatrix, +}; + +if (!target || !matrices[target]) { + console.error(`Usage: node scripts/github-matrix.mjs <${Object.keys(matrices).join('|')}>`); + process.exit(1); +} + +process.stdout.write(JSON.stringify(matrices[target]())); diff --git a/native/landlock-run/scripts/pack-release.mjs b/native/landlock-run/scripts/pack-release.mjs new file mode 100644 index 0000000000..fbec0b610b --- /dev/null +++ b/native/landlock-run/scripts/pack-release.mjs @@ -0,0 +1,76 @@ +#!/usr/bin/env node +/** + * Pack every published package into release tarballs, in publish order + * (platform packages first, then the entries that optionally depend on + * them), and write `publish-order.txt` next to them. `pnpm pack` produces + * the EXACT bytes `pnpm publish` would upload and runs each package's + * `prepack` gate, so a missing binary or unbuilt `lib/` refuses here. + * + * Usage: `node scripts/pack-release.mjs [dest] [--current-platform-only]`. + * The flag packs only THIS host's platform package plus the entries — for + * per-architecture CI legs, where the other architecture's binary does not + * exist (the exact refusal its prepack gate exists for). + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { entryDirs, platformDirs, readJson, root } from './repo.mjs'; + +const args = process.argv.slice(2); +const currentPlatformOnly = args.includes('--current-platform-only'); +const destination = path.resolve(args.find((arg) => !arg.startsWith('--')) || path.join(root, 'dist', 'npm')); + +function hostPlatformDirs() { + const hostPlatform = `${process.platform}-${process.arch}`; + return platformDirs().filter((dir) => readJson(path.join(root, dir, 'prebuilds.json')).platform === hostPlatform); +} + +function run(command, args) { + const result = spawnSync(command, args, { + cwd: root, + stdio: 'inherit', + }); + if (result.error) throw result.error; + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function tarballName(manifest) { + if (manifest.name.startsWith('@')) { + return `${manifest.name.slice(1).replace('/', '-')}-${manifest.version}.tgz`; + } + return `${manifest.name}-${manifest.version}.tgz`; +} + +fs.rmSync(destination, { recursive: true, force: true }); +fs.mkdirSync(destination, { recursive: true }); + +const dirs = [...(currentPlatformOnly ? hostPlatformDirs() : platformDirs()), ...entryDirs()]; +const platformSet = new Set(platformDirs()); +const publishOrder = []; +for (const dir of dirs) { + const manifest = readJson(path.join(root, dir, 'package.json')); + // Platform packages are packed with npm: pnpm pack (observed on 11.7.0) + // normalizes file modes and STRIPS the executable bit, which ships a + // launcher no consumer can spawn; npm pack preserves it. Platform packages + // have no dependencies by construction, so they need none of pnpm's + // workspace-protocol conversion — the entry packages do, and carry no + // executables, so they keep pnpm pack. + if (platformSet.has(dir)) { + run('npm', ['pack', `./${dir}`, '--pack-destination', destination]); + } else { + run('pnpm', ['--dir', dir, 'pack', '--pack-destination', destination]); + } + + const tarball = tarballName(manifest); + const tarballPath = path.join(destination, tarball); + if (!fs.existsSync(tarballPath)) { + throw new Error(`expected pack output not found: ${tarballPath}`); + } + publishOrder.push(tarball); +} + +fs.writeFileSync(path.join(destination, 'publish-order.txt'), `${publishOrder.join('\n')}\n`); +console.log(`Packed ${publishOrder.length} packages into ${path.relative(root, destination)}`); diff --git a/native/landlock-run/scripts/repo.mjs b/native/landlock-run/scripts/repo.mjs new file mode 100644 index 0000000000..8032d3da37 --- /dev/null +++ b/native/landlock-run/scripts/repo.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node +/** + * Shared helpers for the repo scripts: package discovery, the checked-in + * prebuild matrix, and binary verification. The package matrix is explicit + * metadata — `packages/<name>/prebuilds.json` marks a platform package and + * declares its binaries; everything else under `packages/` is an entry + * package. Scripts derive from these files and never guess. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const root = fileURLToPath(new URL('..', import.meta.url)); +export const packagesRoot = path.join(root, 'packages'); + +/** ELF `e_machine` (offset 18, little-endian) per platform-package `cpu` value. */ +export const E_MACHINE = { x64: 62, arm64: 183 }; + +export function readJson(file) { + return JSON.parse(fs.readFileSync(file, 'utf8')); +} + +/** Platform packages: every `packages/<name>` carrying a `prebuilds.json`. */ +export function platformDirs() { + return fs.readdirSync(packagesRoot) + .filter((name) => fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json'))) + .sort() + .map((name) => path.join('packages', name)); +} + +/** Entry packages: every other `packages/<name>` with a `package.json`. */ +export function entryDirs() { + return fs.readdirSync(packagesRoot) + .filter((name) => !fs.existsSync(path.join(packagesRoot, name, 'prebuilds.json'))) + .filter((name) => fs.existsSync(path.join(packagesRoot, name, 'package.json'))) + .sort() + .map((name) => path.join('packages', name)); +} + +/** All published packages in publish order: platform packages before the entries that optionally depend on them. */ +export function packageDirs() { + return [...platformDirs(), ...entryDirs()]; +} + +/** + * Verify one platform package's binaries against its `prebuilds.json`: + * every declared binary exists, nothing undeclared sits in `bin/`, and each + * file's ELF `e_machine` matches the package's declared `cpu`. Throws with + * a remediation message on the first mismatch. + */ +export function verifyPlatformBinaries(packageDir) { + const manifest = readJson(path.join(packageDir, 'package.json')); + const prebuilds = readJson(path.join(packageDir, 'prebuilds.json')); + const cpu = manifest.cpu?.[0]; + if (cpu === undefined || !(cpu in E_MACHINE)) { + throw new Error(`${manifest.name}: unsupported or missing "cpu" in package.json (expected one of: ${Object.keys(E_MACHINE).join(', ')})`); + } + + for (const binary of prebuilds.binaries) { + const file = path.join(packageDir, binary.path); + if (!fs.existsSync(file)) { + throw new Error(`${manifest.name}: missing ${binary.path} — run \`pnpm build:native\` on a ${prebuilds.platform} host (or assemble release artifacts) before packing.`); + } + try { + fs.accessSync(file, fs.constants.X_OK); + } catch { + // Only reachable when the mode was mangled somewhere between build and + // here (e.g. an archive step that normalized permissions) — the build + // itself always produces 755. + throw new Error(`${manifest.name}: ${binary.path} is not executable — a pack/extract step stripped the mode bit.`); + } + const machine = fs.readFileSync(file).readUInt16LE(18); + if (machine !== E_MACHINE[cpu]) { + throw new Error(`${manifest.name}: ${binary.path} has ELF e_machine ${machine}, expected ${E_MACHINE[cpu]} for ${cpu} — the binary was built for a different architecture.`); + } + } + + const declared = prebuilds.binaries.map((binary) => path.basename(binary.path)).sort(); + const binDir = path.join(packageDir, 'bin'); + const actual = fs.existsSync(binDir) ? fs.readdirSync(binDir).sort() : []; + const extra = actual.filter((name) => !declared.includes(name)); + if (extra.length) { + throw new Error(`${manifest.name}: bin/ contains files not declared in prebuilds.json: ${extra.join(', ')}`); + } + + return { name: manifest.name, count: prebuilds.binaries.length }; +} diff --git a/native/landlock-run/scripts/verify-entry-lib.mjs b/native/landlock-run/scripts/verify-entry-lib.mjs new file mode 100644 index 0000000000..214705e2bc --- /dev/null +++ b/native/landlock-run/scripts/verify-entry-lib.mjs @@ -0,0 +1,25 @@ +#!/usr/bin/env node +/** + * Prepack gate for entry packages: refuse to pack a tarball whose built + * `lib/` is missing. Entry `files` lists use globs, and a glob matching + * nothing packs a silently JS-less tarball instead of failing — this gate + * turns that into a loud refusal on a checkout that never ran + * `pnpm build:ts`. + * + * Runs from each entry package's `prepack` hook (pnpm sets the script cwd + * to the package directory). + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +const packageDir = process.cwd(); +const manifest = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')); + +for (const file of ['lib/index.js', 'lib/index.d.ts']) { + if (!fs.existsSync(path.join(packageDir, file))) { + console.error(`verify-entry-lib: ${manifest.name} has no ${file} — run \`pnpm build:ts\` before packing.`); + process.exit(1); + } +} +console.log(`verify-entry-lib: ${manifest.name} built lib/ present.`); diff --git a/native/landlock-run/scripts/verify-launcher-binary.mjs b/native/landlock-run/scripts/verify-launcher-binary.mjs new file mode 100644 index 0000000000..083cd837aa --- /dev/null +++ b/native/landlock-run/scripts/verify-launcher-binary.mjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +/** + * Prepack gate for platform packages: refuse to pack a tarball whose + * declared binaries are missing or built for the wrong architecture. + * + * Without it, `pnpm pack` on a checkout that never ran + * `pnpm run build:native` would ship an EMPTY platform package — the + * binary's absence surfacing only at runtime as a failed probe on every + * consumer — and a binary copied across packages would advertise an + * architecture it cannot execute. The check is presence + ELF `e_machine` + * against the package's declared `cpu`; byte provenance is + * `verify-packed-install.mjs`'s concern (it pins the installed tarball + * against the workspace build). + * + * Runs from each platform package's `prepack` hook (pnpm sets the script + * cwd to the package directory). Also callable directly with an explicit + * package directory: `node scripts/verify-launcher-binary.mjs packages/<name>`. + */ + +import path from 'node:path'; +import { root, verifyPlatformBinaries } from './repo.mjs'; + +const packageDir = process.argv[2] ? path.resolve(root, process.argv[2]) : process.cwd(); + +try { + const { name, count } = verifyPlatformBinaries(packageDir); + console.log(`verify-launcher-binary: ${name} — ${count} binaries present with the right ELF architecture.`); +} catch (error) { + console.error(`verify-launcher-binary: ${error instanceof Error ? error.message : error}`); + process.exit(1); +} diff --git a/native/landlock-run/scripts/verify-packed-install.mjs b/native/landlock-run/scripts/verify-packed-install.mjs new file mode 100644 index 0000000000..60f225a9d2 --- /dev/null +++ b/native/landlock-run/scripts/verify-packed-install.mjs @@ -0,0 +1,223 @@ +#!/usr/bin/env node +/** + * Publish-path rehearsal without publishing: verify the packed tarballs are + * exactly what a consumer install needs. `pnpm pack` already produced the + * bytes `pnpm publish` would upload; this script checks the payload + * (coverage, concrete dependency versions, NO lifecycle install scripts — + * this family has no install fallback on purpose), unpacks the entry plus + * THIS host's platform tarball into a throwaway consumer OUTSIDE the repo, + * byte-pins the installed binary against the workspace build it was packed + * from, and drives the INSTALLED entry under plain `node` — resolution, + * probe, and a real confinement world-proof through the installed launcher. + * + * On non-Linux hosts (no platform package exists) it instead proves the + * documented degradation: resolution falls back to a nonexistent path and + * the probe reports `unusable`. + * + * Usage: `node scripts/verify-packed-install.mjs [tarball-dir] [--current-platform-only]`. + * The flag skips the all-platforms tarball-presence check for + * per-architecture CI legs. `NALR_REQUIRE_LANDLOCK=1` makes an unenforcing + * kernel a failure instead of a skipped world-proof (set on CI, where the + * kernel is known). + */ + +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { entryDirs, packageDirs, platformDirs, readJson, root } from './repo.mjs'; + +const args = process.argv.slice(2); +const currentPlatformOnly = args.includes('--current-platform-only'); +const tarballDir = path.resolve(args.find((arg) => !arg.startsWith('--')) || path.join(root, 'dist', 'npm')); +const entryPackageName = 'node-addon-landlock-run'; + +function tarballName(manifest) { + if (manifest.name.startsWith('@')) { + return `${manifest.name.slice(1).replace('/', '-')}-${manifest.version}.tgz`; + } + return `${manifest.name}-${manifest.version}.tgz`; +} + +function tarballPath(manifest) { + const tarball = path.join(tarballDir, tarballName(manifest)); + if (!fs.existsSync(tarball)) { + throw new Error(`missing packed tarball: ${tarball}`); + } + return tarball; +} + +function run(command, commandArgs, options = {}) { + const result = spawnSync(command, commandArgs, { + cwd: options.cwd || root, + stdio: 'inherit', + env: { ...process.env, ...options.env }, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function runCapture(command, commandArgs) { + const result = spawnSync(command, commandArgs, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); + if (result.error) throw result.error; + if (result.status !== 0) { + process.stderr.write(result.stderr); + process.exit(result.status ?? 1); + } + return result.stdout; +} + +function readPackedManifest(manifest) { + return JSON.parse(runCapture('tar', ['-xOf', tarballPath(manifest), 'package/package.json'])); +} + +function verifyPackedManifest(packed) { + const lifecycle = ['preinstall', 'install', 'postinstall', 'prepare']; + for (const script of lifecycle) { + if (packed.scripts?.[script]) { + throw new Error(`${packed.name}: packed manifest carries a "${script}" lifecycle script — this family has no install fallback`); + } + } + for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies']) { + for (const [name, version] of Object.entries(packed[field] ?? {})) { + if (version.includes('workspace:')) { + throw new Error(`${packed.name}: packed ${field} still uses the workspace protocol: ${name}@${version}`); + } + } + } +} + +function sha256(file) { + return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); +} + +function packageInstallDir(packageName) { + return path.join(tempRoot, 'node_modules', ...packageName.split('/')); +} + +function unpackTarball(manifest) { + const extractRoot = fs.mkdtempSync(path.join(tempRoot, 'extract-')); + run('tar', ['-xzf', tarballPath(manifest), '-C', extractRoot]); + + const source = path.join(extractRoot, 'package'); + const destination = packageInstallDir(manifest.name); + fs.rmSync(destination, { recursive: true, force: true }); + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.renameSync(source, destination); + fs.rmSync(extractRoot, { recursive: true, force: true }); + console.log(`Unpacked ${manifest.name} -> ${path.relative(tempRoot, destination)}`); +} + +const manifests = packageDirs().map((dir) => ({ dir, manifest: readJson(path.join(root, dir, 'package.json')) })); +const entryManifest = manifests.find(({ manifest }) => manifest.name === entryPackageName)?.manifest; +if (!entryManifest) throw new Error(`missing source manifest for ${entryPackageName}`); + +const hostPlatform = `${process.platform}-${process.arch}`; +const currentPlatformEntry = manifests.find( + ({ dir, manifest }) => platformDirs().includes(dir) && manifest.name === `${entryPackageName}-${hostPlatform}`, +); + +// Payload checks: every expected tarball exists (full mode), the packed +// entry's optional-dependency set names exactly the platform packages, and +// no packed manifest carries workspace versions or install lifecycle. +const expectedTarballs = currentPlatformOnly + ? manifests.filter(({ dir }) => entryDirs().includes(dir) || dir === currentPlatformEntry?.dir) + : manifests; +for (const { manifest } of expectedTarballs) { + tarballPath(manifest); +} + +const packedEntry = readPackedManifest(entryManifest); +const platformPackageNames = manifests + .filter(({ dir }) => platformDirs().includes(dir)) + .map(({ manifest }) => manifest.name) + .sort(); +const optionalNames = Object.keys(packedEntry.optionalDependencies || {}).sort(); +if (optionalNames.join('\n') !== platformPackageNames.join('\n')) { + throw new Error(`packed entry optionalDependencies mismatch\nactual:\n${optionalNames.join('\n')}\nexpected:\n${platformPackageNames.join('\n')}`); +} +for (const { manifest } of expectedTarballs) { + verifyPackedManifest(readPackedManifest(manifest)); +} + +// Throwaway ESM consumer, built from local tarballs only — no registry. +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-packed-install-')); +fs.writeFileSync( + path.join(tempRoot, 'package.json'), + `${JSON.stringify({ name: 'nalr-packed-install-check', version: '0.0.0', private: true, type: 'module' }, null, 2)}\n`, +); +console.log(`Verifying packed install in ${tempRoot}`); + +unpackTarball(entryManifest); +if (currentPlatformEntry) { + unpackTarball(currentPlatformEntry.manifest); + + // Byte-pin: the installed binary must be the workspace build it was packed + // from — any divergence means the tarball did not carry the built bytes. + const prebuilds = readJson(path.join(root, currentPlatformEntry.dir, 'prebuilds.json')); + for (const binary of prebuilds.binaries) { + const workspaceFile = path.join(root, currentPlatformEntry.dir, binary.path); + const installedFile = path.join(packageInstallDir(currentPlatformEntry.manifest.name), binary.path); + if (sha256(workspaceFile) !== sha256(installedFile)) { + throw new Error(`installed ${binary.path} differs from the workspace build it was packed from`); + } + console.log(`Byte-pinned ${binary.path} against the workspace build`); + } +} else if (process.platform === 'linux') { + throw new Error(`linux host without a platform package in the matrix: ${hostPlatform}`); +} + +// Drive the INSTALLED entry under plain node: resolution, probe, and (on an +// enforcing kernel) a real confinement world-proof through the installed +// launcher. +const driver = path.join(tempRoot, 'driver.mjs'); +fs.writeFileSync(driver, ` +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { grantArgs, launcherPath, probe } from 'node-addon-landlock-run'; + +const requireLandlock = process.env.NALR_REQUIRE_LANDLOCK === '1'; +const platformPackage = 'node-addon-landlock-run-' + process.platform + '-' + process.arch; +const resolved = launcherPath(); +assert.ok(path.isAbsolute(resolved), 'launcherPath must be absolute'); +assert.ok(resolved.includes(path.join(...platformPackage.split('/'))), 'launcherPath must point into the platform package: ' + resolved); + +if (process.platform === 'linux') { + assert.ok(fs.existsSync(resolved), 'installed launcher missing at ' + resolved); + try { + fs.accessSync(resolved, fs.constants.X_OK); + } catch { + throw new Error('installed launcher is not executable — the pack path stripped the mode bit: ' + resolved); + } + const enforcement = probe(resolved); + console.log('probe through the installed launcher: ' + enforcement); + if (enforcement === 'unusable') { + if (requireLandlock) throw new Error('NALR_REQUIRE_LANDLOCK=1 but the probe reports unusable'); + console.log('kernel does not enforce Landlock — skipping the confinement world-proof'); + } else { + const work = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-confine-')); + const denied = path.join(work, 'denied.txt'); + const deniedRun = spawnSync(resolved, [...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', 'echo x > ' + denied], { encoding: 'utf8' }); + assert.notEqual(deniedRun.status, 0, 'write outside the grants must fail'); + assert.ok(!fs.existsSync(denied), 'denied write must not land on disk'); + const granted = path.join(work, 'granted.txt'); + const grantedRun = spawnSync(resolved, [...grantArgs({ readOnly: ['/'], readWrite: [work] }), '--', '/bin/sh', '-c', 'echo ok > ' + granted], { encoding: 'utf8' }); + assert.equal(grantedRun.status, 0, 'granted write must succeed: ' + grantedRun.stderr); + assert.equal(fs.readFileSync(granted, 'utf8').trim(), 'ok'); + console.log('confinement world-proof passed through the installed launcher'); + } +} else { + assert.ok(!fs.existsSync(resolved), 'no platform package exists for this host — the fallback path must not exist'); + assert.equal(probe(resolved), 'unusable'); + console.log('non-linux host: fallback resolution and unusable probe verified'); +} +`); +run(process.execPath, [driver], { cwd: tempRoot }); + +console.log('Packed install verification passed.'); diff --git a/native/landlock-run/scripts/verify-release.mjs b/native/landlock-run/scripts/verify-release.mjs new file mode 100644 index 0000000000..e812b34a14 --- /dev/null +++ b/native/landlock-run/scripts/verify-release.mjs @@ -0,0 +1,52 @@ +#!/usr/bin/env node +/** + * Release verification. Always: every published package carries one shared + * version, and — when running from a tag or publishing — the `vX.Y.Z` tag + * matches it. With `--prebuilds`: every platform package's declared + * binaries exist with the right ELF architecture (run after + * `assemble-prebuilds.mjs` or a local `build:native`). + */ + +import path from 'node:path'; +import { packageDirs, platformDirs, readJson, root, verifyPlatformBinaries } from './repo.mjs'; + +function verifyVersions() { + const packages = packageDirs().map((dir) => ({ + dir, + manifest: readJson(path.join(root, dir, 'package.json')), + })); + const versions = new Set(packages.map((pkg) => pkg.manifest.version)); + if (versions.size !== 1) { + throw new Error([ + 'published package versions must match:', + ...packages.map((pkg) => `${pkg.dir}: ${pkg.manifest.version}`), + ].join('\n')); + } + + const version = packages[0].manifest.version; + const ref = process.env.GITHUB_REF || ''; + const publish = process.env.RELEASE_PUBLISH === 'true'; + if (publish && !ref.startsWith('refs/tags/v')) { + throw new Error('publishing requires running the workflow from a v* tag'); + } + if (ref.startsWith('refs/tags/v')) { + const tagVersion = ref.slice('refs/tags/v'.length); + if (tagVersion !== version) { + throw new Error(`tag/version mismatch: tag v${tagVersion}, packages ${version}`); + } + } + + console.log(`Verified release version ${version}`); +} + +function verifyPrebuilds() { + for (const dir of platformDirs()) { + const { name, count } = verifyPlatformBinaries(path.join(root, dir)); + console.log(`Verified ${name}: ${count} binaries`); + } +} + +verifyVersions(); +if (process.argv.includes('--prebuilds')) { + verifyPrebuilds(); +} diff --git a/native/landlock-run/test/entry.test.js b/native/landlock-run/test/entry.test.js new file mode 100644 index 0000000000..2e2cfe8f17 --- /dev/null +++ b/native/landlock-run/test/entry.test.js @@ -0,0 +1,76 @@ +/** + * Keyless entry-package tests — run on every host, no kernel or binary + * required. Cover the JS seam's pure surface: grant-argv construction, the + * resolution contract (platform package → fallback), and probe verdicts over + * fake launchers. Requires built `lib/` (`pnpm build:ts`). + */ + +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { + LAUNCHER_BIN, + LAUNCHER_FAILURE_EXIT, + grantArgs, + launcherPath, + probe, +} from 'node-addon-landlock-run'; + +// --- constants are part of the CLI contract --- +assert.equal(LAUNCHER_BIN, 'landlock-run'); +assert.equal(LAUNCHER_FAILURE_EXIT, 125); + +// --- grantArgs: flag spelling, ordering, and empty grants --- +assert.deepEqual(grantArgs({}), []); +assert.deepEqual(grantArgs({ readOnly: ['/'] }), ['--ro', '/']); +assert.deepEqual( + grantArgs({ readOnly: ['/', '/opt'], readWrite: ['/tmp/work'] }), + ['--ro', '/', '--ro', '/opt', '--rw', '/tmp/work'], +); +assert.deepEqual(grantArgs({ readWrite: ['/a'], readOnly: ['/b'] }), ['--ro', '/b', '--rw', '/a']); + +// --- launcherPath: resolves the platform package next to its package.json --- +const platformPackage = `node-addon-landlock-run-${process.platform}-${process.arch}`; +const resolvedViaSeam = launcherPath((specifier) => { + assert.equal(specifier, `${platformPackage}/package.json`); + return path.join('/fake-install', specifier); +}); +assert.equal(resolvedViaSeam, path.join('/fake-install', platformPackage, 'bin', LAUNCHER_BIN)); + +// --- launcherPath: unresolvable package falls back to an absolute, package-boundary path --- +const fallback = launcherPath(() => { + throw new Error('not installed'); +}); +assert.ok(path.isAbsolute(fallback), 'fallback path must be absolute'); +assert.ok( + fallback.includes(path.join('node_modules', ...platformPackage.split('/'), 'bin', LAUNCHER_BIN)), + `fallback must point at the platform package layout: ${fallback}`, +); + +// --- launcherPath: default resolution agrees with this workspace's layout --- +const defaultPath = launcherPath(); +assert.ok(path.isAbsolute(defaultPath)); +assert.ok(defaultPath.endsWith(path.join('bin', LAUNCHER_BIN)), defaultPath); + +// --- probe: a missing launcher is unusable, indistinguishable from an unenforcing kernel --- +assert.equal(probe(path.join(os.tmpdir(), 'nalr-no-such-launcher')), 'unusable'); + +// --- probe: verdict parsing over fake launchers (POSIX shells only) --- +if (process.platform !== 'win32') { + const fakeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-fake-launcher-')); + const fake = (name, script) => { + const file = path.join(fakeDir, name); + fs.writeFileSync(file, `#!/bin/sh\n${script}\n`, { mode: 0o755 }); + return file; + }; + + assert.equal(probe(fake('full', 'echo "landlock: fully enforced"; exit 0')), 'full'); + assert.equal(probe(fake('partial', 'echo "landlock: partially enforced (older ABI)"; exit 0')), 'partial'); + assert.equal(probe(fake('failing', `exit ${LAUNCHER_FAILURE_EXIT}`)), 'unusable'); + assert.equal(probe(fake('hanging', 'sleep 10'), { timeoutMs: 200 }), 'unusable'); + + fs.rmSync(fakeDir, { recursive: true, force: true }); +} + +console.log('entry.test: ok'); diff --git a/native/landlock-run/test/launcher.test.js b/native/landlock-run/test/launcher.test.js new file mode 100644 index 0000000000..4ab0070e1c --- /dev/null +++ b/native/landlock-run/test/launcher.test.js @@ -0,0 +1,127 @@ +/** + * Behavioral tests against the REAL launcher binary on a real kernel: the + * CLI contract (usage errors, exit codes, argv passthrough) and the + * confinement world-proofs (denied writes stay off disk, grants land). + * + * Preconditions and their skip semantics: + * - Non-Linux host: skips entirely (exit 0) — there is nothing to build here. + * - Linux without the built binary: FAILS — run `pnpm build:native` first. + * - Linux whose kernel does not enforce Landlock: skips the enforcement + * half, unless `NALR_REQUIRE_LANDLOCK=1` (set on CI, where a silent skip on + * the very platform that exists to prove enforcement would be a false + * green). + */ + +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { + LAUNCHER_FAILURE_EXIT, + grantArgs, + launcherPath, + probe, +} from 'node-addon-landlock-run'; + +const requireLandlock = process.env.NALR_REQUIRE_LANDLOCK === '1'; + +if (process.platform !== 'linux') { + console.log(`launcher.test: SKIP — the launcher only exists on linux (host: ${process.platform})`); + process.exit(0); +} + +const launcher = launcherPath(); +assert.ok( + fs.existsSync(launcher), + `launcher.test: no built launcher at ${launcher} — run \`pnpm build:native\` (apt-get install musl-tools) first`, +); + +const run = (args, options = {}) => spawnSync(launcher, args, { encoding: 'utf8', ...options }); + +// --- usage errors: parse failures exit LAUNCHER_FAILURE_EXIT before any restriction --- +{ + const noCommand = run([]); + assert.equal(noCommand.status, LAUNCHER_FAILURE_EXIT); + assert.match(noCommand.stderr, /usage error: missing `-- <argv>\.\.\.` command/); + + const unknownFlag = run(['--bogus', '--', 'true']); + assert.equal(unknownFlag.status, LAUNCHER_FAILURE_EXIT); + assert.match(unknownFlag.stderr, /usage error: unknown argument: --bogus/); + + const danglingPath = run(['--ro']); + assert.equal(danglingPath.status, LAUNCHER_FAILURE_EXIT); + assert.match(danglingPath.stderr, /--ro requires a path/); + + for (const args of [ + ['--probe', '--ro', '/'], + ['--probe', '--'], + ['--probe', '--probe'], + ]) { + const probeWithExtras = run(args); + assert.equal(probeWithExtras.status, LAUNCHER_FAILURE_EXIT); + assert.match(probeWithExtras.stderr, /--probe takes no other arguments/); + } +} + +// --- probe: the functional availability signal --- +const enforcement = probe(launcher); +console.log(`launcher.test: probe → ${enforcement}`); +if (enforcement === 'unusable') { + if (requireLandlock) { + console.error('launcher.test: NALR_REQUIRE_LANDLOCK=1 but the probe reports unusable — this kernel cannot prove enforcement'); + process.exit(1); + } + console.log('launcher.test: SKIP enforcement half — kernel does not enforce Landlock'); + process.exit(0); +} +{ + const probeRun = run(['--probe']); + assert.equal(probeRun.status, 0); + assert.match(probeRun.stdout, /^landlock: (fully enforced|partially enforced \(older ABI\))\n$/); +} + +// --- confined exec: the command runs, its exit code passes through --- +{ + const echo = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', 'echo confined-ok']); + assert.equal(echo.status, 0, echo.stderr); + assert.equal(echo.stdout, 'confined-ok\n'); + + const exitCode = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', 'exit 7']); + assert.equal(exitCode.status, 7, 'the wrapped command exit code must pass through unchanged'); +} + +// --- world-proofs: denied writes stay off disk, grants land, inheritance crosses exec --- +{ + const work = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-launcher-test-')); + + const denied = path.join(work, 'denied.txt'); + const deniedRun = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', `echo x > ${denied}`]); + assert.notEqual(deniedRun.status, 0, 'a write outside the grants must fail'); + assert.ok(!fs.existsSync(denied), 'the denied write must not land on disk'); + + const granted = path.join(work, 'granted.txt'); + const grantedRun = run([...grantArgs({ readOnly: ['/'], readWrite: [work] }), '--', '/bin/sh', '-c', `echo ok > ${granted}`]); + assert.equal(grantedRun.status, 0, grantedRun.stderr); + assert.equal(fs.readFileSync(granted, 'utf8'), 'ok\n'); + + // The ruleset is inherited across execve: a CHILD of the wrapped command + // is confined too, not just the direct exec target. + const nested = path.join(work, 'nested.txt'); + const nestedRun = run([...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', `/bin/sh -c 'echo x > ${nested}'; true`]); + assert.equal(nestedRun.status, 0, nestedRun.stderr); + assert.ok(!fs.existsSync(nested), 'a denied write from a nested child must not land either'); + + fs.rmSync(work, { recursive: true, force: true }); +} + +// --- fail closed: an unopenable grant root refuses to exec at all --- +{ + const marker = path.join(os.tmpdir(), `nalr-should-not-exist-${process.pid}`); + const badGrant = run(['--ro', '/no/such/grant/root', '--', '/bin/sh', '-c', `echo x > ${marker}`]); + assert.equal(badGrant.status, LAUNCHER_FAILURE_EXIT); + assert.match(badGrant.stderr, /cannot open rule path/); + assert.ok(!fs.existsSync(marker), 'the command must never run when the launcher fails'); +} + +console.log('launcher.test: ok'); diff --git a/native/landlock-run/tsconfig.base.json b/native/landlock-run/tsconfig.base.json new file mode 100644 index 0000000000..a95ca64f6d --- /dev/null +++ b/native/landlock-run/tsconfig.base.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["node"] + } +} diff --git a/native/landlock-run/tsconfig.json b/native/landlock-run/tsconfig.json new file mode 100644 index 0000000000..3813d343cd --- /dev/null +++ b/native/landlock-run/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "noEmit": true + }, + "files": [], + "include": ["scripts/**/*.ts"], + "references": [ + { "path": "./packages/entry" } + ] +} diff --git a/package.json b/package.json index e736e3f8d1..995a2bf998 100644 --- a/package.json +++ b/package.json @@ -42,18 +42,23 @@ "verify-package-paths": "tsx scripts/verify-package-paths.ts", "verify-package-readme-model-experience": "tsx scripts/verify-package-readme-model-experience.ts", "verify-mermaid": "tsx scripts/verify-mermaid.ts", - "verify-rfc-classification": "tsx scripts/verify-rfc-classification.ts", - "verify-rfc-format": "tsx scripts/verify-rfc-format.ts", + "verify-agent-note-classification": "tsx scripts/verify-agent-note-classification.ts", + "verify-agent-note-format": "tsx scripts/verify-agent-note-format.ts", "verify-type-equiv": "tsx scripts/verify-type-equiv.ts", "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", "verify-cordis-config": "tsx scripts/verify-cordis-config.ts", "gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts", - "gen-rfc-index": "tsx scripts/gen-rfc-index.ts", "verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check", "gen-cordis-api": "tsx scripts/gen-cordis-api.ts", "verify-cordis-api": "tsx scripts/gen-cordis-api.ts --check", @@ -70,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-rfc-classification && pnpm run verify-rfc-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/AGENTS.md b/packages/AGENTS.md index c41c25b7a5..400436a9f1 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -6,9 +6,9 @@ These package-specific rules supplement the repo-wide [conventions](../AGENTS.md - **Optional services use `ctx.get(name)`.** Reserve `ctx.<name>` for declared injections; the property proxy is topology-sensitive, while strict `ctx.get` reads the global service store ([postmortem](../docs/postmortem/0001-acp-default-export-drops-inject.md)). - **Product-visible plugins require a non-unit REAL-composition test.** Hand-built `ctx.plugin(...)` suites are insufficient. Boot test-only `cordis.yml` through the Loader and app/process; mock only external/nondeterministic boundaries and assert model-visible, durable, or user-visible output. Keep opt-ins out of shipped defaults. [Policy](../docs/testing.md). - **Typed same-process service and plugin calls are contracts, not serialization boundaries.** Prefer readonly borrowed values; materialize or defensively validate only at parser/config, queued, model/tool JSON, durable/file, worker, process, or wire boundaries. -- **Initiator-owned private chains derive, then capture.** Under `ctx.agents.withInitiator()`, recover the Agent at each orchestration entry, derive `agent.session`, and let operation-local helpers close over it. Keep `Agent` and `Session` explicit at lifecycle, session-log, service, authority, worker/process, persistence, and wire interfaces; do not widen a leaf helper from `Session` to `Context` merely to hide a parameter ([rationale](../docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md)). +- **Initiator-owned private chains derive, then capture.** Under `ctx.agents.withInitiator()`, recover the Agent at each orchestration entry, derive `agent.session`, and let operation-local helpers close over it. Keep `Agent` and `Session` explicit at lifecycle, session-log, service, authority, worker/process, persistence, and wire interfaces; do not widen a leaf helper from `Session` to `Context` merely to hide a parameter ([rationale](../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md)). - **Represent one asynchronous operation with one lifecycle controller or transaction.** Separate readiness, cancellation, disposal, reservation, or sentinel state requires an independent owner or settlement boundary; otherwise fold it while preserving rollback, callback containment, and quiescence. -- **Shape capability interfaces around all current consumers.** Keep tool-schema, Loader, UI, transport, and backend-specific behavior in the consumer or adapter; do not let one consumer dictate the interface ([capability-seam rationale](../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)). +- **Shape capability interfaces around all current consumers.** Keep tool-schema, Loader, UI, transport, and backend-specific behavior in the consumer or adapter; do not let one consumer dictate the interface ([capability-seam rationale](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)). - **Require a current owner and need.** Tie each abstraction, state machine, option, defensive copy, and compatibility path to a current contract or production consumer, and keep behavior in its owning plugin or service. - **Require evidence for public choices.** Configurability does not justify an unsupported default, public operation set, format, or imported external concept. Use current-consumer evidence or relevant prior art; otherwise require an explicit value or defer the choice. - **Write model-facing contracts from the model's perspective.** Prompts, tool schemas, results, and diagnostics contain only task-relevant concepts, not UI, transport, or implementation vocabulary. Pin stable model-visible text verbatim and dynamic behavior through snapshots or end-to-end coverage. @@ -23,4 +23,4 @@ Naming notes: - Tests live at package level under `tests/`, not `src/__tests__/`. - A package's README and JSDoc are part of the change: altered behavior (config keys, defaults, error codes, wire fields) updates them in the same commit. `doc-sync` gates what it can; apply [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for complete, concise prose and verify accuracy against code. - Package READMEs document model, token, and KV-cache effects using the [canonical Model Experience format](../docs/cookbook/adding-a-package.md#4-write-the-package-readme). -- Package READMEs put durable consumer gaps and non-obvious maintainer constraints under `## Known Limitations and Deferred Work`; ordinary cleanup stays in its TODO or RFC. Packages with none use a justified [allowlist entry](../scripts/verify-package-readme-limitations.ts) ([rationale](../docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md)). +- Package READMEs put durable consumer gaps and non-obvious maintainer constraints under `## Known Limitations and Deferred Work`; ordinary cleanup stays in its TODO or Agent Note. Packages with none use a justified [allowlist entry](../scripts/verify-package-readme-limitations.ts) ([rationale](../.agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md)). diff --git a/packages/README.md b/packages/README.md index 61a1e994c8..9c6a979d61 100644 --- a/packages/README.md +++ b/packages/README.md @@ -25,7 +25,7 @@ Packages live at `packages/<group>/<pkg>/`; groups are containers, while names r | [`todo/`](todo/README.md) | Todo/planning family: the model-facing `todo_write` tool | Product — stable surface | | [`timeout/`](timeout/README.md) | Tool-call timeout policy: the `tools/execute` deadline enforcer | Product — stable surface | | [`guard/`](guard/README.md) | Loop-hygiene guards: advisory repeat-call reminders | Product — stable surface | -| [`cordis/`](cordis/README.md) | Self-referential runtime toolset: inspect the live runtime's plugins and services, mount/unmount model-written plugins ([design](../docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)) | Product — stable surface | +| [`cordis/`](cordis/README.md) | Self-referential runtime toolset: inspect the live runtime's plugins and services, mount/unmount model-written plugins ([design](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)) | Product — stable surface | | [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable surface | | [`session-persistence/`](session-persistence/README.md) | Persistence capability family: the seam + JSONL/SQLite backends | Product — stable surface | | [`session-query/`](session-query/README.md) | Session retrieval: logical corpus, bounded reads, lineage, and event relationships | Product — stable surface | @@ -41,6 +41,6 @@ Groups distinguish product API from support infrastructure. New packages join an The dependency graph is generated: [docs/module-graph.md](../docs/module-graph.md) (`pnpm run gen-module-graph`, freshness-gated in CI). -The rule it must obey: **extension plugins depend on interfaces, never on the concrete loop.** `dsh-agent-loop` is swappable — UI/hook/tool plugins keep working against the `dsh-agent` vocabulary if the loop is replaced. The sanctioned exception is a **composition/bundle** package like `dsh-agent-spine-demo`, whose whole job is to assemble the concrete spine: it depends on `dsh-agent-loop` (and the other concrete spine plugins) on purpose. The rule constrains plugins that EXTEND the system, not the bundle that COMPOSES it. A swappable capability splits into interface / implementation / consumer packages (the bash trio is the template — see [capability seams](../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)). +The rule it must obey: **extension plugins depend on interfaces, never on the concrete loop.** `dsh-agent-loop` is swappable — UI/hook/tool plugins keep working against the `dsh-agent` vocabulary if the loop is replaced. The sanctioned exception is a **composition/bundle** package like `dsh-agent-spine-demo`, whose whole job is to assemble the concrete spine: it depends on `dsh-agent-loop` (and the other concrete spine plugins) on purpose. The rule constrains plugins that EXTEND the system, not the bundle that COMPOSES it. A swappable capability splits into interface / implementation / consumer packages (the bash trio is the template — see [capability seams](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)). Package READMEs cover purpose, APIs, extension points, and [Model Experience](../docs/cookbook/adding-a-package.md#4-write-the-package-readme) unless on the model-agnostic [omission allowlist](../scripts/verify-package-readme-model-experience.ts). They also carry `## Known Limitations and Deferred Work` or use its [allowlist](../scripts/verify-package-readme-limitations.ts). diff --git a/packages/bash/README.md b/packages/bash/README.md index 20c11c5dff..2e2bb5692a 100644 --- a/packages/bash/README.md +++ b/packages/bash/README.md @@ -1,6 +1,6 @@ # bash/ — bash capability family -The canonical three-package capability seam (see [capability seams](../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)): an abstract executor interface, concrete implementations, and the model-facing tool that consumes it. All **product** packages. +The canonical three-package capability seam (see [capability seams](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract executor interface, concrete implementations, and the model-facing tool that consumes it. All **product** packages. | Package | Role | ctx key | |---|---|---| diff --git a/packages/bash/bash-local/README.md b/packages/bash/bash-local/README.md index fcd1317b75..5d99161fc0 100644 --- a/packages/bash/bash-local/README.md +++ b/packages/bash/bash-local/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-bash-local -Local-subprocess implementation of the `@deepseek-ai/dsh-bash` executor seam: `LocalBashExecutor` spawns `bash -c <command>` per call in its own process group, collects bounded output with full-stream spill files, and escalates kills SIGTERM→SIGKILL across the whole group. +Local-subprocess implementation of the `@deepseek-ai/dsh-bash` executor seam: `LocalBashExecutor` spawns `bash -c <command>` per call in its own process group, collects bounded output with size-limited full-stream spill files, and escalates kills SIGTERM→SIGKILL across the whole group. The package root exports the default and named `LocalBashExecutor` plugin plus its `Config`; subprocess plumbing stays internal to the implementation package. @@ -14,7 +14,8 @@ The package root exports the default and named `LocalBashExecutor` plugin plus i timeoutMs: 120000 # default foreground timeout maxTimeoutMs: 600000 # cap for per-call overrides maxOutputBytes: 64000 # per-stream in-memory cap; overflow spills to disk - graceMs: 3000 # SIGTERM→SIGKILL escalation grace on kills + maxSpillBytes: 67108864 # per-stream full-output spill cap + graceMs: 3000 # kill escalation and post-exit pipe-drain grace ``` ## Behavior (and where it came from) @@ -22,9 +23,9 @@ The package root exports the default and named `LocalBashExecutor` plugin plus i Design surveyed against the bash tools of Claude Code, OpenCode, Codex, and pi; the notable choices: - **Spawn per call, no shell state** — every call is a fresh non-login `bash -c` (deterministic; no rc files). All four surveyed tools spawn per call. `XXX(stateful-shell)` in `src/run.ts` records the two proven stateful designs (Claude Code's cwd-only persistence; Codex's PTY exec sessions) for when real workflows demand them. -- **Process-group kills with escalation** — children are spawned `detached` (own process group); kills send SIGTERM to the group, then SIGKILL after the `graceMs` grace (default 3s — OpenCode's escalation; pipelines and subshells die with the parent). ESRCH is tolerated; daemons that re-parent away from the group can still survive — same caveat as the surveyed tools. -- **Tail-keep truncation + spill files** — output beyond `maxOutputBytes` keeps the in-memory TAIL (errors/results cluster at the end — pi/OpenCode rationale) while the FULL stream is appended to a temp file whose path is reported when available. A foreground `BashExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background tasks still use `maxOutputBytes`. If the final spill close reports a delayed writeback failure, the executor still returns the tail but withholds the path rather than advertising a possibly incomplete file. -- **Model-friendly env + credential scrub** — `process.env` minus credential-shaped vars (`*KEY*`/`*SECRET*`/`*TOKEN*`) and all ambient `DSH_*` names, then `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` (Codex's hardcoded set) so pagers and ANSI color don't garble results. A spec's ordinary `env` is merged after the scrub but rejects `DSH_*`; managed `dshEnv` rejects ordinary names and merges last, preventing stale nested-harness identity. Supplied stdin is written and closed; otherwise fd 0 is `/dev/null`. See the [stdin/env RFC](../../../docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [managed environment RFC](../../../docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md). +- **Process-group kills with escalation** — children are spawned `detached` (own process group); kills send SIGTERM to the group, then SIGKILL after the `graceMs` grace (default 3s — OpenCode's escalation; pipelines and subshells die with the parent). After the main shell exits, inherited stdout/stderr pipes receive the same bounded drain grace so a surviving descendant cannot hold the command open indefinitely. ESRCH is tolerated; daemons that re-parent away from the group can still survive — same caveat as the surveyed tools. +- **Tail-keep truncation + bounded spill files** — output beyond `maxOutputBytes` keeps the in-memory TAIL (errors/results cluster at the end — pi/OpenCode rationale) while the FULL stream is appended to a temp file whose path is reported when available. A foreground `BashExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background tasks still use `maxOutputBytes`. A stream larger than `maxSpillBytes` discards its now-incomplete spill and returns only the marked truncated tail. If the final spill close reports a delayed writeback failure, the executor likewise withholds the path rather than advertising an incomplete file. +- **Model-friendly env + credential scrub** — `process.env` minus credential-shaped vars (`*KEY*`/`*SECRET*`/`*TOKEN*`) and all ambient `DSH_*` names, then `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` (Codex's hardcoded set) so pagers and ANSI color don't garble results. A spec's ordinary `env` is merged after the scrub but rejects `DSH_*`; managed `dshEnv` rejects ordinary names and merges last, preventing stale nested-harness identity. Supplied stdin is written and closed; otherwise fd 0 is `/dev/null`. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md). - **Background processes** — `start()` returns a live `BashProcess` handle immediately, no timeout applies (Claude Code detaches timeouts when backgrounding), the handle's `readOutput()` is incremental with whole-stream byte offsets, and disposal kills every running process and awaits its exit. Everything task-shaped (ids, ownership, polling, notices) lives in the generic [`ctx.tasks` runtime](../../tasks/tasks/README.md), which the tool layer registers the handle with — this executor never sees a session or a registry. ## Model Experience @@ -41,6 +42,6 @@ No direct invalidation; the named consumer owns any request-prefix changes. - **No persistent shell or PTY** — every call starts a fresh non-login `bash -c`; cwd-only persistence and interactive terminal sessions remain deferred until a real workflow requires them. - **POSIX-only** — the `bash` binary, detached process groups, group kills, and SIGTERM→SIGKILL escalation are hardcoded; Windows is unsupported. - **The credential scrub is a name heuristic** — `*KEY*`/`*SECRET*`/`*TOKEN*` only; differently-named secrets (e.g. `*PASSWORD*`) pass through, and a whitelist for over-scrubbed vars is noted future work. -- **Spill files are never deleted** — full-output recovery files (and the private per-process spill dir) accumulate under the OS tmpdir until something external cleans them. +- **Completed spill files are not deleted** — bounded full-output recovery files (and the private per-process spill dir) accumulate under the OS tmpdir until something external cleans them; oversize incomplete spills are discarded and deletion is attempted immediately, but a cleanup failure can leave a bounded file behind. The raw process handling lives in `src/run.ts`; `src/index.ts` is the service wiring. diff --git a/packages/bash/bash-local/src/index.ts b/packages/bash/bash-local/src/index.ts index 587c33b9a2..6428cd7ac8 100644 --- a/packages/bash/bash-local/src/index.ts +++ b/packages/bash/bash-local/src/index.ts @@ -10,7 +10,7 @@ import z from 'schemastery' import { BashExecutor } from '@deepseek-ai/dsh-bash' import type { BashExecRequest, BashExecSpec, BashProcess, BashProcessRead, BashRunResult } from '@deepseek-ai/dsh-bash' import { clampTimeout, deadline, timeoutOf } from '@deepseek-ai/dsh-timeout' -import { DEFAULT_GRACE_MS, runBash } from './run.ts' +import { DEFAULT_GRACE_MS, DEFAULT_MAX_SPILL_BYTES, runBash } from './run.ts' import type { RunInternals, RunningBash } from './run.ts' /** Plugin config (all optional — `static Config` supplies the defaults). */ @@ -23,7 +23,9 @@ export interface Config { maxTimeoutMs?: number /** Per-stream in-memory output cap; overflow spills to a temp file. */ maxOutputBytes?: number - /** Grace period between the SIGTERM and the SIGKILL escalation on a kill. */ + /** Per-stream spill-file cap; larger streams retain only their in-memory tail. */ + maxSpillBytes?: number + /** Grace period for kill escalation and for inherited pipes after shell exit. */ graceMs?: number } @@ -46,6 +48,7 @@ export class LocalBashExecutor extends BashExecutor { timeoutMs: z.number().default(120_000), maxTimeoutMs: z.number().default(600_000), maxOutputBytes: z.number().default(64_000), + maxSpillBytes: z.number().default(DEFAULT_MAX_SPILL_BYTES), graceMs: z.number().default(DEFAULT_GRACE_MS), }) @@ -64,6 +67,7 @@ export class LocalBashExecutor extends BashExecutor { assertPositiveFinite('timeoutMs', this.config.timeoutMs) assertPositiveFinite('maxTimeoutMs', this.config.maxTimeoutMs) assertPositiveFinite('maxOutputBytes', this.config.maxOutputBytes) + assertPositiveFinite('maxSpillBytes', this.config.maxSpillBytes) assertPositiveFinite('graceMs', this.config.graceMs) ctx.effect(() => async () => { // Await closure so even a TERM-trapping child cannot outlive the fiber. @@ -120,6 +124,7 @@ export class LocalBashExecutor extends BashExecutor { cwd: spec.workdir, stdoutMaxBytes: spec.stdoutMaxBytes, stderrMaxBytes: this.config.maxOutputBytes, + maxSpillBytes: this.config.maxSpillBytes, graceMs: this.config.graceMs, signal: d.signal, stdin: spec.stdin, @@ -139,6 +144,7 @@ export class LocalBashExecutor extends BashExecutor { cwd: spec.workdir, stdoutMaxBytes: this.config.maxOutputBytes, stderrMaxBytes: this.config.maxOutputBytes, + maxSpillBytes: this.config.maxSpillBytes, graceMs: this.config.graceMs, signal: spec.signal, stdin: spec.stdin, diff --git a/packages/bash/bash-local/src/run.ts b/packages/bash/bash-local/src/run.ts index fa4dae73d6..600e920c96 100644 --- a/packages/bash/bash-local/src/run.ts +++ b/packages/bash/bash-local/src/run.ts @@ -8,7 +8,7 @@ import { type ChildProcessByStdio, spawn } from 'node:child_process' import type { Readable, Writable } from 'node:stream' import { randomBytes } from 'node:crypto' -import { closeSync, mkdtempSync, openSync, writeSync } from 'node:fs' +import { closeSync, mkdtempSync, openSync, unlinkSync, writeSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-bash' @@ -72,7 +72,9 @@ export interface SpawnSpec { stdoutMaxBytes: number /** Stderr in-memory cap; overflow spills to disk (tail kept in memory). */ stderrMaxBytes: number - /** Grace period between the SIGTERM and the SIGKILL escalation on a kill. */ + /** Per-stream spill-file cap; larger streams retain only their in-memory tail. */ + maxSpillBytes: number + /** Grace period for kill escalation and for inherited pipes after shell exit. */ graceMs: number /** * Abort signal — kills the process group when it fires. The executor owns @@ -119,6 +121,9 @@ export interface RunInternals { /** Default SIGTERM→SIGKILL grace period (the `graceMs` config; matches OpenCode's 3s). */ export const DEFAULT_GRACE_MS = 3_000 +/** Default per-stream spill cap (the `maxSpillBytes` config). */ +export const DEFAULT_MAX_SPILL_BYTES = 64 * 1024 * 1024 + let spillCounter = 0 let defaultSpillDir: string | undefined @@ -133,9 +138,9 @@ function privateSpillDir(): string { } /** - * Collects one stream with a bounded in-memory tail. The FULL stream is - * always recoverable: on first overflow a spill file is created and every - * chunk (including those already collected) is appended there. + * Collects one stream with a bounded in-memory tail. On first overflow a + * spill file is created and every chunk (including those already collected) + * is appended there while the full stream remains within `maxSpillBytes`. * * Tail-keep rationale (pi/OpenCode): errors and final results cluster at the * end of command output; the spill file covers the head. @@ -146,11 +151,13 @@ export class OutputCollector { private dropped = false private spillFd: number | undefined private spillFile: string | undefined + private spillDisabled = false /** Total bytes ever pushed (not just retained). */ private total = 0 constructor( private readonly maxBytes: number, + private readonly maxSpillBytes: number, private readonly label: string, private readonly spillDir: string, ) {} @@ -166,7 +173,7 @@ export class OutputCollector { push(chunk: Buffer): void { this.total += chunk.length const overflows = this.bytes + chunk.length > this.maxBytes - if (overflows || this.spillFd !== undefined) this.spillAll(chunk) + if (!this.spillDisabled && (overflows || this.spillFd !== undefined)) this.spillAll(chunk) this.chunks.push(chunk) this.bytes += chunk.length while (this.bytes > this.maxBytes && this.chunks.length > 1) { @@ -188,6 +195,10 @@ export class OutputCollector { /** Open the spill file lazily and append `chunk` (and any prior chunks once). */ private spillAll(chunk: Buffer): void { + if (this.total > this.maxSpillBytes) { + this.discardSpill() + return + } if (this.spillFd === undefined) { // Random suffix + O_EXCL + no-follow-equivalent ('wx' fails on any // existing path, symlink or not) + owner-only mode: defeats spill-path @@ -202,6 +213,30 @@ export class OutputCollector { writeSync(this.spillFd, chunk) } + /** Stop spilling and remove the file once it can no longer hold the complete stream. */ + private discardSpill(): void { + const fd = this.spillFd + const file = this.spillFile + this.spillFd = undefined + this.spillFile = undefined + this.spillDisabled = true + if (fd !== undefined) { + try { + closeSync(fd) + } catch { + // Retain the descriptor so finalize can retry the failed close. + this.spillFd = fd + } + } + if (file !== undefined) { + try { + unlinkSync(file) + } catch { + // A failed unlink leaves at most maxSpillBytes behind, never an unbounded file. + } + } + } + /** * Incremental read in whole-stream byte coordinates: returns everything * pushed since `fromByte`. When `fromByte` has already slid out of the @@ -301,8 +336,8 @@ export function runBash(spec: SpawnSpec, internals: RunInternals = {}): RunningB ? spawn('bash', ['-c', spec.command], { cwd: spec.cwd, env, stdio: ['pipe', 'pipe', 'pipe'], detached: true }) : spawn('bash', ['-c', spec.command], { cwd: spec.cwd, env, stdio: ['ignore', 'pipe', 'pipe'], detached: true }) - const stdout = new OutputCollector(spec.stdoutMaxBytes, 'stdout', spillDir) - const stderr = new OutputCollector(spec.stderrMaxBytes, 'stderr', spillDir) + const stdout = new OutputCollector(spec.stdoutMaxBytes, spec.maxSpillBytes, 'stdout', spillDir) + const stderr = new OutputCollector(spec.stderrMaxBytes, spec.maxSpillBytes, 'stderr', spillDir) child.stdout.on('data', (chunk: Buffer) => { stdout.push(chunk) }) child.stderr.on('data', (chunk: Buffer) => { stderr.push(chunk) }) @@ -328,12 +363,13 @@ export function runBash(spec: SpawnSpec, internals: RunInternals = {}): RunningB } const done = new Promise<SpawnOutcome>((resolve, reject) => { - child.on('error', (error) => { - // No meaningful close outcome follows a spawn failure. - cleanup() - reject(error) - }) - child.on('close', (exitCode, signal) => { + let settled = false + let pipeDrainTimer: NodeJS.Timeout | undefined + const settle = (exitCode: number | null, signal: NodeJS.Signals | null): void => { + if (settled) return + settled = true + child.stdout.destroy() + child.stderr.destroy() cleanup() resolve({ exitCode, @@ -341,9 +377,20 @@ export function runBash(spec: SpawnSpec, internals: RunInternals = {}): RunningB stdout: stdout.finalize(), stderr: stderr.finalize(), }) + } + child.on('error', (error) => { + // No meaningful close outcome follows a spawn failure. + settled = true + cleanup() + reject(error) }) + child.on('exit', (exitCode, signal) => { + pipeDrainTimer = setTimeout(() => { settle(exitCode, signal) }, spec.graceMs) + }) + child.on('close', settle) function cleanup(): void { if (graceTimer !== undefined) clearTimeout(graceTimer) + if (pipeDrainTimer !== undefined) clearTimeout(pipeDrainTimer) spec.signal?.removeEventListener('abort', onAbort) } }) diff --git a/packages/bash/bash-local/tests/executor.spec.ts b/packages/bash/bash-local/tests/executor.spec.ts index 9db0c2eebd..2e24addb3b 100644 --- a/packages/bash/bash-local/tests/executor.spec.ts +++ b/packages/bash/bash-local/tests/executor.spec.ts @@ -66,6 +66,7 @@ describe('LocalBashExecutor.run', () => { await expect(setup({ timeoutMs: Number.NaN })).rejects.toThrow(/timeoutMs/) await expect(setup({ maxTimeoutMs: 0 })).rejects.toThrow(/maxTimeoutMs/) await expect(setup({ maxOutputBytes: -1 })).rejects.toThrow(/maxOutputBytes/) + await expect(setup({ maxSpillBytes: 0 })).rejects.toThrow(/maxSpillBytes/) await expect(setup({ graceMs: 0 })).rejects.toThrow(/graceMs/) const { bash } = await setup() diff --git a/packages/bash/bash-local/tests/run.spec.ts b/packages/bash/bash-local/tests/run.spec.ts index e65500a4b5..91afd1aede 100644 --- a/packages/bash/bash-local/tests/run.spec.ts +++ b/packages/bash/bash-local/tests/run.spec.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, statSync } from 'node:fs' +import { mkdtempSync, readFileSync, statSync, unlinkSync } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' import { describe, expect, it, vi } from 'vitest' @@ -6,7 +6,10 @@ import type { DshEnvironment } from '@deepseek-ai/dsh-bash' import { killGroup, OutputCollector, runBash } from '../src/run.ts' import type { RunningBash } from '../src/run.ts' -const { failNextClose } = vi.hoisted(() => ({ failNextClose: { value: false } })) +const { failNextClose, failNextUnlink } = vi.hoisted(() => ({ + failNextClose: { value: false }, + failNextUnlink: { value: false }, +})) vi.mock('node:fs', async (importOriginal) => { const actual = await importOriginal<typeof import('node:fs')>() return { @@ -18,6 +21,13 @@ vi.mock('node:fs', async (importOriginal) => { } actual.closeSync(fd) }, + unlinkSync(path: Parameters<typeof actual.unlinkSync>[0]): void { + if (failNextUnlink.value) { + failNextUnlink.value = false + throw Object.assign(new Error('simulated EIO on unlink'), { code: 'EIO' }) + } + actual.unlinkSync(path) + }, } }) @@ -29,6 +39,7 @@ function spec(command: string, overrides: Partial<Parameters<typeof runBash>[0]> cwd: process.cwd(), stdoutMaxBytes: 64_000, stderrMaxBytes: 64_000, + maxSpillBytes: 64 * 1024 * 1024, graceMs: 3_000, ...overrides, } @@ -173,6 +184,22 @@ describe('runBash', () => { const result = await running.done expect(result.signal).toBe('SIGTERM') }) + + it('bounds inherited-pipe draining after the shell exits', async () => { + const pidFile = join(spillDir, `pipe-holder-${Date.now()}.pid`) + const started = Date.now() + const running = runBash(spec(`sleep 60 & echo $! > ${pidFile}; echo shell-done`, { graceMs: 100 })) + const descendant = await waitForPidFile(pidFile) + try { + const result = await running.done + expect(Date.now() - started).toBeLessThan(1_000) + expect(result.exitCode).toBe(0) + expect(result.stdout.text).toBe('shell-done\n') + } finally { + process.kill(descendant, 'SIGKILL') + await waitGone(descendant) + } + }) }) describe('stdin and extra env (set by in-process plugins)', () => { @@ -282,7 +309,7 @@ describe('output truncation and spill', () => { describe('OutputCollector', () => { it('keeps the tail of a single oversized chunk', () => { - const collector = new OutputCollector(10, 'test', spillDir) + const collector = new OutputCollector(10, 100, 'test', spillDir) collector.push(Buffer.from('0123456789abcdef')) const out = collector.finalize() expect(out.text).toBe('6789abcdef') @@ -291,7 +318,7 @@ describe('OutputCollector', () => { }) it('readFrom returns increments and flags lossy reads', () => { - const collector = new OutputCollector(10, 'test', spillDir) + const collector = new OutputCollector(10, 100, 'test', spillDir) collector.push(Buffer.from('aaaaa')) const first = collector.readFrom(0) expect(first.text).toBe('aaaaa') @@ -312,7 +339,7 @@ describe('OutputCollector', () => { }) it('contains close failures and drops the spill path', () => { - const collector = new OutputCollector(4, 'closefail', spillDir) + const collector = new OutputCollector(4, 100, 'closefail', spillDir) collector.push(Buffer.from('aaaa')) collector.push(Buffer.from('bbbb')) expect(collector.readFrom(0).spillPath).toBeDefined() @@ -326,6 +353,46 @@ describe('OutputCollector', () => { expect(out!.truncated).toBe(true) expect(out!.spillPath).toBeUndefined() }) + + it('discards a spill that exceeds its configured cap', () => { + const collector = new OutputCollector(4, 8, 'bounded', spillDir) + collector.push(Buffer.from('aaaa')) + collector.push(Buffer.from('bbbb')) + const spillPath = collector.readFrom(0).spillPath! + expect(readFileSync(spillPath, 'utf8')).toBe('aaaabbbb') + + collector.push(Buffer.from('c')) + collector.push(Buffer.from('dddd')) + const out = collector.finalize() + expect(out.text).toBe('dddd') + expect(out.truncated).toBe(true) + expect(out.spillPath).toBeUndefined() + expect(() => readFileSync(spillPath)).toThrow() + }) + + it('does not create a spill when the first overflowing chunk exceeds the cap', () => { + const collector = new OutputCollector(4, 4, 'no-spill', spillDir) + collector.push(Buffer.from('abcdefgh')) + const out = collector.finalize() + expect(out.text).toBe('efgh') + expect(out.truncated).toBe(true) + expect(out.spillPath).toBeUndefined() + }) + + it('contains cleanup failures while disabling an oversize spill', () => { + const collector = new OutputCollector(4, 8, 'cleanup-fail', spillDir) + collector.push(Buffer.from('aaaa')) + collector.push(Buffer.from('bbbb')) + const spillPath = collector.readFrom(0).spillPath! + + failNextClose.value = true + failNextUnlink.value = true + expect(() => { collector.push(Buffer.from('c')) }).not.toThrow() + expect(failNextClose.value).toBe(false) + expect(failNextUnlink.value).toBe(false) + expect(collector.finalize().spillPath).toBeUndefined() + unlinkSync(spillPath) + }) }) describe('killGroup', () => { diff --git a/packages/bash/bash-sandbox/README.md b/packages/bash/bash-sandbox/README.md index fc6e82bcae..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 RFC § Escalation](../../../docs/rfc/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<Config> = 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<string> { async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-write'): Promise<SandboxBashExecutor> { 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 9a3cbc46f1..ec5005ec70 100644 --- a/packages/bash/bash/README.md +++ b/packages/bash/bash/README.md @@ -29,9 +29,9 @@ 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 RFC](../../../docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [the session environment RFC](../../../docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.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). ## Model Experience @@ -44,4 +44,4 @@ No direct invalidation; the named consumer owns any request-prefix changes. ## Known Limitations and Deferred Work - **No interactive-input vocabulary** — `stdin` is written once at spawn and closed; the seam has no channel to feed a running task and no PTY session concept. -- **Foreground timeouts are always executor-owned** — a caller-owned-deadline mode on the seam is explicitly deferred by [the tool-call timeout-policy RFC](../../../docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md). +- **Foreground timeouts are always executor-owned** — a caller-owned-deadline mode on the seam is explicitly deferred by [the tool-call timeout-policy Agent Note](../../../.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.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/src/types.ts b/packages/bash/bash/src/types.ts index 6e0ddca91e..55beccacea 100644 --- a/packages/bash/bash/src/types.ts +++ b/packages/bash/bash/src/types.ts @@ -131,7 +131,7 @@ export interface BashRunResult { * short. Mutually exclusive with {@link aborted}: one fused deadline drives * both the timeout and the caller's cancellation, so a timeout and an abort * racing before process close report the single first-abort cause, not both - * (see the [timeout-library RFC](../../../../docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md)). + * (see the [timeout-library Agent Note](../../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)). */ timedOut: boolean /** 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/README.md b/packages/bash/tool-bash/README.md index 3eea649643..258967fad9 100644 --- a/packages/bash/tool-bash/README.md +++ b/packages/bash/tool-bash/README.md @@ -57,17 +57,17 @@ The tool owns its `presentCall`/`presentResult` render intent. A foreground call ## The tool builds its request from named args only -The `BashExecRequest` seam carries optional `stdoutMaxBytes`, `stdin`, ordinary `env`, and managed `dshEnv`, used by trusted in-process plugins and this tool's environment registry. The model-facing tool exposes none of `stdoutMaxBytes`, `stdin`, or `env`: it builds requests from named command/workdir/timeout/signal/sandbox fields plus the registry-collected `dshEnv`. Extra model keys are ignored and cannot replace managed values. Shell syntax provides equivalent command-level behavior, while the local executor scrubs ambient credentials and stale `DSH_*` values. See the [stdin/env RFC](../../../docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md). +The `BashExecRequest` seam carries optional `stdoutMaxBytes`, `stdin`, ordinary `env`, and managed `dshEnv`, used by trusted in-process plugins and this tool's environment registry. The model-facing tool exposes none of `stdoutMaxBytes`, `stdin`, or `env`: it builds requests from named command/workdir/timeout/signal/sandbox fields plus the registry-collected `dshEnv`. Extra model keys are ignored and cannot replace managed values. Shell syntax provides equivalent command-level behavior, while the local executor scrubs ambient credentials and stale `DSH_*` values. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md). ## Permissions and escalation Commands run with the executor's full authority unless a sandboxing executor ([`dsh-bash-sandbox`](../bash-sandbox/)) confines them — the deny-only sandbox reports denials as result facts, rendered here as the denial marker; per-call allow/deny/ask policy is the `tools/pre-execute` waterfall (see docs/architecture.md). -Escalating bash calls resolve `ctx.approval` before execution. `allowed-once` applies the requested mode only to that call; rejection, cancellation, unavailability, or missing approval context executes nothing and returns a distinct error. On a real denial, the model may retry the same command once in the same turn with the narrowest sufficient mode and justification; the approval prompt itself is the consent step. Escalation is never speculative, and a disabled or rejected approval is final. The [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md) owns the rationale. +Escalating bash calls resolve `ctx.approval` before execution. `allowed-once` applies the requested mode only to that call; rejection, cancellation, unavailability, or missing approval context executes nothing and returns a distinct error. On a real denial, the model may retry the same command once in the same turn with the narrowest sufficient mode and justification; the approval prompt itself is the consent step. Escalation is never speculative, and a disabled or rejected approval is final. The [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns the rationale. ## Per-session mode switching -For sandboxing executors, each call resolves mode as one-shot escalation, then session override, then executor default. Non-sandboxing and agent-less calls carry no session override. Neither the prompt nor a switch notice announces the standing mode; denial results report the effective mode when the boundary matters. See the [`dsh-bash` fold](../bash/README.md) and [sandbox switching contract](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md). +For sandboxing executors, each call resolves mode as one-shot escalation, then session override, then executor default. Non-sandboxing and agent-less calls carry no session override. Neither the prompt nor a switch notice announces the standing mode; denial results report the effective mode when the boundary matters. See the [`dsh-bash` fold](../bash/README.md) and [sandbox switching contract](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). ## Model Experience @@ -150,5 +150,5 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work - **Replay exit pills parse from result text** — output whose final line happens to be exactly `[exit code: N]` / `[killed by signal: …]` shows a wrong pill on session replay; a display-only known residual. -- **The `bash` tool opts out of `timeout-policy` budgets** — it keeps the executor-owned `BASH_TIMEOUT` path, per [the tool-call timeout-policy RFC](../../../docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md). +- **The `bash` tool opts out of `timeout-policy` budgets** — it keeps the executor-owned `BASH_TIMEOUT` path, per [the tool-call timeout-policy Agent Note](../../../.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md). - **Background processes have no executor timeout** — callers must use `task_kill`, or rely on owner/service disposal, when work no longer matters. 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<string, readonly SandboxMode[]> = { - '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<SandboxMode> => { + /** + * 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<SandboxMode> => { 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 af0cdcaa1c..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<string, unknown> }> = [{ 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, @@ -287,7 +287,7 @@ describe('bash tool', () => { }) // Type and required-key violations are rejected by the harness - // (defineTool validates against the SchemaSpec — the arg-validation RFC) before execute. + // (defineTool validates against the SchemaSpec — the arg-validation Agent Note) before execute. it.each([ [{}, /missing required property "command"/], [{ command: 42, description: 'd' }, /"command" must be a string/], @@ -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') }) }) @@ -946,7 +946,7 @@ describe('the model-facing bash tool builds its request from named args only (no * model input into the post-scrub `env` merge or per-run capture budget — NOT * to defend a trust boundary * (the credential scrub in dsh-bash-local is the security control; see the - * bash-stdin-env RFC). Foreground `run()` returns a canned result; `start()` + * bash-stdin-env Agent Note). Foreground `run()` returns a canned result; `start()` * hands back an already-settled fake handle so the task registration completes. */ class RecordingBashExecutor extends BashExecutor { 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/README.md b/packages/code-runtime/README.md index 2c18ef43cf..27ef599ebe 100644 --- a/packages/code-runtime/README.md +++ b/packages/code-runtime/README.md @@ -1,6 +1,6 @@ # code-runtime/ — code-execution capability family -The code-execution capability seam (see [capability seams](../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)): an abstract runtime interface for executing one model-written program against host-provided async bindings, capturing what it printed and returned. The consumer is the tool registry's [Code Mode](../core/tools/README.md) (`tools: { mode: code }` — the `run_code` tool and the generated TypeScript SDK); design in the [Code Mode RFC](../../docs/rfc/implemented/feature/2026-06-15-code-mode.md). **Product** packages. +The code-execution capability seam (see [capability seams](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract runtime interface for executing one model-written program against host-provided async bindings, capturing what it printed and returned. The consumer is the tool registry's [Code Mode](../core/tools/README.md) (`tools: { mode: code }` — the `run_code` tool and the generated TypeScript SDK); design in the [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md). **Product** packages. | Package | Role | ctx key | |---|---|---| diff --git a/packages/code-runtime/code-runtime-worker/README.md b/packages/code-runtime/code-runtime-worker/README.md index 96d65198e2..2645e8810e 100644 --- a/packages/code-runtime/code-runtime-worker/README.md +++ b/packages/code-runtime/code-runtime-worker/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-code-runtime-worker -Worker-thread implementation of the [`@deepseek-ai/dsh-code-runtime`](../code-runtime/README.md) seam: `WorkerCodeRuntime` runs each program in ONE fresh Node `worker_threads.Worker` — TypeScript in, type-stripped host-side, bindings bridged over the message port, `{ value, logs, error? }` out. **Containment, not a security boundary**: trust posture is bash-equivalent by design (the [Code Mode RFC](../../../docs/rfc/implemented/feature/2026-06-15-code-mode.md) § Trust posture), with containment bash does not have — separate isolate, empty environment, heap cap, hard termination. +Worker-thread implementation of the [`@deepseek-ai/dsh-code-runtime`](../code-runtime/README.md) seam: `WorkerCodeRuntime` runs each program in ONE fresh Node `worker_threads.Worker` — TypeScript in, type-stripped host-side, bindings bridged over the message port, `{ value, logs, error? }` out. **Containment, not a security boundary**: trust posture is bash-equivalent by design (the [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) § Trust posture), with containment bash does not have — separate isolate, empty environment, heap cap, hard termination. ## Config 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/code-runtime/code-runtime/README.md b/packages/code-runtime/code-runtime/README.md index e5b1565fec..b31af71397 100644 --- a/packages/code-runtime/code-runtime/README.md +++ b/packages/code-runtime/code-runtime/README.md @@ -2,7 +2,7 @@ The **code-execution seam**: an abstract `CodeRuntime` service (`ctx.codeRuntime`) defining WHAT a code runtime does — run one model-written program against a set of host-provided async bindings and report `{ value, logs, error? }` — without saying HOW. -This package is the interface third of the capability (the bash trio is the template — see [capability seams](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)): implementations subclass `CodeRuntime` and register the service; the consumer is the tool registry's Code Mode, which generates the model-facing SDK and bridges tool dispatch — both specified in the [Code Mode RFC](../../../docs/rfc/implemented/feature/2026-06-15-code-mode.md), whose first implementation is a Node worker-thread backend. The runtime knows nothing about tools or sessions: it is handed named async functions and a program string, and everything tool-shaped stays with the consumer. +This package is the interface third of the capability (the bash trio is the template — see [capability seams](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): implementations subclass `CodeRuntime` and register the service; the consumer is the tool registry's Code Mode, which generates the model-facing SDK and bridges tool dispatch — both specified in the [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md), whose first implementation is a Node worker-thread backend. The runtime knows nothing about tools or sessions: it is handed named async functions and a program string, and everything tool-shaped stays with the consumer. ## Service API (`ctx.codeRuntime`) @@ -29,5 +29,5 @@ No direct invalidation; the named consumer owns any request-prefix changes. ## Known Limitations and Deferred Work - **`run()` is one-shot** — `logs` arrive only on the resolved `CodeRunResult`; the seam exposes no streaming-log or progress surface for a live program's output. -- **A persistent REPL-style kernel is recorded future work** — the no-state-between-runs contract stands until a persistent-kernel backend brings its own logging story ([Code Mode RFC](../../../docs/rfc/implemented/feature/2026-06-15-code-mode.md)). +- **A persistent REPL-style kernel is recorded future work** — the no-state-between-runs contract stands until a persistent-kernel backend brings its own logging story ([Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)). - **Only the worker-thread backend ships** — `'process'`/`'container'` are declared well-known `isolation` values with no implementation; a hard security boundary awaits a container backend. diff --git a/packages/compact/README.md b/packages/compact/README.md index b5f5987571..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](../../docs/rfc/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 RFC](../../docs/rfc/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 6f9f5f387d..d789bf2328 100644 --- a/packages/compact/compact-basic/README.md +++ b/packages/compact/compact-basic/README.md @@ -2,20 +2,21 @@ The **basic compaction backend**: a `BasicCompactService` implementing the `@deepseek-ai/dsh-compact` seam with reusable `ctx.tokenMeter` pressure, token-budget retention, and summarization as a direct one-shot `ctx.llm.stream()` call (interceptable at `llm/stream`). -This is the implementation tier of the compaction capability — see the [interface package](../compact/README.md) for the seam and the [capability-seam RFC](../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md) for the design. +This is the implementation tier of the compaction capability — see the [interface package](../compact/README.md) for the seam and the [capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) for the design. ## What it owns 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 `<compacted-summary>` 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, `<compacted-summary>`, the data-dependent summary, and `</compacted-summary>`. 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, `<compacted-summary>`, the data-dependent summary, and `</compacted-summary>`. 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. -- **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 RFC](../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md)). +- **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<Context> { const modules = new Map<string, unknown>([ ['@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<Context> { } 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<string> = 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<ToolResultPruneConfig> = 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<typeof context.loader.internal> + 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<string, unknown> = {}, +): 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 f0af3c6e74..6e33b6e570 100644 --- a/packages/compact/compact/README.md +++ b/packages/compact/compact/README.md @@ -10,7 +10,7 @@ This package is the interface tier of the compaction capability, split so each c | `@deepseek-ai/dsh-compact-basic` | a backend: `ctx.tokenMeter` pressure + token-budget retention + `llm.stream()` summarization | | `@deepseek-ai/dsh-tool-compact` (deferred) | the model-facing `/compact` tool over `ctx.compact` | -Unlike the bash seam, this interface depends on `@deepseek-ai/dsh-session` and `@deepseek-ai/dsh-llm` — the contract's verbs are defined over a `Session` and its output is the `ContentBlock` vocabulary, so they cannot be expressed without naming those packages. That deviation from the "interface depends only on cordis" guidance is intentional and recorded in the [compaction capability-seam RFC](../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md). +Unlike the bash seam, this interface depends on `@deepseek-ai/dsh-session` and `@deepseek-ai/dsh-llm` — the contract's verbs are defined over a `Session` and its output is the `ContentBlock` vocabulary, so they cannot be expressed without naming those packages. 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). ## Service API (`ctx.compact`) @@ -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/compact/compact/src/index.ts b/packages/compact/compact/src/index.ts index 7f0844c9f9..f4f666bfef 100644 --- a/packages/compact/compact/src/index.ts +++ b/packages/compact/compact/src/index.ts @@ -3,7 +3,7 @@ * compact and replace a history range with one summary node by subclassing * {@link CompactService}. This interface necessarily depends on session and LLM * vocabulary; the rationale is in the - * [compaction RFC](../../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md). + * [compaction Agent Note](../../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md). * @module @deepseek-ai/dsh-compact */ diff --git a/packages/compact/compact/src/types.ts b/packages/compact/compact/src/types.ts index e831573d5a..e531430525 100644 --- a/packages/compact/compact/src/types.ts +++ b/packages/compact/compact/src/types.ts @@ -3,7 +3,7 @@ * Those declaration-merged events are log-only lock/provenance markers, not * surface events; a separate replacement `user/message` carries the summary. * Backend packages own configuration and retention policy; see - * `docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md`. + * `.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md`. * @module @deepseek-ai/dsh-compact/types */ @@ -30,7 +30,7 @@ declare module '@deepseek-ai/dsh-session' { * The model that wrote the summary — the summarize call's envelope, * reported by the backend that made the call, logged so the one-shot * request is reconstructable from log + code and "which model wrote - * this summary" has a durable answer (the reconstructability RFC). + * this summary" has a durable answer (the reconstructability Agent Note). */ model: string /** The generation cap the summarize call sent, when one applied. */ diff --git a/packages/context/README.md b/packages/context/README.md index 8a947703ec..ebfa8d2d11 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -7,4 +7,4 @@ Product plugins that add model-visible request context without defining a tool o | `time-context/` | Durable per-step current time and elapsed-time context | (none) | | `workspace-context/` | `AGENTS.md`/`CLAUDE.md` workspace context loader | (listens on `agent/session-prefix` + `tools/post-execute`) | -The [`workspace-context` decision record](../../docs/rfc/implemented/feature/2026-06-24-workspace-context.md) explains its per-agent/session isolation and lifecycle split. +The [`workspace-context` decision record](../../.agents/notes/implemented/feature/2026-06-24-workspace-context.md) explains its per-agent/session isolation and lifecycle split. diff --git a/packages/context/time-context/README.md b/packages/context/time-context/README.md index f29f908acb..dc79fb6d23 100644 --- a/packages/context/time-context/README.md +++ b/packages/context/time-context/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-time-context -Opt-in durable context with the current zoned time and elapsed time sampled during model-request preparation. `dsh-agent-spine-demo` and shipped examples do not mount it. Decision record: [the durable time-context RFC](../../../docs/rfc/implemented/feature/2026-07-16-durable-per-step-time-context.md). +Opt-in durable context with the current zoned time and elapsed time sampled during model-request preparation. `dsh-agent-spine-demo` and shipped examples do not mount it. Decision record: [the durable time-context Agent Note](../../../.agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md). ## Config 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: <path>` 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: <path>` followed by `The previously loaded instructions from this file no longer apply.` Literal `</system-reminder>` 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 `<system-reminder>` framing. This is caller-selected with `envelope: 'raw'`; ordinary injected context still receives the canonical `<context source="...">` envelope. +The plugin owns the complete `<system-reminder>` 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 `<system-reminder>` + // 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('<context source="plugin">\ndownstream context\n</context>') + expect(blocksText(agent.session.deriveMessages()[1]?.content)).toContain('downstream context') + expect(blocksText(agent.session.deriveMessages()[1]?.content)).not.toContain('<context source=') } finally { await rm(root, { recursive: true, force: true }) await rm(home, { recursive: true, force: true }) @@ -2807,7 +2803,6 @@ describe('workspace context pending state', () => { 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/README.md b/packages/cordis/README.md index 70c7e41ce0..c7c08bbb24 100644 --- a/packages/cordis/README.md +++ b/packages/cordis/README.md @@ -1,6 +1,6 @@ # packages/cordis — the self-referential runtime toolset -Model-facing tools over the live cordis runtime the agent itself runs inside: inspect the loaded plugins and service surface, mount model-written plugins, and dispose them again. Design home: [the toolset RFC](../../docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). +Model-facing tools over the live cordis runtime the agent itself runs inside: inspect the loaded plugins and service surface, mount model-written plugins, and dispose them again. Design home: [the toolset Agent Note](../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). | Package | Role | ctx key | |---|---|---| diff --git a/packages/cordis/tool-cordis/README.md b/packages/cordis/tool-cordis/README.md index a8dc203342..7819cb8c2c 100644 --- a/packages/cordis/tool-cordis/README.md +++ b/packages/cordis/tool-cordis/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-tool-cordis -The self-referential cordis toolset: three model-facing tools over the live runtime the agent runs inside. Design home — sandbox semantics, mount lifecycle, cross-mount composition, the generated API catalog, standing decisions: [the toolset RFC](../../../docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). +The self-referential cordis toolset: three model-facing tools over the live runtime the agent runs inside. Design home — sandbox semantics, mount lifecycle, cross-mount composition, the generated API catalog, standing decisions: [the toolset Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). ## What it does @@ -12,7 +12,7 @@ Exact model-facing schemas: [the generated tool catalog](../../../docs/tool-cata ## Trust stance -The sandbox isolates globals but is not a security boundary. Node globals are absent or redirect to Cordis services such as `ctx.fs`, `ctx.web`, and `ctx.bash`, and writes to `globalThis` stay local, but host-realm helpers make escape possible. Mounted plugins receive a façade without framework internals, yet its allowed services affect the live runtime. Treat this toolset like bash access; see the [design and trust stance](../../../docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). +The sandbox isolates globals but is not a security boundary. Node globals are absent or redirect to Cordis services such as `ctx.fs`, `ctx.web`, and `ctx.bash`, and writes to `globalThis` stay local, but host-realm helpers make escape possible. Mounted plugins receive a façade without framework internals, yet its allowed services affect the live runtime. Treat this toolset like bash access; see the [design and trust stance](../../../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). ## Config diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 52f8477318..efb29e547d 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<FsWriteOutcome>', - 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<FsWriteOutcome>', + 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<FsEditOutcome>', - 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<FsEditOutcome>', + 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 */', }, ], }, @@ -304,6 +304,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.', @@ -522,6 +527,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.', @@ -652,8 +675,8 @@ export const EVENT_API: readonly EventApiEntry[] = [ name: 'agent/prompt-submit', mode: 'waterfall', signature: '\'agent/prompt-submit\'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise<PromptDecision>): Promise<PromptDecision>', - 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', @@ -750,7 +773,7 @@ export const EVENT_API: readonly EventApiEntry[] = [ name: 'llm/stream', mode: 'waterfall', signature: '\'llm/stream\'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>', - jsDoc: '/**\n * Waterfall around every streaming model call (retry, replay, routing).\n * Bound to the {@link LlmService}; call `next()` to reach the resolved\n * adapter\'s stream, or yield your own chunks to short-circuit.\n * @param options - the full request. A LOOP-built request arrives\n * deep-frozen (mutation throws): its content is a pure function of the\n * session log (the reconstructability RFC), so listeners read it, never\n * rewrite it. A hand-built one-shot (compaction summarize) is the\n * caller\'s own object and stays mutable here.\n * @mode waterfall\n */', + jsDoc: '/**\n * Waterfall around every streaming model call (retry, replay, routing).\n * Bound to the {@link LlmService}; call `next()` to reach the resolved\n * adapter\'s stream, or yield your own chunks to short-circuit.\n * @param options - the full request. A LOOP-built request arrives\n * deep-frozen (mutation throws): its content is a pure function of the\n * session log (the reconstructability Agent Note), so listeners read it, never\n * rewrite it. A hand-built one-shot (compaction summarize) is the\n * caller\'s own object and stays mutable here.\n * @mode waterfall\n */', summary: 'Waterfall around every streaming model call (retry, replay, routing).', }, { @@ -1068,10 +1091,6 @@ 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> | void;\n}', @@ -1170,11 +1189,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', @@ -1220,6 +1239,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}', @@ -1258,7 +1285,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', diff --git a/packages/cordis/tool-cordis/src/index.ts b/packages/cordis/tool-cordis/src/index.ts index fd15d75624..4fc5bd4328 100644 --- a/packages/cordis/tool-cordis/src/index.ts +++ b/packages/cordis/tool-cordis/src/index.ts @@ -26,7 +26,7 @@ export interface Config { /** * Milliseconds the SYNCHRONOUS portion of mount code may run in the vm * before evaluation is aborted (default 5000). An async body escapes this - * bound — see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md for the trust stance. + * bound — see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md for the trust stance. */ vmTimeoutMs?: number } diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index e2c7723185..acb49b1ba5 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -19,7 +19,7 @@ Each agent and its session share one caller-chosen `SessionId`, assumed globally `AgentLoop` also implements the `AgentFactory` seam and registers itself via `ctx.agents.setFactory(this)`, so plugins create/resume agents through `ctx.agents` (the interface): - `ctx.agents.create({ sessionId, meta?, seed?, agentOptions?, setup?, signal? }): Promise<AgentHandle>` — programmatic create under the caller-supplied shared id. It awaits the unpublished setup transaction before returning; `meta` carries cwd/lineage/seed-boundary metadata and `seed` reconstructs a forked child prefix after the session boundary validates and snapshots the durable values. `signal` applies only until this promise settles. The resolved [`AgentHandle`](../agent/README.md) owns exact teardown. -- `ctx.agents.resume({ resumeSessionId, agentOptions?, setup?, signal? }): Promise<AgentHandle>` — load a persisted session via `ctx.sessionPersistence` ([session persistence](../../../docs/rfc/implemented/architecture/2026-06-14-session-persistence.md)), register the agent under that same id, reconstruct its history, then await setup against a fresh unpublished agent scope before rollback-covered publication. Turn numbering and derived history continue from the loaded log. Requires a session-persistence backend (NOT hard-injected — non-persistent demos still work; `resume` rejects with a clear error when persistence is absent). `signal` is creation-only. Returns an `AgentHandle`. +- `ctx.agents.resume({ resumeSessionId, agentOptions?, setup?, signal? }): Promise<AgentHandle>` — load a persisted session via `ctx.sessionPersistence` ([session persistence](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)), register the agent under that same id, reconstruct its history, then await setup against a fresh unpublished agent scope before rollback-covered publication. Turn numbering and derived history continue from the loaded log. Requires a session-persistence backend (NOT hard-injected — non-persistent demos still work; `resume` rejects with a clear error when persistence is absent). `signal` is creation-only. Returns an `AgentHandle`. The config-driven `ctx.agentLoop.create()` path keeps its agent owned by the loop fiber (it discards the handle). For a programmatic agent, the handle holder is the only consumer-facing teardown capability; AgentLoop provider unload is the independent structural teardown edge, not another handle exposed to application code. @@ -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`) @@ -114,7 +116,7 @@ Append-only; each synthetic result follows the reusable request prefix and does ## Known Limitations and Deferred Work -- **Classification is unary** — calls whose safety depends on comparing siblings or resources must remain exclusive ([rationale](../../../docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md)). +- **Classification is unary** — calls whose safety depends on comparing siblings or resources must remain exclusive ([rationale](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)). - **Config labels are fresh by default** — omitting `sessionId` creates a fresh `${id}-session-<uuid>` on every startup; exact resume-or-create behavior requires an explicit stable `sessionId`, while `resumeSessionId` requires existing persisted history. - **Config agents have no per-agent persona field or setup hook** — they use the deployment persona; scoped persona/tool composition is available only through the programmatic `ctx.agents.create()` / `resume()` factory options. - **No built-in turn budget** — the default continuation is `continue` whenever a step had tool calls or steering; bounding a runaway turn requires an `agent/turn-continuation` force-stop plugin. 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/loop.ts b/packages/core/agent-loop/src/loop.ts index 0012b504cd..e8008996a9 100644 --- a/packages/core/agent-loop/src/loop.ts +++ b/packages/core/agent-loop/src/loop.ts @@ -1,7 +1,7 @@ /** * Drives one agent across queued durable turns. Turn failures are contained so * later work can run; the session log, not this driver, owns conversation state. - * See docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md. + * See .agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md. * @module dsh-agent-loop/loop */ @@ -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: <T>(run: (acceptContext: (context: HookContext) => void) => Promise<T>) => Promise<T> } /** - * 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<void> { 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<PromptDecision>({ 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<PromptDecision>({ 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<undefined>() + let disposalDone: Promise<void> | 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<undefined>() + 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<undefined>() + 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<undefined>() + 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<undefined>() + let replacementIdle: Promise<void> | 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<undefined>() + const releaseFirstFlush = Promise.withResolvers<undefined>() + 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 b324c02b6c..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 () => { @@ -235,7 +245,7 @@ describe('disposed vs aborted branching', () => { }) }) -describe('structured tool error propagation (the runtime-validation RFC, part 2)', () => { +describe('structured tool error propagation (the runtime-validation Agent Note, part 2)', () => { it('forwards a tool HarnessError onto the tool/result session event', async () => { const { HarnessError } = await import('@deepseek-ai/dsh-llm') // First model turn calls the tool; second turn (after the tool result is 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: '<system-reminder>extra ctx</system-reminder>' }], 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: '<system-reminder>extra ctx</system-reminder>' }]) 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('<context source=\\"plugin\\">') + expect(flat).not.toContain('<context source=') }) - it('inject() can persist raw structured context without the generic context envelope', async () => { + 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('<context source=') @@ -432,7 +441,6 @@ describe('agent loop', () => { 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<undefined>() + const releaseFirstFlush = Promise.withResolvers<undefined>() + 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<undefined>() + const releaseFirstFlush = Promise.withResolvers<undefined>() + 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 2becadcc40..85efda0e4e 100644 --- a/packages/core/agent-loop/tests/properties.spec.ts +++ b/packages/core/agent-loop/tests/properties.spec.ts @@ -1,6 +1,6 @@ /** * Property-based tests for the agent loop's inbox/turn scheduling (the - * property-testing RFC). Deterministic by construction: schedules are driven + * property-testing Agent Note). Deterministic by construction: schedules are driven * through the `agent/status` settle signal (no wall-clock sleeps), so a flake * is a finding, not timing noise. * @@ -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/request-cache.e2e.ts b/packages/core/agent-loop/tests/request-cache.e2e.ts index 9fc832fd0a..36d88feaa9 100644 --- a/packages/core/agent-loop/tests/request-cache.e2e.ts +++ b/packages/core/agent-loop/tests/request-cache.e2e.ts @@ -14,7 +14,7 @@ import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' * multi-step tool turn (plus a follow-up turn) against the live DeepSeek API must report * `cacheReadTokens > 0` on every request after the first — the adapter maps the provider's * `prompt_cache_hit_tokens`, and the per-step usage recorded on `assistant/message` events is - * the production observable for cache behavior (the reconstructability RFC's measurement + * the production observable for cache behavior (the reconstructability Agent Note's measurement * layer: prefix stability is corollary #1). Mocks establish append-extension; * this key-gated test establishes a real provider cache hit. */ diff --git a/packages/core/agent-loop/tests/resume.spec.ts b/packages/core/agent-loop/tests/resume.spec.ts index 74192dde90..57635bbe5f 100644 --- a/packages/core/agent-loop/tests/resume.spec.ts +++ b/packages/core/agent-loop/tests/resume.spec.ts @@ -75,7 +75,7 @@ function throwUnknown(value: unknown): never { throw value } -describe('the session-persistence RFC: AgentLoop factory create/resume', () => { +describe('the session-persistence Agent Note: AgentLoop factory create/resume', () => { it('normalizes a non-Error resume publication failure for rollback and rethrows it', async () => { const sessionId = SessionId('unknown-resume-failure-s') const root = await persistSession(sessionId) diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 372b594eb7..23a7bfddf0 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -26,7 +26,7 @@ The scoped-registration surface: `Agent.ctx` is the agent's scope context (`dsh- - `ctx.agents.withInitiator(agent, operation)` — run with one exact Agent and preserve the operation's exact synchronous value or Promise. - `ctx.agents.withoutInitiator(operation)` — hide an inherited initiator for unrelated process-local work. -The scope carries the `Agent` itself and is process-local. Ambient presence is neither liveness proof nor authorization; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. Teardown rejects new boundaries, lets injected dependents and returned-Promise boundaries drain, then disables the underlying `AsyncLocalStorage`; unreturned work remains owned by the subsystem that detached it. If a boundary's inherited async chain starts an owning Cordis fiber's unload, that nested boundary chain is released from the drain so the unload cannot wait on itself; its continuations observe the disposed service after teardown. The [initiator-scope decision](../../../docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns the detailed boundary and teardown contract. +The scope carries the `Agent` itself and is process-local. Ambient presence is neither liveness proof nor authorization; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. Teardown rejects new boundaries, lets injected dependents and returned-Promise boundaries drain, then disables the underlying `AsyncLocalStorage`; unreturned work remains owned by the subsystem that detached it. If a boundary's inherited async chain starts an owning Cordis fiber's unload, that nested boundary chain is released from the drain so the unload cannot wait on itself; its continuations observe the disposed service after teardown. The [initiator-scope decision](../../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns the detailed boundary and teardown contract. #### Factory seam (creation) @@ -34,7 +34,7 @@ Agent *creation* is provided by the plugin implementing `AgentFactory` (`dsh-age - `ctx.agents.setFactory(factory: AgentFactory): () => void` — register the creation factory (the loop calls this on construction). Throws on a second factory; the slot clears on dispose. - `ctx.agents.create(options: CreateAgentOptions): Promise<AgentHandle>` — create a session and agent, await optional setup while unpublished, then publish through final `SessionStore.enter()` and `AgentRegistry.enter()` checks. Concurrent same-ID creation is unsupported: more than one operation may prepare, but only one can enter; every loser rolls its private scope/session/driver back. An optional creation-only `signal` cancels unpublished setup and is detached before the handle is returned; later cancellation uses `handle.dispose()` or `agent.cancel()`. Publication is rollback-covered and every delivered creation edge is paired during rollback. Rejects if no factory is registered. -- `ctx.agents.resume(options: ResumeAgentOptions): Promise<AgentHandle>` — load a persisted session ([session persistence](../../../docs/rfc/implemented/architecture/2026-06-14-session-persistence.md)), mint a fresh unpublished agent scope, await optional setup, and use the same final-entry publication sequence. Its optional `signal` is likewise creation-only. Rejects if no factory is registered or session persistence is unconfigured. +- `ctx.agents.resume(options: ResumeAgentOptions): Promise<AgentHandle>` — load a persisted session ([session persistence](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)), mint a fresh unpublished agent scope, await optional setup, and use the same final-entry publication sequence. Its optional `signal` is likewise creation-only. Rejects if no factory is registered or session persistence is unconfigured. `AgentHandle = { agent: Agent; dispose(): Promise<void> }`. The disposer is a **consumer capability** — no observer holding the bare registry entry can tear the agent down. The caller fiber and the registered factory provider are structural co-owners: caller unload enforces structured ownership, while factory unload must stop old instances because their scoped dependency surface belongs to that provider. `dispose()` from any owner reaches one memoized quiescence boundary: it stops the loop, `await`s its exit plus every outstanding idle-injection flush (not just the `disposed` status flip), unregisters the agent, removes its session from the store, and finally unwinds its scoped world. This order captures every agent-started `session/flush` before the session is detached and keeps scoped listeners alive through those checkpoints. `ctx.agents.get(id)` still returns a bare `Agent`; the ACP bridge and in-process subagent backends hold consumer handles, while config-created agents are already owned by the loop fiber. @@ -44,9 +44,9 @@ Agent *creation* is provided by the plugin implementing `AgentFactory` (`dsh-age The lifecycle edges have two important local caveats. `agent/created` runs after scoped setup and after both session and agent registry entries exist. Setup is trusted composition-only code; the immediately following non-vetoing `agent/session-start` notification is the first supported startup injection point. `agent/disposed` always means the exact agent has left the registry. AgentLoop emits it after its driver is quiescent, while ordered teardown may still be detaching the session and unwinding the scope; custom agents registered directly own any stronger driver-ordering contract themselves. -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 RFC](../../../docs/rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#three-execution-boundaries-are-deliberately-one-way). +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 `<context>` 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](../../../docs/rfc/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()`. @@ -103,6 +105,6 @@ Prefix-stable while an agent's scoped registrations are unchanged. Setup or relo - **Ambient identity may outlive liveness** — consumers still check `agent.status`, cancellation, and the owning capability contract before lifecycle-sensitive work. - **Inter-agent channels beyond delegation** — shared state, streaming child output, and background/poll semantics remain outside the current synchronous `ctx.subagents` seam. - **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx)` transaction instead. -- **No public step-only abort** — `cancel()` clears ALL pending work (queued + steering + in-flight); an abort that preserves queued prompts returns only with a named consumer ([stop-surface RFC](../../../docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md)). +- **No public step-only abort** — `cancel()` clears ALL pending work (queued + steering + in-flight); an abort that preserves queued prompts returns only with a named consumer ([stop-surface Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md)). - **`HookContext` carries exactly one `MessageSource`** — contributions from several plugins merged onto one tool call collapse under one source; mixed provenance is unrepresentable. - **`SessionStartSource` reserves `'clear'`/`'compact'` with no emitter yet** — only `'startup'`/`'resume'` occur until the driving subsystems land (`TODO(compaction)`). diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts index 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: Agent, turn: number, step: number, signal: AbortSignal): Promise<void> | 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/scope/README.md b/packages/core/scope/README.md index 36cc059bc4..fd6d10c84c 100644 --- a/packages/core/scope/README.md +++ b/packages/core/scope/README.md @@ -15,7 +15,7 @@ Scoped registration primitive. `createScope(ctx, key)` creates a tagged Cordis c ## Design contract -The registration context determines both visibility and ownership, preventing a registration from being visible in one scope but disposed with another. Scopes route trusted same-process plugins; they are not sandboxes or authority boundaries. See the [agent-scope RFC](../../../docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-explicit-non-goals) for rationale and security non-goals. +The registration context determines both visibility and ownership, preventing a registration from being visible in one scope but disposed with another. Scopes route trusted same-process plugins; they are not sandboxes or authority boundaries. See the [agent-scope Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-explicit-non-goals) for rationale and security non-goals. Handing out a scoped context hands out the minting plugin's service-resolution surface (resolution walks the minting fiber's dependency chain, not the holder's) — mint it from the plugin whose dependencies the scoped registrations need to resolve. diff --git a/packages/core/session/README.md b/packages/core/session/README.md index 07bbc4c078..1b584ac3d9 100644 --- a/packages/core/session/README.md +++ b/packages/core/session/README.md @@ -22,7 +22,7 @@ Use the split lifecycle only when teardown must be ordered with another resource - `enter(session)` performs the collision check, publishes without announcing, and returns an entry-bound idempotent detach. Concurrent same-id preparations are allowed, but only one entry succeeds; a stale detach cannot remove its replacement. - `announce(session)` emits the single creation edge and rejects repeat or reentrant announcements. Detach during that dispatch is deferred and later emits the paired disposal edge; an unannounced entry emits neither lifecycle edge. -`dsh-agent-loop` uses this split so final loop flush precedes session detach; see the [ownership RFC](../../../docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md). +`dsh-agent-loop` uses this split so final loop flush precedes session detach; see the [ownership Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md). ### Live service events @@ -32,7 +32,7 @@ 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. @@ -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 RFC](../../../docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md). +`request/header` records a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, or `change`. `foldRequestHeader()` selects the latest snapshot; legacy delta events and the removed `fallback` reason are rejected. `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`) @@ -79,7 +79,7 @@ Every `SessionEvent` carries two optional top-level fields (structural metadata) - 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 `<context source="<source-kind>">`, its content blocks, and `</context>`; `steering/message` uses the identical `<steering source="<source-kind>">` / `</steering>` 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 @@ -128,6 +128,6 @@ Logging causes no invalidation, and exact reconstruction preserves request-prefi ## Known Limitations and Deferred Work - **Session branching/tree** (pi-style entry tree) — deferred unless needed beyond boundary-based `fork()`. -- **`fork()` cuts only at closed-turn boundaries of live sessions** — the boundary must be a `turn/end` event and the source must be in the store; forking a persisted-but-unloaded session is excluded from the [fork API](../../../docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.md). +- **`fork()` cuts only at closed-turn boundaries of live sessions** — the boundary must be a `turn/end` event and the source must be in the store; forking a persisted-but-unloaded session is excluded from the [fork API](../../../.agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md). - **`SESSION_FORMAT_VERSION` stays pinned at `0`** — pre-release, no compatibility implied: a backend rejects any other version, and no migration path exists until the first release ([policy](../../../AGENTS.md)). - **`TurnEndReasonMap` omits the ACP-named `refusal` / `max_turn_requests` variants** — producer-gated: they land when an adapter or the loop first emits them. diff --git a/packages/core/session/src/index.ts b/packages/core/session/src/index.ts index b8b76ec2ed..99024999e9 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 = `</${tag}>` - 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 @@ -228,22 +213,6 @@ interface SessionEntry { /** Store attachment for the append path; module-private to keep Session store-agnostic publicly. */ const attachments = new WeakMap<Session, SessionEntry>() -/** - * 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. * @@ -496,7 +465,7 @@ export class Session { * The per-node pure function {@link deriveMessages} folds over the surface; * an external reconstructor (or the dev invariant) folds the same function * over a log prefix's surface to rebuild the exact messages any request was - * built from (the reconstructability RFC). The returned message wrapper is + * built from (the reconstructability Agent Note). The returned message wrapper is * fresh; its content reuses the logged event's already deep-frozen durable * data, so changing the wrapper cannot rewrite the log and changing content * throws. @@ -509,7 +478,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. `<context>`/`<steering>`) here: framing is + // caller-owned — a producer bakes it into `content`, as workspace-context + // does with `<system-reminder>` — 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 +506,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. 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<string, unknown> + const replacementRest = { ...event.data } as Record<string, unknown> + 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..7188c58b97 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'> @@ -109,8 +106,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 +176,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/properties.spec.ts b/packages/core/session/tests/properties.spec.ts index 9cb744c312..911d8ab7ab 100644 --- a/packages/core/session/tests/properties.spec.ts +++ b/packages/core/session/tests/properties.spec.ts @@ -1,5 +1,5 @@ /** - * Property-based tests for the Session event log (the property-testing RFC). + * Property-based tests for the Session event log (the property-testing Agent Note). * * Generates arbitrary event logs and asserts the derivation invariants the * agent loop and replay depend on: deriveMessages is deterministic and diff --git a/packages/core/session/tests/session.spec.ts b/packages/core/session/tests/session.spec.ts index 3b15157bde..588c8048f4 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: '<context source="plugin">' }) - expect(contextMessage!.content.at(-1)).toMatchObject({ type: 'text', text: '</context>' }) - expect(steeringMessage!.content[0]).toMatchObject({ type: 'text', text: '<steering source="user">' }) + 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: '<system-reminder>Additional instructions from: pkg/AGENTS.md</system-reminder>' }], source: { kind: 'plugin', plugin: 'workspace-context' }, - envelope: 'raw', meta, }, { surfaceOp: 'append' }) 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: '<context source="plugin">' }) - expect(messages[1]!.content[0]).toMatchObject({ type: 'text', text: '<steering source="user">' }) + expect(messages[0]!.content).toEqual([{ type: 'text', text: 'file changed' }]) + expect(messages[1]!.content).toEqual([{ type: 'text', text: 'focus' }]) }) }) diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index 91b9eaf164..5c5c554351 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -7,7 +7,7 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem | Key | Default | Meaning | |---|---|---| | `persona` | `''` | The global deployment-persona default: the ONE config-authored prompt fragment, rendered as the order-0 `deployment:persona` section unless an agent-scoped contribution shadows it. A template — complete `{{…}}` groups are interpreted strictly against the registered variables (the shipped loop registers `{{model}}`/`{{cwd}}`), with no escape syntax for literal braces yet. Empty ⇒ the section is dropped at render. | -| `toolOrder` | — | Explicit model-facing tool order, as a list of `ToolSchema.name`s with one `'<unlisted-tools>'` rest entry (`TOOL_ORDER_REST`): listed tools take their listed position, unlisted tools land at the rest entry in lexicographic name order. Absent ⇒ plain lexicographic name order. Applied to the collected tools BEFORE the `system-prompt/assemble` waterfall — like the sections' `order` sort, it canonicalizes what the registry contributed (registration order is a plugin-load artifact), and a waterfall listener that mutates the list owns the determinism of what it emits. Misconfiguration fails loud: a list without exactly one rest entry, or with duplicates, throws at load; a listed name with no registered tool rejects every `assemble()`; a tool provider returning the reserved rest-entry name also rejects. Under the shipped loop the turn fails before any model request. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.md). | +| `toolOrder` | — | Explicit model-facing tool order, as a list of `ToolSchema.name`s with one `'<unlisted-tools>'` rest entry (`TOOL_ORDER_REST`): listed tools take their listed position, unlisted tools land at the rest entry in lexicographic name order. Absent ⇒ plain lexicographic name order. Applied to the collected tools BEFORE the `system-prompt/assemble` waterfall — like the sections' `order` sort, it canonicalizes what the registry contributed (registration order is a plugin-load artifact), and a waterfall listener that mutates the list owns the determinism of what it emits. Misconfiguration fails loud: a list without exactly one rest entry, or with duplicates, throws at load; a listed name with no registered tool rejects every `assemble()`; a tool provider returning the reserved rest-entry name also rejects. Under the shipped loop the turn fails before any model request. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md). | ## Service: `SystemPrompt` (ctx key: `systemPrompt`) @@ -38,7 +38,7 @@ Merge-extensible: plugins can declare extra fields on `PromptAssembly` and `Asse - Tool schema providers: `ToolRegistry` registers itself as a tool provider automatically. - The [`system-prompt/assemble` waterfall](#live-events): cooperatively mutate or replace the assembly per caller. -Design rationale: [the prompt-variables RFC](../../../docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md). +Design rationale: [the prompt-variables Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md). ## Model Experience diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 1acb633a59..2cb841d0e2 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -16,9 +16,9 @@ tools: ### Public API - `ctx.tools.register(definition: ToolDefinition): () => void` Register a trusted typed same-process definition. The layer is the calling context's scope: a plain plugin context registers globally; an agent's `agent.ctx` registers for that agent alone, shadowing a same-named global tool there. Duplicate names within one layer throw; non-native modes also reject the reserved `run_code` transport name. `timeoutMs`, when present, must be positive and finite. Disposed with the calling fiber. -- `ctx.tools.restrict(filter)` applies an agent-scoped allow/deny mask to global tools and throws from a plain context. The filter is snapshotted at registration; multiple masks intersect and scope-local tools merge afterwards. Deny masks admit later unnamed globals, while allow masks exclude later names. Unknown, local, or reserved names and empty filters reject. This is live visibility composition, not an authority boundary; see the [scope security non-goal](../../../docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-explicit-non-goals). +- `ctx.tools.restrict(filter)` applies an agent-scoped allow/deny mask to global tools and throws from a plain context. The filter is snapshotted at registration; multiple masks intersect and scope-local tools merge afterwards. Deny masks admit later unnamed globals, while allow masks exclude later names. Unknown, local, or reserved names and empty filters reject. This is live visibility composition, not an authority boundary; see the [scope security non-goal](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-explicit-non-goals). - `ctx.tools.get(name: string, scope?: ScopeKey): ToolDefinition | undefined` Resolution as one scope sees it (shadowing applied; a restricted-away global reads as absent) — presenters pass the calling agent so the card matches what executed. -- `ctx.tools.schemas(scope?: ScopeKey): ToolSchema[]` Schemas of everything the scope can see (without the `execute` functions). The shipped tools' schemas are catalogued in [docs/tool-catalog.md](../../../docs/tool-catalog.md), generated by booting each tool plugin and harvesting this method (see [the tool-schema-catalog RFC](../../../docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md)). +- `ctx.tools.schemas(scope?: ScopeKey): ToolSchema[]` Schemas of everything the scope can see (without the `execute` functions). The shipped tools' schemas are catalogued in [docs/tool-catalog.md](../../../docs/tool-catalog.md), generated by booting each tool plugin and harvesting this method (see [the tool-schema-catalog Agent Note](../../../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md)). - `ctx.tools.guard(guard: ToolGuard): () => void` Register a monotonic synchronous execution guard after `tools/pre-execute`: returning a reason denies the call, while `undefined` leaves it unchanged. A plain-context guard applies globally; an `agent.ctx` guard applies only to that agent. Later waterfall listeners cannot turn a guard denial back into permission. Disposed with the calling fiber. - `ctx.tools.execute(exec)` losslessly snapshots and freezes arguments, assigns an opaque token, runs the complete policy/dispatch/result pipeline, then independently snapshots the authoritative outcome before final observation. Invalid arguments use the same result path without reaching policy or the body; around wrappers may replace only `signal`. - `ctx.tools.executionMode(exec)` returns `parallel` only when the visible definition's `isConcurrencySafe(exec.arguments)` classifier returns exactly `true`; unknown, hidden, undeclared, invalid, or throwing classifications are exclusive. @@ -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. @@ -88,7 +88,7 @@ See `defineTool`, `validateArgs`, `ToolArgsError`, `SchemaSpec`, `InferArgs`, an Optional `timeoutMs` must be positive and finite; it is policy metadata, not model-visible schema. -Optional `isConcurrencySafe(args)` receives typed, softly validated arguments. Exact `true` permits concurrent dispatch/body execution; invalid input and all other outcomes remain exclusive. Opted-in bodies do not mutate parent-owned state, and shared-state races must commute or fail closed. The [parallel tool-call RFC](../../../docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md) owns the full safety contract. +Optional `isConcurrencySafe(args)` receives typed, softly validated arguments. Exact `true` permits concurrent dispatch/body execution; invalid input and all other outcomes remain exclusive. Opted-in bodies do not mutate parent-owned state, and shared-state races must commute or fail closed. The [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md) owns the full safety contract. ### Structured-output schema subset @@ -101,19 +101,19 @@ Tools optionally own pure `presentCall()` and `presentResult()` render intents, - Call views are `{ card: 'generic', title, kind?, rawInput?, content?, locations? }`, `{ card: 'terminal', title, description?, cwd? }`, or `{ card: 'diff', title, diffs, locations? }`. - Result views are `{ card: 'generic', title?, content? }`, `{ card: 'terminal', title?, output?, exitCode?, signal? }`, or `{ card: 'diff', title?, diffs }`. -Returning `undefined` selects generic fallback. Presenters depend only on their arguments because UIs call them during live streaming and log replay. Result presentation may read JSON-serializable `result.meta`, which persists with the result; `defineTool` soft-validates older logged arguments and falls back instead of crashing replay. `dsh-tool-bash` and `dsh-tool-fs` are the reference implementations; the [render-intent RFC](../../../docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md) owns the rationale. +Returning `undefined` selects generic fallback. Presenters depend only on their arguments because UIs call them during live streaming and log replay. Result presentation may read JSON-serializable `result.meta`, which persists with the result; `defineTool` soft-validates older logged arguments and falls back instead of crashing replay. `dsh-tool-bash` and `dsh-tool-fs` are the reference implementations; the [render-intent Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) owns the rationale. ### Code Mode -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 RFC](../../../docs/rfc/implemented/feature/2026-06-15-code-mode.md) and [code-runtime seam](../../code-runtime/README.md). Try `pnpm run demo:code-mode`. +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 `<parent>:code:<n>`; `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 `<parent>:code:<n>`; `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 -The agent loop groups consecutive `parallel` calls into a bounded rolling pool and treats each `exclusive` call as an ordering barrier. Only dispatch/body overlaps; policy, durable results, and context retain model order. Code Mode bindings remain serial. The [parallel tool-call RFC](../../../docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md) owns the shipped declarations and rationale. +The agent loop groups consecutive `parallel` calls into a bounded rolling pool and treats each `exclusive` call as an ordering barrier. Only dispatch/body overlaps; policy, durable results, and context retain model order. Code Mode bindings remain serial. The [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md) owns the shipped declarations and rationale. ## Model Experience @@ -177,9 +177,9 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work - **Concurrency policy is not an event seam** — `executionMode()` reads the resolved tool definition directly; plugins can only declare a classifier on definitions they own. -- **`tools/pre-execute` deliberately cannot rewrite `exec.arguments`** — logged and rendered args would desync from what ran; the rewrite design is [a proposed RFC](../../../docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md). +- **`tools/pre-execute` deliberately cannot rewrite `exec.arguments`** — logged and rendered args would desync from what ran; the rewrite design is [a proposed Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md). - **`defineTool`'s schema DSL is a deliberate subset** — string/number/boolean/object/array with string-only `enum`; `validateArgs` tolerates extra keys and preserves `default` as a model-visible JSON Schema annotation without applying it during validation; dynamic Cordis mounts may supply defaults even though first-party definitions do not, while raw-registered JSON-Schema tools validate their own input. - **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-timeout-policy` wrapper. - **Code Mode is TypeScript-only and the presentation mode is service-wide** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language === 'typescript'`; scoped restrictions/shadows still choose each agent's visible bindings, but one tool cannot be native-only while another is code-only. - **Code Mode bindings return text only** — non-text content blocks in a sub-call result collapse to `[<type> content]` placeholders. -- **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode RFC](../../../docs/rfc/implemented/feature/2026-06-15-code-mode.md). +- **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md). diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 5fc045ace2..18fa286a16 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -139,7 +139,7 @@ export interface ToolDefinition extends ToolSchema { * Opted-in executions must not mutate parent-owned state. Shared state must * tolerate concurrent dispatch; recorder races are permitted only when they * commute or fail closed. See the - * [parallel-tool-call RFC](../../../../docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md) + * [parallel-tool-call Agent Note](../../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md) * for the full contract. * @param args - parsed arguments; `defineTool` validates before calling. * @returns Whether this call may join a parallel group. @@ -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/src/ts-types.ts b/packages/core/tools/src/ts-types.ts index bd8c08ed62..e5f67d0891 100644 --- a/packages/core/tools/src/ts-types.ts +++ b/packages/core/tools/src/ts-types.ts @@ -75,7 +75,7 @@ export function jsonSchemaToTs(schema: unknown, indent = 0): string { } } -/** The fixed model-facing usage contract rendered above the declarations (see the Code Mode RFC's "What the model sees"). */ +/** The fixed model-facing usage contract rendered above the declarations (see the Code Mode Agent Note's "What the model sees"). */ const SDK_INSTRUCTIONS = `## Writing code for run_code Pass \`run_code\` the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program: diff --git a/packages/core/tools/tests/code-mode.spec.ts b/packages/core/tools/tests/code-mode.spec.ts index bbc19d5d75..227ff98129 100644 --- a/packages/core/tools/tests/code-mode.spec.ts +++ b/packages/core/tools/tests/code-mode.spec.ts @@ -13,7 +13,7 @@ import { Session, SessionId } from '@deepseek-ai/dsh-session' import type { SessionEventMap } from '@deepseek-ai/dsh-session' /** - * Code Mode unit tier (per the RFC's plan): provider contribution per mode, + * Code Mode unit tier (per the Agent Note's plan): provider contribution per mode, * misconfiguration rejections, the run_code dispatch bridge (serialization, * abort, JSON normalization, error mapping, events, quiescence), and HMR * safety — all against an in-repo fake runtime, exactly the @@ -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/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index 3097f1abeb..5aa4b6eb61 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -49,6 +49,19 @@ describe('gen-tool-catalog collectToolCatalog', () => { expect(bash?.source).toBe('packages/bash/tool-bash/src/index.ts') }) + it('harvests search tools without depending on the generator process PATH', async () => { + const oldPath = process.env.PATH + try { + process.env.PATH = '' + const catalog = await collectToolCatalog() + const search = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-fs-search') + expect(search?.schemas.map(s => s.name).sort()).toEqual(['glob', 'grep']) + } finally { + if (oldPath === undefined) delete process.env.PATH + else process.env.PATH = oldPath + } + }) + it('records the shipped `subagent_fork` alias in a note (config-driven tool name)', async () => { // `tool-subagent`'s registered name is the load-time `toolName` config, so the shipped // agents surface this one package as both `subagent` and `subagent_fork`. diff --git a/packages/core/tools/tests/properties.spec.ts b/packages/core/tools/tests/properties.spec.ts index f5fce4d382..51b49fccd4 100644 --- a/packages/core/tools/tests/properties.spec.ts +++ b/packages/core/tools/tests/properties.spec.ts @@ -1,8 +1,8 @@ /** - * Property-based tests for the tool-schema DSL (the property-testing RFC), including + * Property-based tests for the tool-schema DSL (the property-testing Agent Note), including * the the property-testing ↔ runtime-validation composition composition: generated args that satisfy a SchemaSpec must * pass validateArgs, and targeted corruptions must be rejected. This closes the - * validator/InferArgs drift risk noted in the arg-validation RFC. + * validator/InferArgs drift risk noted in the arg-validation Agent Note. */ import { describe, expect, it } from 'vitest' 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('<unprintable thrown value>') + expect(warn).toHaveBeenCalledTimes(2) + expect(warn.mock.calls.map(call => String(call[0]))).toEqual(expect.arrayContaining([ + expect.stringContaining('<unprintable thrown value>'), + expect.stringContaining('async observer failure'), + ])) }) }) diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts index 3de73c3326..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 () => { @@ -1174,7 +1173,7 @@ describe('ToolRegistry.get', () => { }) }) -describe('validateArgs (the runtime-validation RFC, part 1)', () => { +describe('validateArgs (the runtime-validation Agent Note, part 1)', () => { it('returns [] for valid args and is total over malformed input', () => { const spec = { path: { type: 'string', required: true }, @@ -1274,7 +1273,7 @@ describe('validateArgs (the runtime-validation RFC, part 1)', () => { }) }) -describe('defineTool validation (the runtime-validation RFC, part 1)', () => { +describe('defineTool validation (the runtime-validation Agent Note, part 1)', () => { it('returns an isError result with the violations when the model sends bad args', async () => { const ctx = await setup() ctx.tools.register(defineTool({ diff --git a/packages/examples/acp-demo/README.md b/packages/examples/acp-demo/README.md index 9ef18739c8..e70881af8c 100644 --- a/packages/examples/acp-demo/README.md +++ b/packages/examples/acp-demo/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-acp-demo -The **ACP server app**: a Cordis app plugin that composes the default agent spine ([`@deepseek-ai/dsh-agent-spine-demo`](../../examples/agent-spine-demo/README.md)) with the front-door cluster an [Agent Client Protocol](../../ui/acp/README.md) server needs, and a `bin` that boots a leaf `cordis.yml` speaking ACP JSON-RPC on stdio. +The **ACP server app**: a Cordis app plugin that composes the default agent spine ([`@deepseek-ai/dsh-agent-spine-demo`](../agent-spine-demo/README.md)) with the front-door cluster an [Agent Client Protocol](../../ui/acp/README.md) server needs, and a `bin` that boots a leaf `cordis.yml` speaking ACP JSON-RPC on stdio. It is the structured counterpart to [`@deepseek-ai/dsh-stdio-demo`](../stdio-demo/README.md): both consume the same spine, but this one bakes in the OPPOSITE front-door cluster. @@ -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 `'<unlisted-tools>'` 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/acp-demo/src/index.ts b/packages/examples/acp-demo/src/index.ts index 6785e5f957..d9baa96394 100644 --- a/packages/examples/acp-demo/src/index.ts +++ b/packages/examples/acp-demo/src/index.ts @@ -53,7 +53,7 @@ export interface Config { skills?: agentCore.SkillConfig /** Model-facing bash tool config forwarded through agent-core. */ toolBash?: NonNullable<agentCore.Config['toolBash']> - /** Generic background-task control-tool config forwarded through agent-core. */ + /** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */ toolTasks?: NonNullable<agentCore.Config['toolTasks']> } @@ -75,7 +75,7 @@ export const Config: z<Config> = z.object({ workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(), skills: agentCore.SkillConfigSchema, toolBash: agentCore.ToolBashConfigSchema, - toolTasks: agentCore.ToolTasksConfigSchema, + toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]), }) /* jscpd:ignore-end */ diff --git a/packages/examples/agent-spine-demo/README.md b/packages/examples/agent-spine-demo/README.md index f7a0f28cea..9c431d5698 100644 --- a/packages/examples/agent-spine-demo/README.md +++ b/packages/examples/agent-spine-demo/README.md @@ -34,9 +34,9 @@ The spine is everything COMMON to every front door. The swappable and front-door - **the LLM adapter** — the bundle ships the abstract `llm` service; the leaf registers a concrete adapter on `ctx.llm` (`llm-deepseek`, `llm-pi-ai`, `llm-replay`). - **the bash executor** — the bundle ships `tool-bash` (the consumer schema); the leaf provides `ctx.bash` (`bash-local` or a sandboxed impl). - **non-local skill providers** — the bundle ships the skill registry, the local filesystem provider, and the `skill` tool; deployments can add other providers such as embedded or remote catalogs as siblings. -- **presentation + per-app infra** — the terminal (`dsh-tui` / `dsh-stdio`) or ACP front door and `hmr`. These form the coupled front-door cluster that the app packages ([`dsh-stdio-demo`](../../examples/stdio-demo/README.md), [`dsh-acp-demo`](../../examples/acp-demo/README.md)) bake in. `timer` is in the spine because it is common and stdout-silent; front doors own stdout and remain outside. +- **presentation + per-app infra** — the terminal (`dsh-tui` / `dsh-stdio`) or ACP front door and `hmr`. These form the coupled front-door cluster that the app packages ([`dsh-stdio-demo`](../stdio-demo/README.md), [`dsh-acp-demo`](../acp-demo/README.md)) bake in. `timer` is in the spine because it is common and stdout-silent; front doors own stdout and remain outside. -This is the [interface/implementation/consumer seam](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md) raised to the composition level: the bundle owns the shared spine, the leaf owns the backends, the app package owns the front door. +This is the [interface/implementation/consumer seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md) raised to the composition level: the bundle owns the shared spine, the leaf owns the backends, the app package owns the front door. ## Config @@ -46,7 +46,7 @@ import type { Config } from '@deepseek-ai/dsh-agent-spine-demo' // workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults. ``` -The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — a stdio app pre-creates `main`, while the ACP app creates agents on demand at `session/new`; `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. It resolves `dshHome` once through [`@deepseek-ai/dsh-home`](../../util/home/README.md) and forwards that absolute value to tool-bash's managed environment and local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bash producer, while `toolTasks` controls generic `task_output` wait bounds; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields. +The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — a stdio app pre-creates `main`, while the ACP app creates agents on demand at `session/new`; `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, and set `toolTasks: false` to retain the task service for foreground producers without exposing `task_output` / `task_list` / `task_kill`. It resolves `dshHome` once through [`@deepseek-ai/dsh-home`](../../util/home/README.md) and forwards that absolute value to tool-bash's managed environment and enabled local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bash producer; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields. ## Why a code bundle, not a shared YAML include @@ -62,5 +62,5 @@ No direct invalidation; the named consumer owns any request-prefix changes. ## Known Limitations and Deferred Work -- **The spine set is fixed in code** — `apply()` mounts every child unconditionally (including `tool-bash`); no config excludes or replaces one, so swapping the loop or dropping a spine member means composing a different bundle. +- **Most of the spine set is fixed in code** — `apply()` always mounts the core services and `tool-bash`; config can omit the bundled skills and task-control tools, but swapping the loop or dropping another spine member means composing a different bundle. - **`dsh-invariants` mounts unconditionally** — this bundle has no toggle, so every composition using it pays the dev-mode relational assertions; Session's always-on validation and freezing are separate. diff --git a/packages/examples/agent-spine-demo/src/index.ts b/packages/examples/agent-spine-demo/src/index.ts index a4965ca457..2d489435a5 100644 --- a/packages/examples/agent-spine-demo/src/index.ts +++ b/packages/examples/agent-spine-demo/src/index.ts @@ -31,6 +31,8 @@ export const name = 'agent-spine-demo' /** Skill bundle config forwarded to the registry, local provider, and model-facing consumer. */ export interface SkillConfig { + /** Mount the bundled local skill provider and model-facing skill tool (default true). */ + enabled?: boolean /** Registry-level discovery cache settings. */ registry?: SkillRegistryConfig /** Local filesystem skill provider settings. */ @@ -73,12 +75,13 @@ export interface Config { skills?: SkillConfig /** Model-facing bash tool config, including this producer's background opt-in. */ toolBash?: toolBash.Config - /** Generic background-task control-tool wait bounds. */ - toolTasks?: toolTasks.Config + /** Generic background-task controls; set false to keep the task service without model-facing task tools. */ + toolTasks?: toolTasks.Config | false } /** The skill config schema exported for app packages that forward `skills`. */ export const SkillConfigSchema: z<SkillConfig> = z.object({ + enabled: z.boolean().default(true), registry: SkillService.Config, local: SkillLocal.Config, tool: toolSkill.Config, @@ -100,7 +103,7 @@ export const Config = z.intersect([ skills: SkillConfigSchema, workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(), toolBash: ToolBashConfigSchema, - toolTasks: ToolTasksConfigSchema, + toolTasks: z.union([z.const(false), ToolTasksConfigSchema]), }) as unknown as z<Pick<Config, 'tools' | 'dshHome' | 'skills' | 'workspaceContext' | 'toolBash' | 'toolTasks'>>, ]) as unknown as z<Config> @@ -137,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) @@ -150,8 +153,11 @@ export function apply(ctx: Context, config: Config): void { ...config.toolOrder !== undefined ? { toolOrder: config.toolOrder } : {}, }) ctx.plugin(ToolRegistry, config.tools ?? {}) - ctx.plugin(SkillService, config.skills?.registry ?? {}) - ctx.plugin(SkillLocal, Object.assign({}, config.skills?.local, { dshHome })) + const skillsEnabled = config.skills?.enabled ?? true + if (skillsEnabled) { + ctx.plugin(SkillService, config.skills?.registry ?? {}) + ctx.plugin(SkillLocal, Object.assign({}, config.skills?.local, { dshHome })) + } ctx.plugin(AgentRegistry) ctx.plugin(TaskService) ctx.plugin(invariants) @@ -161,8 +167,8 @@ export function apply(ctx: Context, config: Config): void { } // Both plugins prepend session-prefix messages. Registration order is the // rendered order, so workspace instructions must precede the skill catalog. - ctx.plugin(toolSkill, config.skills?.tool ?? {}) - ctx.plugin(toolTasks, config.toolTasks ?? {}) + if (skillsEnabled) ctx.plugin(toolSkill, config.skills?.tool ?? {}) + if (config.toolTasks !== false) ctx.plugin(toolTasks, config.toolTasks ?? {}) ctx.plugin(AgentLoop, { agents: config.agents ?? [], ...config.maxParallelToolCalls !== undefined ? { maxParallelToolCalls: config.maxParallelToolCalls } : {}, 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 5d5b336ff9..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 () => { @@ -344,6 +344,21 @@ describe('dsh-agent-spine-demo bundle', () => { await ctx.fiber.dispose() }) + it('can omit skills and model-facing task controls for a foreground-only deployment', async () => { + const ctx = await mount({ + workspaceContext: false, + skills: { enabled: false }, + toolBash: { enableRunInBackground: false }, + toolTasks: false, + }, true) + + expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['bash']) + expect(ctx.get('skills')).toBeUndefined() + expect(ctx.get('tasks')).toBeDefined() + + await ctx.fiber.dispose() + }) + it('picks shared spine config without leaking front-door fields', () => { const appConfig = { model: 'front-door-only', @@ -352,9 +367,9 @@ describe('dsh-agent-spine-demo bundle', () => { tools: { mode: 'native' as const }, dshHome: '/tmp/dsh-home', workspaceContext: false as const, - skills: {}, + skills: { enabled: false }, toolBash: { enableRunInBackground: false }, - toolTasks: { waitTimeoutMs: 7, maxWaitTimeoutMs: 11 }, + toolTasks: false as const, } expect(agentCore.pickSpineConfig(appConfig)).toEqual({ @@ -363,7 +378,7 @@ describe('dsh-agent-spine-demo bundle', () => { tools: appConfig.tools, dshHome: appConfig.dshHome, workspaceContext: false, - skills: {}, + skills: appConfig.skills, toolBash: appConfig.toolBash, toolTasks: appConfig.toolTasks, }) diff --git a/packages/examples/cli-demo/src/index.ts b/packages/examples/cli-demo/src/index.ts index 1308209681..e5c77af9ed 100644 --- a/packages/examples/cli-demo/src/index.ts +++ b/packages/examples/cli-demo/src/index.ts @@ -61,7 +61,7 @@ export const Config: z<Config> = z.object({ toolOrder: z.array(z.string()).default(undefined as unknown as string[]), tools: ToolRegistry.Config, toolBash: agentCore.ToolBashConfigSchema, - toolTasks: agentCore.ToolTasksConfigSchema, + toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]), workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(), }) /* jscpd:ignore-end */ diff --git a/packages/examples/cli-demo/tests/cli-demo.spec.ts b/packages/examples/cli-demo/tests/cli-demo.spec.ts index 343d6184d7..2111ff6aa8 100644 --- a/packages/examples/cli-demo/tests/cli-demo.spec.ts +++ b/packages/examples/cli-demo/tests/cli-demo.spec.ts @@ -146,6 +146,21 @@ describe('dsh-cli-demo app composition', () => { expect(wait).toHaveBeenCalledWith(id, 7, undefined, undefined) }) + it('accepts false to keep task services without model-facing task controls', async () => { + const ctx = await mount({ + provider: 'mock', + model: 'mock', + skills: { enabled: false }, + toolTasks: false, + workspaceContext: false, + }) + + expect(ctx.get('tasks')).toBeDefined() + expect(ctx.get('tools')?.get('task_output')).toBeUndefined() + expect(ctx.get('tools')?.get('task_list')).toBeUndefined() + expect(ctx.get('tools')?.get('task_kill')).toBeUndefined() + }) + it('exposes the Loader-safe namespace plugin shape and schema', () => { expect(cliDemo.name).toBe('cli-demo') expect(cliDemo.Config).toBeDefined() diff --git a/packages/examples/jsonrpc-demo/README.md b/packages/examples/jsonrpc-demo/README.md index 364b24550c..083d9a83ce 100644 --- a/packages/examples/jsonrpc-demo/README.md +++ b/packages/examples/jsonrpc-demo/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-jsonrpc-demo -Bin-only app that boots an external `cordis.yml`; its [`jsonrpc`](../../ui/jsonrpc/README.md) entry serves SDK clients over newline-delimited stdio. The config composes the spine, backends, and serving plugin. `lib/bin.js` is also the [single-executable runtime](../../../docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) entry. +Bin-only app that boots an external `cordis.yml`; its [`jsonrpc`](../../ui/jsonrpc/README.md) entry serves SDK clients over newline-delimited stdio. The config composes the spine, backends, and serving plugin. `lib/bin.js` is also the [single-executable runtime](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) entry. ## Config discovery @@ -10,7 +10,7 @@ A config without `dsh-jsonrpc` is valid and serves nothing; the bin does not des ## Exit lifecycle -stdin EOF and `SIGTERM` dispose the root to quiescence and exit 0; `SIGINT` exits 130 after the same disposal. EOF may cut off an in-flight turn as documented in the [distribution RFC](../../../docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md). The `jsonrpc` plugin owns response-before-exit protocol shutdown; both paths are idempotent and safe to race. +stdin EOF and `SIGTERM` dispose the root to quiescence and exit 0; `SIGINT` exits 130 after the same disposal. EOF may cut off an in-flight turn as documented in the [distribution Agent Note](../../../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md). The `jsonrpc` plugin owns response-before-exit protocol shutdown; both paths are idempotent and safe to race. ## stdout is the protocol diff --git a/packages/examples/stdio-demo/README.md b/packages/examples/stdio-demo/README.md index 4eafc9e251..2d706e3008 100644 --- a/packages/examples/stdio-demo/README.md +++ b/packages/examples/stdio-demo/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-stdio-demo -The **terminal chat app**: a Cordis app plugin that composes the default agent spine ([`@deepseek-ai/dsh-agent-spine-demo`](../../examples/agent-spine-demo/README.md)) with JSONL persistence, human interaction, a pre-created `main` agent, and a TTY-selected pi-tui/readline front door. Its `bin` boots a leaf `cordis.yml`. +The **terminal chat app**: a Cordis app plugin that composes the default agent spine ([`@deepseek-ai/dsh-agent-spine-demo`](../agent-spine-demo/README.md)) with JSONL persistence, human interaction, a pre-created `main` agent, and a TTY-selected pi-tui/readline front door. Its `bin` boots a leaf `cordis.yml`. It is the terminal counterpart to [`@deepseek-ai/dsh-acp-demo`](../acp-demo/README.md): both consume the same spine, while ACP reserves stdout for JSON-RPC and creates sessions from the client. diff --git a/packages/examples/stdio-demo/src/index.ts b/packages/examples/stdio-demo/src/index.ts index 531c4a3d5e..0bf66ab007 100644 --- a/packages/examples/stdio-demo/src/index.ts +++ b/packages/examples/stdio-demo/src/index.ts @@ -97,7 +97,7 @@ export interface Config { skills?: agentCore.SkillConfig /** Model-facing bash tool config forwarded through agent-core. */ toolBash?: NonNullable<agentCore.Config['toolBash']> - /** Generic background-task control-tool config forwarded through agent-core. */ + /** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */ toolTasks?: NonNullable<agentCore.Config['toolTasks']> /** * If set, the pre-created agent RESUMES this persisted session id instead of @@ -125,7 +125,7 @@ export const Config: z<Config> = z.object({ ui: UiConfigSchema, skills: agentCore.SkillConfigSchema, toolBash: agentCore.ToolBashConfigSchema, - toolTasks: agentCore.ToolTasksConfigSchema, + toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]), resumeSessionId: z.string(), workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(), }) 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 039cb39ae9..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-search/` | Model-facing `glob`/`grep` discovery tools, backed by fixed ripgrep commands through the bash seam (`ctx.bash`), NOT by `ctx.fs` provider methods | (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 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-local/README.md b/packages/fs/fs-local/README.md index 60cf671a92..47d97266e8 100644 --- a/packages/fs/fs-local/README.md +++ b/packages/fs/fs-local/README.md @@ -12,7 +12,7 @@ await ctx.plugin(LocalFileSystem, { cwd: process.cwd() }) ## Behavior -- **`resolve(path, opts?)`** — a relative `path` resolves against `opts.cwd` when the caller supplies one (the model-facing tools pass the calling agent's session cwd — see [the per-session cwd RFC](../../../docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.md)), else `config.cwd` (default `process.cwd()`); an absolute `path` ignores both. `opts.signal` is checked before and after local resolution, while a remote sibling backend may use it to abort its round-trip. The `targetKey` is the file's `realpath`, so two input paths reaching the same file through symlinks share one identity, and writes/edits land on the link target (preserving the link). A not-yet-existing path uses the realpathed parent directory plus basename when the parent exists; only an unresolvable parent falls back to the absolute path. `displayPath` is the absolute (un-resolved) path. +- **`resolve(path, opts?)`** — a relative `path` resolves against `opts.cwd` when the caller supplies one (the model-facing tools pass the calling agent's session cwd — see [the per-session cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md)), else `config.cwd` (default `process.cwd()`); an absolute `path` ignores both. `opts.signal` is checked before and after local resolution, while a remote sibling backend may use it to abort its round-trip. The `targetKey` is the file's `realpath`, so two input paths reaching the same file through symlinks share one identity, and writes/edits land on the link target (preserving the link). A not-yet-existing path uses the realpathed parent directory plus basename when the parent exists; only an unresolvable parent falls back to the absolute path. `displayPath` is the absolute (un-resolved) path. - **`stat` / `lstat`** — return target metadata or `undefined` when absent. `stat` reports `FsInfo` for an already resolved target (`version` = an opaque token derived from bigint `dev:ino:size:mtimeNs:ctimeNs`, `type` of `file`/`directory`/`other`, byte `size`); path-shaped `lstat` reports `FsPathInfo` without following the final symlink and can therefore return `symlink`. Both check cancellation before and after their asynchronous metadata probe, so an abort that lands in flight reports `FS_ABORTED` rather than stale absence. - **`readText` / `streamText`** — UTF-8 only. `readText` reads the whole file; `streamText` streams it in chunks (cross-chunk decoding) so a huge file never has to be held whole in memory. Both reject invalid UTF-8 and NUL-byte binary samples (`FS_NOT_TEXT`) and non-regular targets. The `read` tool (`@deepseek-ai/dsh-tool-fs`) decides which to call by size and owns the line windowing. - **`listDir`** — lists one directory level in stable `name.localeCompare()` order. Each entry carries the child basename, type, resolved child target (`displayPath` under the listed directory, `targetKey` as the realpath identity), and cheap stat metadata (`version`, plus `size` for regular files). It never opens or decodes file contents. Missing targets report `FS_NOT_FOUND`, file/special-file targets report `FS_NOT_DIRECTORY`, aborted calls report `FS_ABORTED`, permission failures report `FS_PERMISSION_DENIED`, and other listing or child metadata I/O failures report `FS_IO_ERROR`. Broken/disappeared children are returned as `other` without metadata, but permission/IO failures while resolving a child fail the whole listing with a structured `FsError`. @@ -31,7 +31,7 @@ No direct invalidation; the named consumer owns any request-prefix changes. ## Known Limitations and Deferred Work -- **`config.cwd` is not a sandbox** — it is a resolution default, not containment: absolute paths and `..` escape it. Enforce containment with a stricter `ctx.fs` backend or a permission plugin on the `tools/execute` waterfall ([capability-seam RFC](../../../docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md#consequences)). +- **`config.cwd` is not a sandbox** — it is a resolution default, not containment: absolute paths and `..` escape it. Enforce containment with a stricter `ctx.fs` backend or a permission plugin on the `tools/execute` waterfall ([capability-seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md#consequences)). - **An overwrite reads the whole prior file into memory** — solely as the UI diff basis; bounding that pre-read above a size threshold is deferred (`TODO(overwrite-diff-bound)`). - **Version tokens are `mtimeMs:size`** — an external change that preserves both within the filesystem's timestamp granularity defeats the stale guard. - **`editText` holds the whole file (plus the edited copy) in memory** — streaming exists only on the read path. diff --git a/packages/fs/fs-local/src/index.ts b/packages/fs/fs-local/src/index.ts index bd47b53919..18433f3f7c 100644 --- a/packages/fs/fs-local/src/index.ts +++ b/packages/fs/fs-local/src/index.ts @@ -45,7 +45,7 @@ type ResolvedConfig = Required<Config> /** * The host-filesystem backend. Reads resolve relative paths from {@link Config.cwd} * (a resolution default, NOT a containment boundary — see the filesystem - * capability-seam RFC); enforce + * capability-seam Agent Note); enforce * containment with a stricter backend or a `tools/execute` permission plugin. */ export class LocalFileSystem extends FileSystem { diff --git a/packages/fs/fs-policy/README.md b/packages/fs/fs-policy/README.md index f24bd088eb..9d54491a8b 100644 --- a/packages/fs/fs-policy/README.md +++ b/packages/fs/fs-policy/README.md @@ -68,4 +68,4 @@ Append-only; newly visible content follows the reusable request prefix and does - **Observed state does not survive a session resume** — persistence of the `WeakMap` record is deferred, so a resumed session must re-read files before guarded writes/edits. - **Actors without an agent session can never satisfy the policy** — their edits throw `FS_NOT_OBSERVED` and their writes always resolve `createIfAbsent`, so a non-agent caller cannot overwrite an existing file through the gate. - **Direct `ctx.fs` reads emit no `fs/observed`** — a file read outside the `read` tool stays unobserved, and a later guarded edit rejects with `FS_NOT_OBSERVED` until the tool reads it. -- **Authorization is version freshness, not view completeness** — any windowed read authorizes a full-file overwrite of an unchanged file, deliberately weaker than a full-view rule ([seam-split RFC](../../../docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md)). +- **Authorization is version freshness, not view completeness** — any windowed read authorizes a full-file overwrite of an unchanged file, deliberately weaker than a full-view rule ([seam-split Agent Note](../../../.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md)). 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> 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> 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<FsWriteOutcome> { + 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<FsEditOutcome> { + 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<FsTarget> { + 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<ReturnType<Context['plugin']>> + +async function boot(mode: SandboxMode): Promise<void> { + 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<FsTarget> { + 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/README.md b/packages/fs/fs/README.md index 4ea3ec5b9f..9209c237f8 100644 --- a/packages/fs/fs/README.md +++ b/packages/fs/fs/README.md @@ -2,7 +2,7 @@ The **filesystem provider seam**: an abstract `FileSystem` service (`ctx.fs`) defining the storage primitives a backend provides — resolve a path, stat metadata, no-follow path metadata, read/stream text, list directories, write atomically, and apply a literal edit — without saying HOW. Both mutations take their version guard **optionally**, so `ctx.fs` on its own is a complete, unconstrained text-storage seam. This package also owns the `fs/*` policy event vocabulary the tool dispatches and the policy plugin listens for. -This package is the provider-seam layer of the four-layer filesystem stack, split so each concern can evolve (and be swapped) independently (see [the capability-seam RFC](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md), [the filesystem capability-seam RFC](../../../docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.md), [the split-the-filesystem-seam RFC](../../../docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md), and [the file-context event-gate RFC](../../../docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.md)): +This package is the provider-seam layer of the four-layer filesystem stack, split so each concern can evolve (and be swapped) independently (see [the capability-seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md), [the filesystem capability-seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md), [the split-the-filesystem-seam Agent Note](../../../.agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md), and [the file-context event-gate Agent Note](../../../.agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md)): | Layer | Package | Role | |---|---|---| @@ -42,7 +42,7 @@ This package declares three events (see the generated [events catalog](../../../ ## Vocabulary -`FsTargetKey` / `FsVersion` are branded opaque ids ([the branded-ids RFC](../../../docs/rfc/implemented/architecture/2026-06-20-branded-ids.md)) — consumers must not parse `targetKey` or interpret `version`; only `displayPath` is for model/UI output. `FsWriteIntent` is the explicit GUARDED write intent (`createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`; `replaceIfVersion` replaces only at the observed version, else `FS_STALE_VERSION`); omitting it from `writeText` is the third, unconditional state. `FsPathInfo` is the no-follow metadata shape that can report `symlink`, unlike target-level `FsInfo`. Failures throw `FsError` (extends `HarnessError`, [the structured error taxonomy RFC](../../../docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.md)) carrying a stable `FsErrorCode` (`FS_NOT_FOUND`, `FS_NOT_DIRECTORY`, `FS_NOT_TEXT`, `FS_NOT_REGULAR_FILE`, `FS_PERMISSION_DENIED`, `FS_IO_ERROR`, `FS_STALE_VERSION`, `FS_NOT_OBSERVED`, `FS_AMBIGUOUS_EDIT`, `FS_EDIT_NOT_FOUND`, `FS_ABORTED`); the tool registry surfaces `{ name, code }` on `isError` results. See `src/types.ts` for the full contracts. +`FsTargetKey` / `FsVersion` are branded opaque ids ([the branded-ids Agent Note](../../../.agents/notes/implemented/architecture/2026-06-20-branded-ids.md)) — consumers must not parse `targetKey` or interpret `version`; only `displayPath` is for model/UI output. `FsWriteIntent` is the explicit GUARDED write intent (`createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`; `replaceIfVersion` replaces only at the observed version, else `FS_STALE_VERSION`); omitting it from `writeText` is the third, unconditional state. `FsPathInfo` is the no-follow metadata shape that can report `symlink`, unlike target-level `FsInfo`. Failures throw `FsError` (extends `HarnessError`, [the structured error taxonomy Agent Note](../../../.agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md)) carrying a stable `FsErrorCode` (`FS_NOT_FOUND`, `FS_NOT_DIRECTORY`, `FS_NOT_TEXT`, `FS_NOT_REGULAR_FILE`, `FS_PERMISSION_DENIED`, `FS_IO_ERROR`, `FS_STALE_VERSION`, `FS_NOT_OBSERVED`, `FS_AMBIGUOUS_EDIT`, `FS_EDIT_NOT_FOUND`, `FS_ABORTED`); the tool registry surfaces `{ name, code }` on `isError` results. See `src/types.ts` for the full contracts. ## Model Experience @@ -54,7 +54,7 @@ No direct invalidation; the named consumer owns any request-prefix changes. ## Known Limitations and Deferred Work -- **Text-only by contract** — backends reject binary/non-UTF-8 content with `FS_NOT_TEXT`; binary-safe operations are a deliberate deferral of [the tool-schemas RFC](../../../docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md). -- **Eight primitives only** — no delete, rename/move, copy, or watch; `listDir` is single-level, with recursion, globbing, pagination, and search out of scope per [the directory-listing RFC](../../../docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md). +- **Text-only by contract** — backends reject binary/non-UTF-8 content with `FS_NOT_TEXT`; binary-safe operations are a deliberate deferral of [the tool-schemas Agent Note](../../../.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md). +- **Eight primitives only** — no delete, rename/move, copy, or watch; `listDir` is single-level, with recursion, globbing, pagination, and search out of scope per [the directory-listing Agent Note](../../../.agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md). - **No IO deadline** — the seam arms no timeout; cancellation is a best-effort optional `AbortSignal` per primitive (the deliberate [fs-family stance](../README.md)). - **Resolve-then-operate costs a remote backend two round-trips per tool call** — folding or caching resolution is left to such a backend. 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<FsWriteOutcome> + abstract writeText( + target: FsTarget, + content: string, + expected?: FsWriteIntent, + signal?: AbortSignal, + sandboxMode?: SandboxMode, + ): Promise<FsWriteOutcome> /** * 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<FsEditOutcome> + abstract editText( + target: FsTarget, + edit: FsEditRequest, + expected?: { version: FsVersion }, + signal?: AbortSignal, + sandboxMode?: SandboxMode, + ): Promise<FsEditOutcome> } 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-search/README.md b/packages/fs/tool-fs-search/README.md index 3dd8da34b8..7b254fd59e 100644 --- a/packages/fs/tool-fs-search/README.md +++ b/packages/fs/tool-fs-search/README.md @@ -1,20 +1,20 @@ # @deepseek-ai/dsh-tool-fs-search -The **model-facing filesystem discovery tools** — `glob`, `grep` — backed by the **bash executor seam**, not by `ctx.fs` provider methods. Each call assembles a fixed ripgrep command (every model-controlled value through one package-private shell-quoting helper), runs it via `ctx.bash.resolve(request)` → `ctx.bash.run(spec)` as an ordinary foreground tool call, parses the raw `rg` output, and returns a bounded, workdir-relative result. The package injects `tools`, `systemPrompt`, and `bash` — deliberately **not** `fs`; `ctx.spillStore` is read opportunistically with `ctx.get()` because formatted-result spill is optional. +The **model-facing filesystem discovery tools** — `glob`, `grep` — backed by the **bash executor seam**, not by `ctx.fs` provider methods. At load, the package probes `command -v rg` through `ctx.bash`; if the executor cannot find ripgrep on its `PATH`, it logs a warning and registers no tools or prompt sections. Each call assembles a fixed ripgrep command (every model-controlled value through one package-private shell-quoting helper), runs it via `ctx.bash.resolve(request)` → `ctx.bash.run(spec)` as an ordinary foreground tool call, parses the raw `rg` output, and returns a bounded, workdir-relative result. The package injects `tools`, `systemPrompt`, and `bash` — deliberately **not** `fs`; `ctx.spillStore` is read opportunistically with `ctx.get()` because formatted-result spill is optional. ```ts ignore-check -// Default deployment: a bash executor, then the discovery tools. +// Default deployment: a bash executor whose PATH includes rg, then the discovery tools. await ctx.plugin(LocalBashExecutor, { cwd: process.cwd() }) // @deepseek-ai/dsh-bash-local -await ctx.plugin(ToolFsSearch) // this package — registers glob/grep +await ctx.plugin(ToolFsSearch) // this package — conditionally registers glob/grep // Optional: a spill backend makes capped results fully recoverable. await ctx.plugin(LocalSpillStore) // @deepseek-ai/dsh-spill-local ``` Why bash-backed: local workspace discovery is naturally a process-backed `rg` workflow, and putting search on `ctx.fs` would force every filesystem backend to grow a search API. The bash executor owns request defaulting/capping, subprocess execution, process-group termination, environment scrubbing, raw output capture, and backend substitution (local, sandboxed, remote); this package owns schemas, argument validation, shell quoting, parsing, retention, formatted-result spill, and timeout declaration. The tools never call `ctx.bash.start()` and never expose a bash task id — the call returns only after `rg` exits, times out, is aborted, or fails. -## Deployment requirement: co-located bash + filesystem +## Deployment requirement: rg + co-located bash/filesystem -Returned paths are displayed relative to the resolved bash workdir (the calling agent's session cwd when present, else the executor's configured default) and are follow-up-readable with `read` only when the bash workdir and the filesystem root are the same workspace. v1 documents that requirement and performs no runtime cross-service validation; remote or virtual filesystem search waits for a shared workspace contract or a provider-specific search backend. +The mounted bash executor must be able to resolve `rg` from its `PATH` at plugin load; otherwise `glob` and `grep` are absent from the model-visible tool schema. Returned paths are displayed relative to the resolved bash workdir (the calling agent's session cwd when present, else the executor's configured default) and are follow-up-readable with `read` only when the bash workdir and the filesystem root are the same workspace. v1 documents that co-location requirement and performs no runtime cross-service validation; remote or virtual filesystem search waits for a shared workspace contract or a provider-specific search backend. ## Config @@ -43,7 +43,7 @@ Raw `rg` stdout is an internal transport detail. Each search requests `stdoutMax ## Errors -Search failures carry the package-owned `SearchError` (a `HarnessError` subclass), surfaced as `{ name, code }` on `isError` results: `SEARCH_INVALID_PATTERN` (ripgrep rejected the regex/glob), `SEARCH_FAILED` (missing `rg`, inaccessible target, signal kill, malformed `--json` output), `SEARCH_RAW_OUTPUT_OVERFLOW` (raw output over `rawOutputMaxBytes`, or still truncated after the requested stdout capture budget), and `SEARCH_ABORTED` (tool timeout, caller cancellation, or the bash executor's own timeout). ripgrep exit semantics are tool-owned: exit 0 is success with results, exit 1 is a successful empty search (`No files found` / `No matches found`), and only other exits are failures. Model argument mistakes (blank pattern, a list-valued `include`) stay ordinary tool argument errors. +Search failures carry the package-owned `SearchError` (a `HarnessError` subclass), surfaced as `{ name, code }` on `isError` results: `SEARCH_INVALID_PATTERN` (ripgrep rejected the regex/glob), `SEARCH_FAILED` (runtime `rg` disappearance after registration, inaccessible target, signal kill, malformed `--json` output), `SEARCH_RAW_OUTPUT_OVERFLOW` (raw output over `rawOutputMaxBytes`, or still truncated after the requested stdout capture budget), and `SEARCH_ABORTED` (tool timeout, caller cancellation, or the bash executor's own timeout). ripgrep exit semantics are tool-owned: exit 0 is success with results, exit 1 is a successful empty search (`No files found` / `No matches found`), and only other exits are failures. Model argument mistakes (blank pattern, a list-valued `include`) stay ordinary tool argument errors. ## Model Experience @@ -51,7 +51,7 @@ Search failures carry the package-owned `SearchError` (a `HarnessError` subclass #### What the model sees -Every request in this plugin's registration scope contains the independently registered glob and grep guidance below. Agent-scoped tool restrictions can hide either schema without removing its prompt section. +After the load-time `rg` probe succeeds, every request in this plugin's registration scope contains the independently registered glob and grep guidance below. Agent-scoped tool restrictions can hide either schema without removing its prompt section. ##### Glob guidance @@ -67,7 +67,7 @@ Use the grep tool — not shell grep or rg — to search file contents. Use read #### Token effect -Fixed guidance cost per request while the plugin is active. +Fixed guidance cost per request while the tools are registered. #### KV Cache effect @@ -77,7 +77,7 @@ Prefix-stable while the plugin scope and guidance text are unchanged. Activation #### What the model sees -The generated [`glob` and `grep` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs-search) while this surface is visible. +The generated [`glob` and `grep` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-fs-search) after the load-time `rg` probe succeeds and while this surface is visible. #### Token effect @@ -118,5 +118,5 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work - **Search and file access have no shared-workspace proof** — returned paths are follow-up-readable only when the bash workdir and filesystem root denote the same workspace; the package performs no runtime cross-service validation. -- **Ripgrep is a deployment dependency** — a missing or incompatible `rg` executable fails calls with `SEARCH_FAILED`; remote or virtual filesystems need a co-located executor or another search consumer. +- **Ripgrep is a deployment dependency** — a missing `rg` executable makes the package register no tools or guidance; an incompatible executable or one that disappears after registration fails calls with `SEARCH_FAILED`. Remote or virtual filesystems need a co-located executor or another search consumer. - **The schemas expose one bounded page** — offset pagination, case-mode switches, alternate output modes, and provider-backed discovery remain outside this package; capped complete output requires a spill backend. diff --git a/packages/fs/tool-fs-search/src/index.ts b/packages/fs/tool-fs-search/src/index.ts index 8c33d5770a..5930890b7a 100644 --- a/packages/fs/tool-fs-search/src/index.ts +++ b/packages/fs/tool-fs-search/src/index.ts @@ -1,6 +1,7 @@ /** * The model-facing filesystem discovery tool suite (`glob`, `grep`) over the - * bash executor seam (`ctx.bash`). This single plugin registers both tools. + * bash executor seam (`ctx.bash`). This single plugin registers both tools + * only when the mounted bash executor can find `rg` on its `PATH`. * * ## Bash-backed, not a `ctx.fs` provider method * @@ -12,9 +13,11 @@ * parsing, retention, formatted-result spill, and timeout declaration; the * bash executor owns request defaulting/capping, subprocess execution, * process-group termination, environment scrubbing, raw output capture, and - * backend substitution. The package injects `tools`, `systemPrompt`, and - * `bash` — deliberately NOT `fs`, and `ctx.spillStore` is read opportunistically - * with `ctx.get()` because formatted-result spill is optional. + * backend substitution. At load, the package probes `command -v rg` through the + * same bash seam; if ripgrep is absent, `glob` / `grep` and their prompt + * sections are not registered. The package injects `tools`, `systemPrompt`, + * and `bash` — deliberately NOT `fs`, and `ctx.spillStore` is read + * opportunistically with `ctx.get()` because formatted-result spill is optional. * * Returned paths are displayed relative to the resolved bash workdir and are * follow-up-readable only in co-located deployments where the bash workdir and @@ -80,6 +83,9 @@ export const Config: z<Config> = z.object({ /** The shape after schemastery applied the defaults. */ type ResolvedConfig = Required<Config> +/** POSIX-shell builtin probe for the ripgrep binary in the bash executor environment. */ +const RG_PROBE_COMMAND = 'command -v rg >/dev/null 2>&1' + /** Every search cap counts items/bytes/milliseconds — a positive integer, or retention and timeout arithmetic misbehaves silently. */ function assertPositiveInteger(name: string, value: number): void { if (!Number.isInteger(value) || value < 1) { @@ -87,8 +93,38 @@ function assertPositiveInteger(name: string, value: number): void { } } -/** Register the `glob`/`grep` filesystem discovery tool suite. */ -export function apply(ctx: Context, config: Config): void { +/** + * Check whether the mounted bash executor can find `rg`. + * + * Nonzero exit means "not available" and disables this optional tool suite. + * Infrastructure failures stay loud: a deployment with a broken bash executor + * should not silently lose tools in a way that looks like a deliberate skip. + * + * @param ctx - plugin context whose `bash` service is the executor the tools will use. + * @returns true when `command -v rg` exits 0, false when it exits nonzero. + */ +async function ripgrepAvailable(ctx: Context): Promise<boolean> { + const spec = ctx.bash.resolve({ command: RG_PROBE_COMMAND }) + let result + try { + result = await ctx.bash.run(spec) + } catch (error: unknown) { + throw new Error(`tool-fs-search: ripgrep availability probe could not start: ${String(error)}`, { cause: error }) + } + if (result.aborted || result.timedOut || result.signal !== null || result.exitCode === null) { + throw new Error('tool-fs-search: ripgrep availability probe did not complete') + } + return result.exitCode === 0 +} + +/** + * Register the `glob`/`grep` filesystem discovery tool suite when `rg` exists. + * + * @param ctx - plugin context; registrations are effects scoped to this plugin. + * @param config - resolved plugin configuration from schemastery. + * @returns when ripgrep is unavailable, resolves without registering any tools. + */ +export async function apply(ctx: Context, config: Config): Promise<void> { // schemastery (Config) has already filled every defaulted field. const resolved = config as ResolvedConfig assertPositiveInteger('globMaxResults', resolved.globMaxResults) @@ -96,6 +132,10 @@ export function apply(ctx: Context, config: Config): void { assertPositiveInteger('grepMaxLineBytes', resolved.grepMaxLineBytes) assertPositiveInteger('rawOutputMaxBytes', resolved.rawOutputMaxBytes) assertPositiveInteger('timeoutMs', resolved.timeoutMs) + if (!await ripgrepAvailable(ctx)) { + ctx.logger.warn('tool-fs-search: ripgrep (rg) not found on the bash executor PATH; glob/grep tools not registered') + return + } applyGlobTool(ctx, { maxResults: resolved.globMaxResults, rawOutputMaxBytes: resolved.rawOutputMaxBytes, diff --git a/packages/fs/tool-fs-search/tests/load-path.spec.ts b/packages/fs/tool-fs-search/tests/load-path.spec.ts index d3c28619a3..2eeffe65e6 100644 --- a/packages/fs/tool-fs-search/tests/load-path.spec.ts +++ b/packages/fs/tool-fs-search/tests/load-path.spec.ts @@ -18,9 +18,48 @@ import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' -import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' +import { BashExecutor } from '@deepseek-ai/dsh-bash' +import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash' import * as toolFsSearch from '@deepseek-ai/dsh-tool-fs-search' +const RG_PROBE_COMMAND = 'command -v rg >/dev/null 2>&1' + +/** + * Deterministic bash service for this Loader guard: the test wants to exercise + * the real unwrap/inject path, not depend on whether the host image has rg. + */ +class ProbeSuccessBashExecutor extends BashExecutor { + override resolve(request: BashExecRequest): BashExecSpec { + return { + command: request.command, + workdir: request.workdir ?? '/work', + timeoutMs: request.timeoutMs ?? 60_000, + stdoutMaxBytes: request.stdoutMaxBytes ?? 64_000, + signal: request.signal, + sandboxMode: request.sandboxMode, + } + } + + override run(spec: BashExecSpec): Promise<BashRunResult> { + if (spec.command !== RG_PROBE_COMMAND) { + throw new Error(`unexpected command in load-path guard: ${spec.command}`) + } + return Promise.resolve({ + exitCode: 0, + signal: null, + timedOut: false, + aborted: false, + timeoutMs: spec.timeoutMs, + stdout: { text: '', truncated: false }, + stderr: { text: '', truncated: false }, + }) + } + + override start(): BashProcess { + throw new Error('load-path guard must not start background processes') + } +} + describe('dsh-tool-fs-search real-load-path guard', () => { it('has no default export and keeps name/inject/Config through unwrapExports', () => { expect('default' in toolFsSearch).toBe(false) @@ -38,7 +77,7 @@ describe('dsh-tool-fs-search real-load-path guard', () => { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) - await ctx.plugin(LocalBashExecutor, {}) + await ctx.plugin(ProbeSuccessBashExecutor) const loader = Object.create(Loader.prototype) as Loader const unwrapped = loader.unwrapExports(toolFsSearch) as Parameters<Context['plugin']>[0] diff --git a/packages/fs/tool-fs-search/tests/tools.spec.ts b/packages/fs/tool-fs-search/tests/tools.spec.ts index 5363344f07..c11b10556a 100644 --- a/packages/fs/tool-fs-search/tests/tools.spec.ts +++ b/packages/fs/tool-fs-search/tests/tools.spec.ts @@ -2,12 +2,12 @@ * Consumer-surface tests for the search tools over a FAKE bash executor and a * FAKE spill backend, exercised through `ctx.tools.execute()` so nothing * bypasses the tool registry. The fake executor makes every seam outcome - * scriptable — truncated stdout with/without a raw spill path, abort/timeout, - * signal kills, ripgrep exit codes — so these tests verify schemas, argument - * validation, shell-safe command construction, workdir derivation, signal - * forwarding, `SEARCH_*` error classification, retention, formatted-result - * spill handoff, and the no-background-task invariant. Real-`rg` behavior is - * pinned separately in integration.spec.ts. + * scriptable — registration-time `rg` probing, truncated stdout with/without a + * raw spill path, abort/timeout, signal kills, ripgrep exit codes — so these + * tests verify schemas, argument validation, shell-safe command construction, + * workdir derivation, signal forwarding, `SEARCH_*` error classification, + * retention, formatted-result spill handoff, and the no-background-task + * invariant. Real-`rg` behavior is pinned separately in integration.spec.ts. */ import { describe, expect, it } from 'vitest' @@ -31,6 +31,8 @@ import { toWorkdirRelative, } from '@deepseek-ai/dsh-tool-fs-search' +const RG_PROBE_COMMAND = 'command -v rg >/dev/null 2>&1' + /** A successful run result over the given stdout; overrides script the failure shapes. */ function runResult(stdout: string, overrides?: Partial<BashRunResult>): BashRunResult { return { @@ -52,13 +54,18 @@ function runResult(stdout: string, overrides?: Partial<BashRunResult>): BashRunR * create a background task. */ class FakeBash extends BashExecutor { + probeRequests: BashExecRequest[] = [] + probeSpecs: BashExecSpec[] = [] requests: BashExecRequest[] = [] specs: BashExecSpec[] = [] startCalls = 0 + probeResult: BashRunResult = runResult('') + probeError?: Error handler: (spec: BashExecSpec) => BashRunResult = () => runResult('') override resolve(request: BashExecRequest): BashExecSpec { - this.requests.push(request) + if (request.command === RG_PROBE_COMMAND) this.probeRequests.push(request) + else this.requests.push(request) return { command: request.command, workdir: request.workdir ?? '/work', @@ -68,9 +75,14 @@ class FakeBash extends BashExecutor { sandboxMode: request.sandboxMode, } } - override run(spec: BashExecSpec): Promise<BashRunResult> { + override async run(spec: BashExecSpec): Promise<BashRunResult> { + if (spec.command === RG_PROBE_COMMAND) { + this.probeSpecs.push(spec) + if (this.probeError) throw this.probeError + return this.probeResult + } this.specs.push(spec) - return Promise.resolve(this.handler(spec)) + return this.handler(spec) } override start(): BashProcess { this.startCalls++ @@ -97,18 +109,36 @@ class FakeSpill extends SpillStore { interface SetupOptions { config?: ToolFsSearch.Config spill?: boolean + probeError?: Error + probeResult?: BashRunResult } async function setup(options: SetupOptions = {}) { const ctx = new Context() + const warnings: string[] = [] + ctx.logger.warn = ((message: unknown) => { warnings.push(String(message)) }) as typeof ctx.logger.warn await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(FakeBash) + const bash = ctx.bash as FakeBash + if (options.probeResult) bash.probeResult = options.probeResult + if (options.probeError) bash.probeError = options.probeError if (options.spill === true) await ctx.plugin(FakeSpill) const fiber = await ctx.plugin(ToolFsSearch, options.config) - const bash = ctx.bash as FakeBash const spill = options.spill === true ? ctx.get('spillStore') as FakeSpill : undefined - return { ctx, bash, spill, fiber } + return { ctx, bash, spill, fiber, warnings } +} + +/** Assert plugin setup rejects without letting Vitest pretty-print a live Context on failure. */ +async function expectSetupRejects(options: SetupOptions, message: RegExp): Promise<void> { + let thrown: string | undefined + try { + const loaded = await setup(options) + await loaded.fiber.dispose() + } catch (error: unknown) { + thrown = error instanceof Error ? error.message : String(error) + } + expect(thrown).toMatch(message) } /** A stand-in agent whose session header carries the given cwd (and a stable id). */ @@ -136,13 +166,37 @@ function matchLine(path: string, lineNumber: number, lineText: string): string { describe('registration', () => { it('registers glob and grep with their prompt sections', async () => { - const { ctx } = await setup() + const { ctx, bash } = await setup() + expect(bash.probeRequests).toHaveLength(1) + expect(bash.probeRequests[0]?.command).toBe(RG_PROBE_COMMAND) + expect(bash.probeRequests[0]).not.toHaveProperty('workdir') expect(ctx.tools.schemas().map(s => s.name).sort()).toEqual(['glob', 'grep']) const prompt = renderPrompt(await ctx.systemPrompt.assemble()) expect(prompt).toContain('Use the glob tool') expect(prompt).toContain('Use the grep tool') }) + it('does not register glob or grep when the bash executor cannot find rg', async () => { + const { ctx, warnings } = await setup({ probeResult: runResult('', { exitCode: 1 }) }) + expect(ctx.tools.schemas()).toHaveLength(0) + const sections = (await ctx.systemPrompt.assemble()).sections.map(s => s.name) + expect(sections).not.toContain('tool:glob') + expect(sections).not.toContain('tool:grep') + expect(warnings).toEqual([ + 'tool-fs-search: ripgrep (rg) not found on the bash executor PATH; glob/grep tools not registered', + ]) + }) + + it('rejects plugin load when the rg availability probe cannot run', async () => { + await expectSetupRejects({ probeError: new Error('spawn bash ENOENT') }, /spawn bash ENOENT/) + }) + + it('rejects plugin load when the rg availability probe is aborted or killed', async () => { + await expectSetupRejects({ + probeResult: runResult('', { aborted: true, exitCode: null, signal: 'SIGTERM' }), + }, /tool-fs-search: ripgrep availability probe did not complete/) + }) + it('stays pending until ctx.bash exists (inject)', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt) diff --git a/packages/fs/tool-fs/README.md b/packages/fs/tool-fs/README.md index 16535c446d..6dd22d384f 100644 --- a/packages/fs/tool-fs/README.md +++ b/packages/fs/tool-fs/README.md @@ -22,7 +22,7 @@ All keys are optional; the defaults are the shipped read caps. | `readMaxBytes` | `51200` | Byte cap on one `read` call's selected lines; overflow ends the window with a "capped" footer. | | `readStreamMinSize` | `10485760` | Files at or above this size (or with unknown size) stream instead of loading whole into memory. | -## Tools (schemas per [the filesystem tool schemas RFC](../../../docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.md)) +## Tools (schemas per [the filesystem tool schemas Agent Note](../../../.agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md)) | Tool | Arguments | Behavior | |---|---|---| @@ -34,7 +34,7 @@ Field names are snake_case to match Claude Code and existing harness tool schema ## The tool is the executor; policy is an event gate -The tools do **not** inject a policy service or inspect any cache. Each tool resolves the path via `ctx.fs.resolve(path, { cwd, signal })` — passing the calling agent's session cwd (`exec.agent.session.header.cwd`) so a relative path resolves against the session's workspace, matching `dsh-tool-bash`, and forwarding tool cancellation through resolution (see [the per-session cwd RFC](../../../docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.md)) — then: +The tools do **not** inject a policy service or inspect any cache. Each tool resolves the path via `ctx.fs.resolve(path, { cwd, signal })` — passing the calling agent's session cwd (`exec.agent.session.header.cwd`) so a relative path resolves against the session's workspace, matching `dsh-tool-bash`, and forwarding tool cancellation through resolution (see [the per-session cwd Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md)) — then: - **read** — one `ctx.fs.stat` (type + size routing + version), then `readText`/`streamText`, then builds the line window, then emits `fs/observed` with a plain `ctx.emit`. (1 stat.) - **write** — `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` for the optional guard, then `ctx.fs.writeText(target, content, intent)`, then `fs/observed`. (0 stat.) @@ -46,7 +46,7 @@ The tool passes `exec` (the tool-execution context) as the opaque `actor` on eve `fs/observed` fires AFTER the read/write/edit already succeeded, via a plain `ctx.emit`. A listener is contractually a synchronous, side-effect-only recorder (`@deepseek-ai/dsh-fs-policy`'s is a `WeakMap.set`); the tool does not guard the emit, so a listener that throws would surface as the tool's `isError` result — async or fallible observation does not belong on this event. -`read` opts into concurrent scheduling because its only mutation is the synchronous version recorder. Recorder races fail closed when a later `write` or `edit` re-checks the version under its target lock; both mutation tools remain exclusive. See the [parallel tool-call RFC](../../../docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md). +`read` opts into concurrent scheduling because its only mutation is the synchronous version recorder. Recorder races fail closed when a later `write` or `edit` re-checks the version under its target lock; both mutation tools remain exclusive. See the [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md). The package root exports only the Cordis plugin contract (`name`, `inject`, `Config`, and `apply`). Read rendering (line windowing + output formatting) lives in `src/read-render.ts` (Cordis-free, independently unit-tested); `src/read.ts`/`write.ts`/`edit.ts` are the tool executors and `src/index.ts` composes them. 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<SandboxMode | undefined> { + 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 </content>` } +/** + * 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<FsWriteOutcome> { + 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<FsEditOutcome> { + 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<string, unknown> }> = []): 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<string, unknown>) => { 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<string, { enum?: string[] }> } } + } + + 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/guard/repeat-tool-guard/README.md b/packages/guard/repeat-tool-guard/README.md index 4c868654fc..ef5e4e5846 100644 --- a/packages/guard/repeat-tool-guard/README.md +++ b/packages/guard/repeat-tool-guard/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-repeat-tool-guard -An advisory loop-breaker, not a model-facing tool: it never appears in the tool list, never vetoes or rewrites a call, and adds exactly one behavior — it watches each agent's stream of tool calls, counts runs of consecutive calls to the same tool with identical canonicalized arguments, and at configured run lengths injects an escalating advisory reminder telling the model to stop repeating itself, re-read the last result, and either change approach or conclude. The decision (retry differently, gather more evidence, or finish) stays entirely with the model: a legitimately repeated call is delayed by nothing and blocked by nothing. Decision record: [the repeat-tool-guard RFC](../../../docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.md). +An advisory loop-breaker, not a model-facing tool: it never appears in the tool list, never vetoes or rewrites a call, and adds exactly one behavior — it watches each agent's stream of tool calls, counts runs of consecutive calls to the same tool with identical canonicalized arguments, and at configured run lengths injects an escalating advisory reminder telling the model to stop repeating itself, re-read the last result, and either change approach or conclude. The decision (retry differently, gather more evidence, or finish) stays entirely with the model: a legitimately repeated call is delayed by nothing and blocked by nothing. Decision record: [the repeat-tool-guard Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md). ## Config @@ -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 2279f53ade..0c630686b4 100644 --- a/packages/guard/repeat-tool-guard/src/index.ts +++ b/packages/guard/repeat-tool-guard/src/index.ts @@ -2,7 +2,7 @@ * Advisory per-agent repeat-call detector. It enriches post-execute decisions * with logged model context without vetoing or rewriting calls. Configuration * and chain semantics live in the package README; rationale lives in the - * repeat-tool-guard RFC. + * repeat-tool-guard Agent Note. * @module @deepseek-ai/dsh-repeat-tool-guard */ @@ -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/README.md b/packages/hooks/README.md index b483964c32..0bd64e3f99 100644 --- a/packages/hooks/README.md +++ b/packages/hooks/README.md @@ -1,6 +1,6 @@ # hooks/ — hook bridges + shared protocol -The hooks subsystem lets users extend the agent at lifecycle points the way Claude Code and Codex do — by pointing a bridge plugin at an existing `hooks.json` (or settings) so those external shell hooks run faithfully. The canonical extension surface itself is the harness's typed interception seams ([the interception-seams RFC](../../docs/rfc/implemented/feature/2026-06-30-interception-seams.md)); a "native hook" is just an ordinary cordis plugin on those seams. These packages are the **bridges** that translate the external shell-hook protocol onto that same surface, plus the shared wire-protocol library they build on. +The hooks subsystem lets users extend the agent at lifecycle points the way Claude Code and Codex do — by pointing a bridge plugin at an existing `hooks.json` (or settings) so those external shell hooks run faithfully. The canonical extension surface itself is the harness's typed interception seams ([the interception-seams Agent Note](../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md)); a "native hook" is just an ordinary cordis plugin on those seams. These packages are the **bridges** that translate the external shell-hook protocol onto that same surface, plus the shared wire-protocol library they build on. | Package | Role | Shape | |---|---|---| diff --git a/packages/hooks/hook-protocol/README.md b/packages/hooks/hook-protocol/README.md index b36059bd4f..cae3f5e6b6 100644 --- a/packages/hooks/hook-protocol/README.md +++ b/packages/hooks/hook-protocol/README.md @@ -27,7 +27,7 @@ Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claud Declaration-merged into `SessionEventMap` (log-only, like `compact/*` — NOT a `SurfaceEventType`, no `surfaceOp`): `hook/invoked` (a hook command ran) and `hook/result` (its outcome, paired by `handlerId`, with `appendHookResult` owning the decision rule). Payloads and per-event JSDoc are in the generated [persistence log event catalog](../../../docs/persistence-catalog.md); `stderrSummary` is truncated to the record's `stderrSummaryMaxChars` (the bridge's config, reference default `DEFAULT_STDERR_SUMMARY_MAX_CHARS` = 500; omitted when empty). -Like every event they must sit inside an open turn. The mid-turn points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn by construction; `SessionStart` gets no `hook/*` record (its injected `context/message` is the durable evidence) — see the hooks RFC. +Like every event they must sit inside an open turn. The mid-turn points (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`) fire inside the loop's open turn by construction; `SessionStart` gets no `hook/*` record (its injected `context/message` is the durable evidence) — see the hooks Agent Note. ## Model Experience @@ -39,5 +39,5 @@ No direct invalidation; the named consumer owns any request-prefix changes. ## Known Limitations and Deferred Work -- **`HookOutput.updatedInput` is parsed but not honored** — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite RFC](../../../docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)); a bridge logs + warns when a hook sets it. See `src/types.ts` for the full contracts. +- **`HookOutput.updatedInput` is parsed but not honored** — input rewrite is a deferred consistency-design problem ([the pre-tool-input-rewrite Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)); a bridge logs + warns when a hook sets it. See `src/types.ts` for the full contracts. - **An invalid matcher regex matches nothing, silently** — `matchesMatcher` never throws; surfacing the error needs a diagnostic-returning variant or parse-time validation (`TODO(matcher-diagnostics)`). diff --git a/packages/hooks/hook-protocol/src/types.ts b/packages/hooks/hook-protocol/src/types.ts index 7458419a01..e14473b3e1 100644 --- a/packages/hooks/hook-protocol/src/types.ts +++ b/packages/hooks/hook-protocol/src/types.ts @@ -43,7 +43,7 @@ declare module '@deepseek-ai/dsh-session' { /** * The bridge that ran a hook — the CC bridge stamps `'claude'`, the Codex * bridge `'codex'`. A native plugin on the interception seams is not a bridge - * and writes no `hook/*` provenance (see the interception-seams RFC). + * and writes no `hook/*` provenance (see the interception-seams Agent Note). */ export type HookDialect = 'claude' | 'codex' @@ -130,7 +130,7 @@ export interface HookOutput { systemMessage?: string /** * A tool-input rewrite a hook requested (CC `updatedInput`). PARSED but NOT - * honored — input rewrite is deferred (see the interception-seams RFC); a + * honored — input rewrite is deferred (see the interception-seams Agent Note); a * bridge logs + warns when this is present. */ updatedInput?: Record<string, unknown> diff --git a/packages/hooks/hooks-claude/README.md b/packages/hooks/hooks-claude/README.md index 96c0362023..b492b2939d 100644 --- a/packages/hooks/hooks-claude/README.md +++ b/packages/hooks/hooks-claude/README.md @@ -2,7 +2,7 @@ A cordis plugin that runs the supported command-hook subset of a user's existing **Claude Code** hook config (a `hooks.json`, or a settings file's `hooks` key) on the harness's canonical interception seams. It is the **CC dialect** half of the hooks subsystem: it owns the bridge's CC-shaped per-event stdin payloads, CC's env + `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` substitution, and the mapping from a hook's neutral outcome onto the harness's typed Decisions. The dialect-agnostic primitives (matcher, exit-code/stdout codec, `ctx.bash` execution, most-restrictive merge, the `hook/*` events) come from [`@deepseek-ai/dsh-hook-protocol`](../hook-protocol/README.md). -A native cordis plugin could do everything this bridge does — more powerfully, with typed returns and no serialization boundary. **The bridge exists only as a compatibility path for the mapped CC command-hook subset**; anything bespoke should be a native plugin on the same seams (see [the interception-seams RFC](../../../docs/rfc/implemented/feature/2026-06-30-interception-seams.md)). +A native cordis plugin could do everything this bridge does — more powerfully, with typed returns and no serialization boundary. **The bridge exists only as a compatibility path for the mapped CC command-hook subset**; anything bespoke should be a native plugin on the same seams (see [the interception-seams Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md)). ## Config @@ -44,7 +44,7 @@ The hooks **themselves** run in the agent's session workspace: for the agent-sco The three emit points run detached — no seam awaits a `SessionStart`/`SubagentStart`/`SubagentStop` hook. Each run chain is tracked, and disposing the bridge aborts still-running hook processes, then drains the continuations before the dispose resolves (`createDetachedRuns` in `dsh-hook-protocol`). -The matcher subject is the tool name (`PreToolUse`/`PostToolUse`), the session source (`SessionStart`), or a constant `agent_type` of `general-purpose` (`SubagentStart`/`SubagentStop` — the harness subagent seam carries no per-kind label, so the bridge reports Claude Code's own Task-tool default; a default/`*`/empty `agent_type` matcher fires, a specific-kind matcher does not); `UserPromptSubmit`/`Stop` ignore matchers. Multiple file-configured hooks on one point run **serially, in config order**, and fold most-restrictively (`deny > ask > allow`, see `dsh-hook-protocol`); serial keeps each hook's `hook/invoked`/`hook/result` pair adjacent in the log, and the fold is order-independent for the decision (see the RFC's "run serially, not concurrently" note). +The matcher subject is the tool name (`PreToolUse`/`PostToolUse`), the session source (`SessionStart`), or a constant `agent_type` of `general-purpose` (`SubagentStart`/`SubagentStop` — the harness subagent seam carries no per-kind label, so the bridge reports Claude Code's own Task-tool default; a default/`*`/empty `agent_type` matcher fires, a specific-kind matcher does not); `UserPromptSubmit`/`Stop` ignore matchers. Multiple file-configured hooks on one point run **serially, in config order**, and fold most-restrictively (`deny > ask > allow`, see `dsh-hook-protocol`); serial keeps each hook's `hook/invoked`/`hook/result` pair adjacent in the log, and the fold is order-independent for the decision (see the Agent Note's "run serially, not concurrently" note). Every agent-scoped stdin payload carries `session_id` and string-shaped `transcript_path`. The bridge resolves the latter through `ctx.sessionPersistence.locate(session.header)` when available and otherwise sends `''`. Lookup does not create or flush the artifact, so a path can be absent before the first turn-end checkpoint or omit the current open turn. @@ -87,7 +87,7 @@ A blocked prompt sends no request and invalidates nothing. Denial, feedback, and - **Unsupported hook events (23 of Claude Code's current 30):** `Setup`, `InstructionsLoaded`, `UserPromptExpansion`, `MessageDisplay`, `PermissionRequest`, `PostToolUseFailure`, `PostToolBatch`, `PermissionDenied`, `Notification`, `TaskCreated`, `TaskCompleted`, `StopFailure`, `TeammateIdle`, `ConfigChange`, `CwdChanged`, `FileChanged`, `WorktreeCreate`, `WorktreeRemove`, `PreCompact`, `PostCompact`, `SessionEnd`, `Elicitation`, and `ElicitationResult`. Config for these events is parsed but never dispatched. The comparison baseline is Claude Code's [official hook-event reference](https://code.claude.com/docs/en/hooks#hook-events). - **`SessionStart` is partial:** JSON `additionalContext` is consumed, but plain stdout context, `initialUserMessage`, `sessionTitle`, `watchPaths`, `reloadSkills`, and `CLAUDE_ENV_FILE` are unsupported. The hook runs detached, so context can miss the first request (`TODO(session-start-gating)`), and the payload omits current optional fields such as `model`, `agent_type`, and `session_title`. - **`UserPromptSubmit` is partial:** blocking and JSON `additionalContext` work, but plain stdout context, `sessionTitle`, and `suppressOriginalPrompt` are unsupported. Unless overridden, the bridge also uses its 600-second default instead of Claude Code's event-specific 30-second command timeout. -- **`PreToolUse` is partial:** `deny` and `ask` decisions work; `allow` does not pre-approve, `defer` is unsupported, `additionalContext` is ignored, and `updatedInput` is logged + warned but not honored ([the pre-tool-input-rewrite RFC](../../../docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)). +- **`PreToolUse` is partial:** `deny` and `ask` decisions work; `allow` does not pre-approve, `defer` is unsupported, `additionalContext` is ignored, and `updatedInput` is logged + warned but not honored ([the pre-tool-input-rewrite Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)). - **`PostToolUse` is partial:** blocking feedback and JSON `additionalContext` work, but `updatedToolOutput` and `updatedMCPToolOutput` are unsupported and `tool_response` is flattened to text. - **`SubagentStart` and `SubagentStop` are partial:** both report a constant `agent_type` of `general-purpose` and use the child session id where Claude Code reports the parent session. Start context is best-effort and can only reach a live in-process child, while stop is observe-only and cannot block the subagent or feed it context. Start omits `transcript_path`; stop also omits `agent_transcript_path`, `last_assistant_message`, `background_tasks`, and `session_crons` and always reports `stop_hook_active: false`. - **`Stop` is partial:** blocking forces another model turn, but `stop_hook_active` is always `false`, `last_assistant_message`, `background_tasks`, and `session_crons` are omitted, and the consecutive-block cap is not implemented (`TODO(stop-loop-guard)`). An unconditionally blocking hook therefore force-continues every step unless it self-limits. diff --git a/packages/hooks/hooks-claude/src/index.ts b/packages/hooks/hooks-claude/src/index.ts index 121e5cf75a..03ac21fea6 100644 --- a/packages/hooks/hooks-claude/src/index.ts +++ b/packages/hooks/hooks-claude/src/index.ts @@ -5,7 +5,7 @@ * mapping; shared execution and parsing live in `dsh-hook-protocol`. * `updatedInput` is logged and warned but not honored. Bespoke behavior should * use typed native plugins on the same seams; see the - * [hook-bridges RFC](../../../../docs/rfc/implemented/feature/2026-06-30-hook-bridges.md). + * [hook-bridges Agent Note](../../../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md). * @module @deepseek-ai/dsh-hooks-claude */ 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/README.md b/packages/hooks/hooks-codex/README.md index 0e69362c36..0c8c4c16ba 100644 --- a/packages/hooks/hooks-codex/README.md +++ b/packages/hooks/hooks-codex/README.md @@ -10,7 +10,7 @@ This bridge implements a deliberate subset of Codex's current hook protocol: - **No Codex plugin env injection and no config-time placeholder substitution** (the command still receives the executor's environment and runs through its shell). - **No pre-tool approval or rewrite path** — a hook can block, but the bridge does not pre-approve or replace tool input. -A native cordis plugin could do everything this bridge does, more powerfully; the bridge exists only as a compatibility path for the mapped Codex subset (see [the interception-seams RFC](../../../docs/rfc/implemented/feature/2026-06-30-interception-seams.md)). +A native cordis plugin could do everything this bridge does, more powerfully; the bridge exists only as a compatibility path for the mapped Codex subset (see [the interception-seams Agent Note](../../../.agents/notes/implemented/feature/2026-06-30-interception-seams.md)). ## Config diff --git a/packages/hooks/hooks-codex/src/index.ts b/packages/hooks/hooks-codex/src/index.ts index 8fa89a469d..75c33e2d92 100644 --- a/packages/hooks/hooks-codex/src/index.ts +++ b/packages/hooks/hooks-codex/src/index.ts @@ -5,7 +5,7 @@ * or command substitution, and no pre-tool approval or rewrite path; only * blocking decisions are honored. Shared execution and parsing live in * `dsh-hook-protocol`; see the - * [hook-bridges RFC](../../../../docs/rfc/implemented/feature/2026-06-30-hook-bridges.md). + * [hook-bridges Agent Note](../../../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md). * @module @deepseek-ai/dsh-hooks-codex */ 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/llm/README.md b/packages/llm/README.md index 4a5e6769e2..ac08ffafc2 100644 --- a/packages/llm/README.md +++ b/packages/llm/README.md @@ -9,4 +9,4 @@ The LLM seam and its provider adapters. The interface package (`llm`) owns the a | `llm-deepseek/` | DeepSeek API adapter (hand-rolled fetch/SSE) | (registers on `ctx.llm`) | | `llm-pi-ai/` | Multi-provider adapter via `@earendil-works/pi-ai` | (registers on `ctx.llm`) | -The interface lives at `llm/llm/`; adapters and the reusable token meter are flat siblings under the group. Requests route by `provider`, while `model` is passed through to the selected adapter. A new provider adapter joins here and registers one or more provider routes on `ctx.llm` without touching the interface. See [twin LLM adapters](../../docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the contract-validation origin of the two shipping implementations and the [replay token meter RFC](../../docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.md) for measurement ownership. +The interface lives at `llm/llm/`; adapters and the reusable token meter are flat siblings under the group. Requests route by `provider`, while `model` is passed through to the selected adapter. A new provider adapter joins here and registers one or more provider routes on `ctx.llm` without touching the interface. See [twin LLM adapters](../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the contract-validation origin of the two shipping implementations and the [replay token meter Agent Note](../../.agents/notes/implemented/architecture/2026-07-15-replay-token-meter-service.md) for measurement ownership. diff --git a/packages/llm/llm-deepseek/src/adapter.ts b/packages/llm/llm-deepseek/src/adapter.ts index 4f7cc66f87..918c8eee82 100644 --- a/packages/llm/llm-deepseek/src/adapter.ts +++ b/packages/llm/llm-deepseek/src/adapter.ts @@ -91,6 +91,9 @@ export class DeepSeekAdapter extends LlmAdapter { 'content-type': 'application/json', 'accept': 'text/event-stream', ...attributionHeaders(), + ...options.sessionId !== undefined + ? { 'x-deepseek-harness-session-id': String(options.sessionId) } + : {}, }, body: JSON.stringify(body), ...options.signal ? { signal: options.signal } : {}, diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index 5d15a1e437..954a0ecebd 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -3,6 +3,7 @@ import type { IncomingMessage, Server, ServerResponse } from 'node:http' import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import LlmService, { CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, userAgent } from '@deepseek-ai/dsh-llm' +import { SessionId } from '@deepseek-ai/dsh-session' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import { DeepSeekAdapter } from '@deepseek-ai/dsh-llm-deepseek' import { httpErrorCode } from '../src/adapter.ts' @@ -133,6 +134,19 @@ describe('DeepSeekAdapter against a mock server', () => { expect(kinds).toEqual(['block-start', 'text-delta', 'block-end', 'usage', 'finish']) }) + it('forwards the harness session id for host-side trajectory routing', async () => { + const server = await mockServer([{ kind: 'sse', events: textEvents }]) + const ctx = await harness(server.url) + + await assemble(ctx, { + model: 'deepseek-v4-flash', + messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], + sessionId: SessionId('child-session'), + }) + + expect(server.headers[0]?.['x-deepseek-harness-session-id']).toBe('child-session') + }) + it('forwards thinking config onto the wire', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const ctx = await harness(server.url, { thinking: 'disabled', reasoningEffort: 'high' }) diff --git a/packages/llm/llm/README.md b/packages/llm/llm/README.md index 24a8879aa3..f9142dbc52 100644 --- a/packages/llm/llm/README.md +++ b/packages/llm/llm/README.md @@ -40,7 +40,7 @@ Streaming is a raw chunk protocol (`block-start`, `text-delta`, `reasoning-delta ### App attribution (`attribution.ts`) -Every product adapter sends application identity on provider HTTP requests. `attributionHeaders(identity?)` builds the standard `User-Agent`, defaulting to public `APP_IDENTITY`; white-label deployments may replace but not suppress it. Adapters verify the wire header directly or through their library hook. See [the attribution RFC](../../../docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md). +Every product adapter sends application identity on provider HTTP requests. `attributionHeaders(identity?)` builds the standard `User-Agent`, defaulting to public `APP_IDENTITY`; white-label deployments may replace but not suppress it. Adapters verify the wire header directly or through their library hook. See [the attribution Agent Note](../../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md). ### Classes @@ -52,7 +52,7 @@ Every product adapter sends application identity on provider HTTP requests. `att ### Real adapters -Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses hand-rolled fetch/SSE for the `deepseek` route, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) dynamically resolves configured provider/model pairs through `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish, tool arguments remain raw strings, and errors take one of two sanctioned paths. See [the twin LLM adapters](../../../docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the design rationale. +Two adapters implement `LlmAdapter` on different internals: [`@deepseek-ai/dsh-llm-deepseek`](../llm-deepseek) uses hand-rolled fetch/SSE for the `deepseek` route, while [`@deepseek-ai/dsh-llm-pi-ai`](../llm-pi-ai) dynamically resolves configured provider/model pairs through `@earendil-works/pi-ai`. Both follow the `StreamChunk` conventions in `types.ts`: usage precedes finish, tool arguments remain raw strings, and errors take one of two sanctioned paths. See [the twin LLM adapters](../../../.agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md) for the design rationale. ## Model Experience @@ -65,8 +65,8 @@ Pass-through; the registry preserves the assembled request prefix, while the sel ## Known Limitations and Deferred Work - **No default retry/caching/rate-limit policy ships in this service** — `llm/stream` remains the call-wrapper seam; the agent loop separately offers proven model-request failures to `agent/request-error`, whose default preserves the original failure. -- **`GenerateOptions` sampling is `temperature`/`maxTokens`/`stop` only** — no `tool_choice`, `top_p`, or penalty fields; the vocabulary grows when a producer lands ([dropped inert knobs](../../../docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.md)). -- **Producer-gated variants stay out until produced** — `prefill`, per-tool `strict`, block `cache` hints, and the `agent` message-source variant were pruned as producerless ([RFC](../../../docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md)). +- **`GenerateOptions` sampling is `temperature`/`maxTokens`/`stop` only** — no `tool_choice`, `top_p`, or penalty fields; the vocabulary grows when a producer lands ([dropped inert knobs](../../../.agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md)). +- **Producer-gated variants stay out until produced** — `prefill`, per-tool `strict`, block `cache` hints, and the `agent` message-source variant were pruned as producerless ([Agent Note](../../../.agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md)). - **`BlockAssembler` handles core block kinds only** — a plugin-added block type whose stream is never closed by `block-end` makes `blocks()` throw. - **`APP_IDENTITY.url` names a repository that does not exist yet** — `FIXME`: creating the public `deepseek-ai/deepseek-harness-sdk` repo gates the first release. - **`GenerateOptions.sessionId` is a locally-declared brand** — importing dsh-session's `SessionId` would cycle; a future ids-owning package would dissolve the workaround. diff --git a/packages/llm/llm/src/attribution.ts b/packages/llm/llm/src/attribution.ts index 8f0f156aa1..cdaea4b96b 100644 --- a/packages/llm/llm/src/attribution.ts +++ b/packages/llm/llm/src/attribution.ts @@ -1,7 +1,7 @@ /** * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping * adapters from drifting. See - * `docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`. + * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`. * * App-attribution vocabulary for provider requests. * @module @deepseek-ai/dsh-llm/attribution diff --git a/packages/llm/llm/src/index.ts b/packages/llm/llm/src/index.ts index ac31738a61..f276aa9f92 100644 --- a/packages/llm/llm/src/index.ts +++ b/packages/llm/llm/src/index.ts @@ -35,7 +35,7 @@ declare module 'cordis' { * adapter's stream, or yield your own chunks to short-circuit. * @param options - the full request. A LOOP-built request arrives * deep-frozen (mutation throws): its content is a pure function of the - * session log (the reconstructability RFC), so listeners read it, never + * session log (the reconstructability Agent Note), so listeners read it, never * rewrite it. A hand-built one-shot (compaction summarize) is the * caller's own object and stays mutable here. * @mode waterfall diff --git a/packages/llm/llm/tests/properties.spec.ts b/packages/llm/llm/tests/properties.spec.ts index 3bb2a76c38..0d65b545d1 100644 --- a/packages/llm/llm/tests/properties.spec.ts +++ b/packages/llm/llm/tests/properties.spec.ts @@ -1,5 +1,5 @@ /** - * Property-based tests for the BlockAssembler (the property-testing RFC). + * Property-based tests for the BlockAssembler (the property-testing Agent Note). * * The assembler is protocol-shaped: arbitrary interleavings of block-start, * deltas, block-end, usage, and finish — valid and malformed (duplicate diff --git a/packages/mcp/mcp-client/src/tools.ts b/packages/mcp/mcp-client/src/tools.ts index ae01fc0f84..7b9217e814 100644 --- a/packages/mcp/mcp-client/src/tools.ts +++ b/packages/mcp/mcp-client/src/tools.ts @@ -3,7 +3,7 @@ * under deterministic server-qualified public names, and handles re-sync when * the server's tool list changes. * - * Naming contract (see the mcp-client RFC "Naming invariants"): every MCP tool + * Naming contract (see the mcp-client Agent Note "Naming invariants"): every MCP tool * has the stable identity `(serverName, rawName)`; the model-facing public name * is `mcp__<serverName>__<rawName>`, normalized to the DeepSeek function-name * constraints. The raw name is only ever sent on the wire (`tools/call`); the diff --git a/packages/sandbox/README.md b/packages/sandbox/README.md index cb2c7b5747..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](../../docs/rfc/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 RFC](../../docs/rfc/implemented/feature/2026-07-06-sandbox.md). +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 RFC'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/README.md b/packages/sandbox/sandbox-local/README.md index 7df7cc2e31..ed85802cf7 100644 --- a/packages/sandbox/sandbox-local/README.md +++ b/packages/sandbox/sandbox-local/README.md @@ -4,9 +4,9 @@ Local implementation of the [`dsh-sandbox`](../sandbox/) seam. It selects and ca The package root exports the default and named `LocalSandboxProvider` plugin, `Config`, and its public test-injection seam; platform profile builders stay internal. -Unsupported platforms and unusable runners fail closed with `SANDBOX_UNAVAILABLE`; execution never silently falls through unconfined. Each wrap carries runner-failure signatures so consumers can distinguish a broken sandbox from a command failure. The [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md) owns selection rationale and profile differences. +Unsupported platforms and unusable runners fail closed with `SANDBOX_UNAVAILABLE`; execution never silently falls through unconfined. Each wrap carries runner-failure signatures so consumers can distinguish a broken sandbox from a command failure. The [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns selection rationale and profile differences. -Policy is per call; the provider stores only the mechanism and cached runner verdict. Each wrap reports enforcement completeness plus backend-specific denial and runner-failure signatures. `runnerCommand` is an operator assertion of a bwrap-shaped runner and skips probes, but missing or unexecutable commands still fail closed at execution. Because its mechanism is unknown, it carries both Linux denial dialects. `probeTimeoutMs` bounds functional probes. The [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md) owns selection and failure semantics. +Policy is per call; the provider stores only the mechanism and cached runner verdict. Each wrap reports enforcement completeness plus backend-specific denial and runner-failure signatures. `runnerCommand` is an operator assertion of a bwrap-shaped runner and skips probes, but missing or unexecutable commands still fail closed at execution. Because its mechanism is unknown, it carries both Linux denial dialects. `probeTimeoutMs` bounds functional probes. The [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md) owns selection and failure semantics. The Seatbelt profile is allow-default with `(deny file-write*)` plus write allow-lists, so exactly the mode's promised file effects are governed: `read-only` grants the `/dev/null` literal alone; `workspace-write` adds the workspace root, `/tmp`, and the per-user darwin temp dir (`os.tmpdir()` — the platform's real temp area for mkstemp-family tools), every root canonicalized because Seatbelt matches resolved paths (`/tmp` IS `/private/tmp`). Apple marks the `sandbox-exec` CLI deprecated but ships it on every macOS; the functional probe is what fails closed if that ever changes. 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<Config> = 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/README.md b/packages/sandbox/sandbox/README.md index 087786c802..fccb08c18f 100644 --- a/packages/sandbox/sandbox/README.md +++ b/packages/sandbox/sandbox/README.md @@ -1,12 +1,12 @@ # @deepseek-ai/dsh-sandbox -Abstract process-sandbox seam. Owns the `ctx.sandbox` service contract ([`SandboxProvider`](src/index.ts)) and the confinement vocabulary the harness shares: `SandboxMode` (`read-only` / `workspace-write` / `danger-full-access`, file effects only), `SandboxEnforcement` (`full` / `partial`, per kernel ABI), `SandboxPolicy` (per-CALL policy — mode + workspace root), and the fail-closed `SANDBOX_UNAVAILABLE` error. Interface package of the [capability-seam split](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md): depends only on cordis (+ the harness error base), never on a backend. +Abstract process-sandbox seam. Owns the `ctx.sandbox` service contract ([`SandboxProvider`](src/index.ts)) and the confinement vocabulary the harness shares: `SandboxMode` (`read-only` / `workspace-write` / `danger-full-access`, file effects only), `SandboxEnforcement` (`full` / `partial`, per kernel ABI), `SandboxPolicy` (per-CALL policy — mode + workspace root), and the fail-closed `SANDBOX_UNAVAILABLE` error. Interface package of the [capability-seam split](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md): depends only on cordis (+ the harness error base), never on a backend. The contract in one line: `ctx.sandbox.confine(argv, policy)` returns the argv to spawn INSTEAD of your own — wrapped so the process (and everything it spawns) runs confined — plus two facts about the selected backend: the enforcement completeness it achieves and its denial dialect (`denialSignatures`, the stderr substrings its kernel prints on a denied file effect — what stderr-inferring consumers match instead of a cross-backend union); when no backend is usable it throws rather than passing the argv through unconfined. Policy rides the call, not the provider: two consumers may confine under different policies at the same instant (bash under `read-only` while a confined child agent keeps its state directory writable), and an approved escalated retry is just a new call with a wider policy. -**Same-world confinement only.** A backend shares the host's filesystem and kernel (`bwrap`, Landlock, Seatbelt); `workspaceRoot` names a real host path. Containers, microVMs, and remote executors are NOT backends of this seam — they replace whole capability implementations (`ctx.bash`, `ctx.fs`) as environment-coherent groups. The boundary and its rationale: [the sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md). +**Same-world confinement only.** A backend shares the host's filesystem and kernel (`bwrap`, Landlock, Seatbelt); `workspaceRoot` names a real host path. Containers, microVMs, and remote executors are NOT backends of this seam — they replace whole capability implementations (`ctx.bash`, `ctx.fs`) as environment-coherent groups. The boundary and its rationale: [the sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). Implementations: [`@deepseek-ai/dsh-sandbox-local`](../sandbox-local/) (Linux: `bwrap`, else the per-platform Landlock launcher; macOS: `sandbox-exec`/Seatbelt). Consumers: [`@deepseek-ai/dsh-bash-sandbox`](../../bash/bash-sandbox/) (wraps `['bash', '-c', command]`). 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<string, readonly SandboxMode[]> = { + '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<A = object, C = string> { + /** + * 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<EscalationOutcome> +} + +/** + * 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<A = object, C = string> { + /** The approval requester (`ctx.approval`), or `undefined` when none is composed. */ + approver: EscalationApprover<A, C> | 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<A, C>(request: EscalationRequest, approval: EscalationApproval<A, C>): Promise<SandboxMode> { + 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<Parameters<typeof approveEscalation>[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<Parameters<typeof approveEscalation>[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/sdk/README.md b/packages/sdk/README.md index 953d52ea76..9bd32b4017 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -2,7 +2,7 @@ Developer tooling for creating, editing, building, and running DeepSeek Harness projects. -The [feature RFC](../../docs/rfc/proposed/feature/2026-07-14-sdk-developer-projects.md) owns the developer workflow; the [architecture RFC](../../docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md) owns the package and project-editing boundaries. +The [feature Agent Note](../../.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md) owns the developer workflow; the [architecture Agent Note](../../.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md) owns the package and project-editing boundaries. | Package | Role | |---|---| diff --git a/packages/sdk/helper/README.md b/packages/sdk/helper/README.md index 31ee34f59e..45ad545458 100644 --- a/packages/sdk/helper/README.md +++ b/packages/sdk/helper/README.md @@ -1,6 +1,6 @@ # `@deepseek-ai/dsh-helper` -Shared project domain and infrastructure for `create-sdk` and `dsh-sdk config`. `SdkProject` is a read-only snapshot; `ProjectEditSession` is the only mutation and commit boundary. The [SDK architecture RFC](../../../docs/rfc/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md) owns the rationale. +Shared project domain and infrastructure for `create-sdk` and `dsh-sdk config`. `SdkProject` is a read-only snapshot; `ProjectEditSession` is the only mutation and commit boundary. The [SDK architecture Agent Note](../../../.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md) owns the rationale. The package owns the builtin typed-spec catalog, provider/app behavior entities, structured project file objects, helper-owned project templates, the shared typed `TextTemplate` renderer, package-manager strategies, local-plugin blueprints, typed questions, and the clack prompt adapter. It never boots a Cordis application. diff --git a/packages/session-persistence/README.md b/packages/session-persistence/README.md index 603e525ee0..6435a4bea7 100644 --- a/packages/session-persistence/README.md +++ b/packages/session-persistence/README.md @@ -8,4 +8,4 @@ The durable session-persistence seam and its storage backends. The interface pac | `session-persistence-jsonl/` | JSONL-sidecar persistence backend | (registers `ctx.sessionPersistence`) | | `session-persistence-sqlite/` | SQLite persistence backend | (registers `ctx.sessionPersistence`) | -The interface lives at `session-persistence/session-persistence/`; backends are flat siblings. A new storage backend joins here and registers on `ctx.sessionPersistence`. See [session persistence](../../docs/rfc/implemented/architecture/2026-06-14-session-persistence.md). +The interface lives at `session-persistence/session-persistence/`; backends are flat siblings. A new storage backend joins here and registers on `ctx.sessionPersistence`. See [session persistence](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md). diff --git a/packages/session-persistence/session-persistence-jsonl/README.md b/packages/session-persistence/session-persistence-jsonl/README.md index ed7a056c69..cf733b85d4 100644 --- a/packages/session-persistence/session-persistence-jsonl/README.md +++ b/packages/session-persistence/session-persistence-jsonl/README.md @@ -25,7 +25,7 @@ The JSONL durable session-persistence backend — a concrete `SessionPersistence - **Lazy materialization.** `create(meta)` writes nothing; on the first `append`, the backend writes and `fsync`s a temporary file, publishes it without overwrite via a hard link, then `fsync`s the directory when the host supports it. A created-but-never-appended session leaves nothing on disk and is absent from `list`. - **Append-only.** Committed events (at or below a flushed `turn/end`) are never rewritten. Subsequent appends are line appends at EOF + `fsync`. -- **Crash recovery — preserve valid tail work.** `load` keeps the contiguous valid prefix of an interrupted final turn. It truncates from the first unparsable or sequence-gapped uncommitted record, then appends the synthetic tool, step, and turn closers required by the shared [persistence contract](../../../docs/rfc/implemented/architecture/2026-06-14-session-persistence.md); the same defect at or before the last committed `turn/end` rejects. +- **Crash recovery — preserve valid tail work.** `load` keeps the contiguous valid prefix of an interrupted final turn. It truncates from the first unparsable or sequence-gapped uncommitted record, then appends the synthetic tool, step, and turn closers required by the shared [persistence contract](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md); the same defect at or before the last committed `turn/end` rejects. - **Contiguous-seq.** `append` rejects a batch whose first `seq` does not continue the stored log, and rejects non-JSON-serializable `event.data` naming the offending event type. ## Write path diff --git a/packages/session-persistence/session-persistence-sqlite/README.md b/packages/session-persistence/session-persistence-sqlite/README.md index 5eb0338f99..8099aaf9cc 100644 --- a/packages/session-persistence/session-persistence-sqlite/README.md +++ b/packages/session-persistence/session-persistence-sqlite/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-session-persistence-sqlite -A SQLite durable session-persistence backend — a second `SessionPersistence` implementation ([session persistence](../../../docs/rfc/implemented/architecture/2026-06-14-session-persistence.md)), built to validate that the abstract seam and the shared `runPersistenceContract` suite are genuinely backend-agnostic. It satisfies the SAME contract as `dsh-session-persistence-jsonl` (append-only, contiguous-seq, lazy materialization, interrupted-turn close on load), expressed over `node:sqlite` rows instead of file bytes. +A SQLite durable session-persistence backend — a second `SessionPersistence` implementation ([session persistence](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)), built to validate that the abstract seam and the shared `runPersistenceContract` suite are genuinely backend-agnostic. It satisfies the SAME contract as `dsh-session-persistence-jsonl` (append-only, contiguous-seq, lazy materialization, interrupted-turn close on load), expressed over `node:sqlite` rows instead of file bytes. `locate(meta)` returns `undefined`: all sessions share one database, so there is no honest independent per-session transcript path. @@ -8,7 +8,7 @@ A SQLite durable session-persistence backend — a second `SessionPersistence` i ## Storage model -Each `SessionEvent` maps 1:1 onto a row in an `events` table `(session_id, seq, type, time, data, source_event_seqs, surface_op)` — `data` is the event payload as JSON text, so the row shape is the event verbatim (including `assistant/chunk`, keeping `seq` contiguous). The two `TEXT` columns `source_event_seqs` and `surface_op` are nullable; they store the event's optional surface-metadata fields (see [session surface](../../../docs/rfc/implemented/architecture/2026-06-18-session-surface.md)). Out-of-log metadata (`SessionHeader`) lives in a `sessions` row. A `sessions` row is written only by the first `append` — its existence is the lazy-materialization signal (`list` reports exactly the sessions that have a row), so no separate column is needed. +Each `SessionEvent` maps 1:1 onto a row in an `events` table `(session_id, seq, type, time, data, source_event_seqs, surface_op)` — `data` is the event payload as JSON text, so the row shape is the event verbatim (including `assistant/chunk`, keeping `seq` contiguous). The two `TEXT` columns `source_event_seqs` and `surface_op` are nullable; they store the event's optional surface-metadata fields (see [session surface](../../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md)). Out-of-log metadata (`SessionHeader`) lives in a `sessions` row. A `sessions` row is written only by the first `append` — its existence is the lazy-materialization signal (`list` reports exactly the sessions that have a row), so no separate column is needed. The repository's Node range supports unflagged `node:sqlite`. The database enables foreign keys and uses the configured journal mode (`wal` by default; use a rollback mode where WAL shared-memory files are unsuitable). `PRAGMA user_version` stores the table-layout version; databases with any other version are rejected because this unreleased format has no migrations. @@ -18,7 +18,7 @@ On filesystems with POSIX modes, the backend requests mode `0700` for missing di - **Append = a transaction.** `append` runs `BEGIN`/`COMMIT` around the batch: it materializes the `sessions` row (if still lazy) and INSERTs every event, asserting the contiguous-seq contract first (the first event's `seq` must equal the stored next-seq). A mid-batch failure (a UNIQUE violation on a duplicated seq) rolls back entirely, so the stored log and the in-memory cursor stay consistent. (`load()` already balanced the stored log, so `append` never has to repair a crash tail.) - **Lazy materialization.** `create()` records intent in memory only — no row is written until the first `append`. A created-but-never-appended session has no `sessions` row, so it is absent from `list()` (which reports exactly the sessions that have a row). -- **Interrupted-turn close on load.** `load()` implements the shared [crash-recovery contract](../../../docs/rfc/implemented/architecture/2026-06-14-session-persistence.md): preserve the valid interrupted turn, append its synthetic closing events in one transaction, and remove only a torn tail row. Committed parse errors or sequence gaps make the session unloadable. Because recovery mutates stored rows, the next append starts from a balanced log and accurate cursor. +- **Interrupted-turn close on load.** `load()` implements the shared [crash-recovery contract](../../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md): preserve the valid interrupted turn, append its synthetic closing events in one transaction, and remove only a torn tail row. Committed parse errors or sequence gaps make the session unloadable. Because recovery mutates stored rows, the next append starts from a balanced log and accurate cursor. ## Configuration (schemastery) diff --git a/packages/session-persistence/session-persistence/README.md b/packages/session-persistence/session-persistence/README.md index a8c478d941..ca04cd1ab8 100644 --- a/packages/session-persistence/session-persistence/README.md +++ b/packages/session-persistence/session-persistence/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-session-persistence -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](../../../docs/rfc/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 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. @@ -23,7 +23,7 @@ The persisted unit IS the existing `SessionEvent` (event-sourced model — the l ## The write coordinator -`PersistenceCoordinator` owns per-id state, write-behind buffers and serialization, the `session/event` → `session/flush` drain, lazy materialization, crash-tail repair, session adoption, and quiescent disposal. A first-party backend composes one, implements the small `PersistenceBackend` storage hook interface, and delegates its four public service methods. JSONL and SQLite therefore share lifecycle correctness while retaining different storage primitives; see the [coordinator RFC](../../../docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md). +`PersistenceCoordinator` owns per-id state, write-behind buffers and serialization, the `session/event` → `session/flush` drain, lazy materialization, crash-tail repair, session adoption, and quiescent disposal. A first-party backend composes one, implements the small `PersistenceBackend` storage hook interface, and delegates its four public service methods. JSONL and SQLite therefore share lifecycle correctness while retaining different storage primitives; see the [coordinator Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md). When a live session emits `session/disposed`, the coordinator waits for its initialization, serializes a final buffer drain, then releases every map entry owned by that exact `Session` object. A failed final drain keeps the pending buffer for backend teardown to retry. Backend teardown stops event admission first, awaits all in-flight session retirements and remaining per-id operations, drains any retained buffers, and only then closes the storage handle. @@ -41,7 +41,7 @@ The `PersistenceBackend<TornMarker>` hooks (the only seam between the coordinato | `list()` | List all stored metadata. | | `close?()` | Optional lifecycle teardown (e.g. close a db handle), awaited after the dispose drain. | -The `tornMarker` is fully OPAQUE: the coordinator only tests `!== undefined` and round-trips it to `commitRepair`, never inspecting its value (the JSONL backend uses the byte offset to truncate to, the SQLite backend the seq to delete from). The public `SessionPersistence` service shape is unchanged, so a third-party backend MAY still implement the abstract service directly without the coordinator. See [the write-coordinator RFC](../../../docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md). +The `tornMarker` is fully OPAQUE: the coordinator only tests `!== undefined` and round-trips it to `commitRepair`, never inspecting its value (the JSONL backend uses the byte offset to truncate to, the SQLite backend the seq to delete from). The public `SessionPersistence` service shape is unchanged, so a third-party backend MAY still implement the abstract service directly without the coordinator. See [the write-coordinator Agent Note](../../../.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md). ## Testing backends diff --git a/packages/session-query/session-query/README.md b/packages/session-query/session-query/README.md index 1a5ed99949..76fc5eff80 100644 --- a/packages/session-query/session-query/README.md +++ b/packages/session-query/session-query/README.md @@ -33,4 +33,4 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work - **No caller authorization** — this is trusted context-wide infrastructure; a future model tool or UI must constrain which sessions its caller may inspect. -- **No search or extraction** — filters, extraction registry, search-provider protocol, index synchronization, and a model-facing tool are absent. The [tracing decision](../../../docs/rfc/implemented/feature/2026-07-13-session-query-tracing.md) owns relationship semantics; content-bearing full-text-search results and their chainable filters belong beside their first implementation in the proposed [SQLite package](../../../docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.md). +- **No search or extraction** — filters, extraction registry, search-provider protocol, index synchronization, and a model-facing tool are absent. The [tracing decision](../../../.agents/notes/implemented/feature/2026-07-13-session-query-tracing.md) owns relationship semantics; content-bearing full-text-search results and their chainable filters belong beside their first implementation in the proposed [SQLite package](../../../.agents/notes/proposed/feature/2026-07-10-sqlite-session-query-provider.md). diff --git a/packages/skill/tool-skill/README.md b/packages/skill/tool-skill/README.md index 4d46ad0b75..89a6e843ab 100644 --- a/packages/skill/tool-skill/README.md +++ b/packages/skill/tool-skill/README.md @@ -8,7 +8,7 @@ Requires `ctx.tools` and `ctx.skills` (`inject: ['tools', 'skills']`). The plugin contributes one user-role `<system-reminder>` catalog through `agent/session-prefix`. It resolves skills for the calling session's cwd, forwards the prefix abort signal to discovery, and lists only sorted `name` and `description` entries; skill bodies, paths, sources, providers, and `whenToUse` hints remain outside the catalog. The catalog is omitted when no model-invocable skills are available, and also when that agent's tool view restricts away the shipped `skill` tool or resolves a same-name scoped shadow instead. This exact-definition check keeps prompt guidance, the model-visible schema, and executable dispatch aligned. -`catalogDescriptionMaxLength` controls normalized, XML-escaped catalog descriptions. Its default is `500` and values must be integers of at least `3`, which reserves room for a truncation ellipsis. The [session-prefix RFC](../../../docs/rfc/implemented/feature/2026-07-07-session-prefix.md) defines the request-only, header-logged lifecycle of this message. +`catalogDescriptionMaxLength` controls normalized, XML-escaped catalog descriptions. Its default is `500` and values must be integers of at least `3`, which reserves room for a truncation ellipsis. The [session-prefix Agent Note](../../../.agents/notes/implemented/feature/2026-07-07-session-prefix.md) defines the request-only, header-logged lifecycle of this message. ## Tool: `skill` diff --git a/packages/spill/README.md b/packages/spill/README.md index 7d54c91eb5..c7b59adf74 100644 --- a/packages/spill/README.md +++ b/packages/spill/README.md @@ -10,4 +10,4 @@ The tool-output spill capability seam: an abstract storage interface, a local fi The interface lives at `spill/spill/`. The split mirrors bash/fs: the seam owns storage only, `spill-local` owns the filesystem mechanics, and `spill-policy` owns WHEN to spill and the model-facing notice. Preview mechanics stay in [`util/retention`](../util/README.md) — the policy composes the two without either owning the other's job. -See the [tool output spill RFC](../../docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md) for the design rationale, including why final-result spill is separate from tool-owned early spill (bash streams, subagent rollouts) and why creation belongs to the runtime spill seam rather than the model-facing `write` tool. +See the [tool output spill Agent Note](../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md) for the design rationale, including why final-result spill is separate from tool-owned early spill (bash streams, subagent rollouts) and why creation belongs to the runtime spill seam rather than the model-facing `write` tool. diff --git a/packages/spill/spill-local/README.md b/packages/spill/spill-local/README.md index 07199c5caa..cef794b548 100644 --- a/packages/spill/spill-local/README.md +++ b/packages/spill/spill-local/README.md @@ -16,7 +16,7 @@ Files land at `<root>/session-<hash>/​<random>-<safeName>`: |---|---|---| | `root` | private 0700 temp dir | Root directory for spill files. Set to keep them under a known location. | -`saveText` rejects on a real storage failure (permissions, ENOSPC); the spill policy treats a rejection as best-effort and keeps the inline result. See the seam README for the vocabulary and the [tool output spill RFC](../../../docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md) for the design. +`saveText` rejects on a real storage failure (permissions, ENOSPC); the spill policy treats a rejection as best-effort and keeps the inline result. See the seam README for the vocabulary and the [tool output spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md) for the design. ## Model Experience diff --git a/packages/spill/spill-policy/README.md b/packages/spill/spill-policy/README.md index 64625c61ef..936f254d6a 100644 --- a/packages/spill/spill-policy/README.md +++ b/packages/spill/spill-policy/README.md @@ -30,7 +30,7 @@ This plugin registers **no service** and owns no storage or preview mechanics: p ## Scope -The policy sees only the FINAL formatted tool result — not a tool's internal resource. If a provider already truncated (e.g. `web-fetch-local.maxBodyChars`), the spill artifact holds the full formatted result the tool returned, not the full original source. Provider/resource caps stay mandatory and separate. Tool-owned early spill (bash streams, subagent rollouts) is future work — see the [tool output spill RFC](../../../docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md). +The policy sees only the FINAL formatted tool result — not a tool's internal resource. If a provider already truncated (e.g. `web-fetch-local.maxBodyChars`), the spill artifact holds the full formatted result the tool returned, not the full original source. Provider/resource caps stay mandatory and separate. Tool-owned early spill (bash streams, subagent rollouts) is future work — see the [tool output spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md). ## Model Experience diff --git a/packages/spill/spill/README.md b/packages/spill/spill/README.md index c6a26cfddc..8e64e72608 100644 --- a/packages/spill/spill/README.md +++ b/packages/spill/spill/README.md @@ -24,7 +24,7 @@ Storage is grouped by the request's `owner` session as a save-time namespace; th `SaveTextSpill` (owner, source, suggestedName, content) is the request; `SpillRef` (locator, bytes, retrievalHint) is the result. `SpillLocator` is [branded](../../util/brand) and rendered to the model as an opaque string — a local path for `dsh-spill-local`, but a future backend may return a URI, key, or command token without changing policy/tool consumers. `SpillOwner.sessionId` is the save-time storage namespace: forked sessions inherit existing locators from the seeded log without copying or re-owning them, and new spills after the fork use the child session id. `SpillSource` (toolName, callId, label) is descriptive provenance for backend naming and inspection, not access control. See `src/types.ts` for the full contracts. -See the [tool output spill RFC](../../../docs/rfc/implemented/architecture/2026-07-08-tool-output-spill-files.md) for the design rationale, including why creation belongs to the runtime spill seam rather than the model-facing `write` tool. +See the [tool output spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md) for the design rationale, including why creation belongs to the runtime spill seam rather than the model-facing `write` tool. ## Model Experience diff --git a/packages/subagent/README.md b/packages/subagent/README.md index 9935233e85..ccc6ab9cba 100644 --- a/packages/subagent/README.md +++ b/packages/subagent/README.md @@ -1,6 +1,6 @@ # subagent/ — subagent capability family -The subagent seam: an agent delegating work to a child agent. Like the [bash](../bash/README.md) and [llm](../llm/README.md) families this is a capability seam (see [capability seams](../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)) — but with one defining difference: **multiple provider implementations coexist in one context**, registered by name, rather than the single-implementation bash shape. The registry mirrors the LLM adapter registry. +The subagent seam: an agent delegating work to a child agent. Like the [bash](../bash/README.md) and [llm](../llm/README.md) families this is a capability seam (see [capability seams](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)) — but with one defining difference: **multiple provider implementations coexist in one context**, registered by name, rather than the single-implementation bash shape. The registry mirrors the LLM adapter registry. | Package | Role | ctx key | |---|---|---| @@ -14,4 +14,4 @@ The subagent seam: an agent delegating work to a child agent. Like the [bash](.. The interface lives at `subagent/subagent/`. The in-process `subagent-spawn` / `subagent-fork` backends share the `subagent-inprocess` driver (a library with no provider of its own — both depend on it, neither on the other), and the out-of-process `subagent-acp` backend builds on the `subagent-subprocess` library (the credential env scrub, the dispose ladder, isolated config dirs). Tests replace only the child boundary with package-local fixtures. -The proposal and design rationale: [docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md](../../docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md). +The proposal and design rationale: [.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md](../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md). diff --git a/packages/subagent/subagent-acp/README.md b/packages/subagent/subagent-acp/README.md index 0ce06fca2e..7ac583575b 100644 --- a/packages/subagent/subagent-acp/README.md +++ b/packages/subagent/subagent-acp/README.md @@ -91,7 +91,7 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work -- **A fresh process per run** — persistent-process pooling is a future optimization ([the seam RFC](../../../docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md)). +- **A fresh process per run** — persistent-process pooling is a future optimization ([the seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md)). - **No optional start-time capabilities** — this provider cannot apply the local harness's `outputSchema`, depth cap, tool filter, or persona inside the remote process, so it advertises none and the service rejects requests that require them. - **Only `agent_message_chunk` text is collected** — the child's tool-call activity, thought chunks, and plan updates are not surfaced to the parent. - **Permission prompts are auto-answered** (`permission: allow | reject`) — no human is surfaced a child's `session/request_permission` in this cut. diff --git a/packages/subagent/subagent-subprocess/README.md b/packages/subagent/subagent-subprocess/README.md index 5ca9720f00..dd1784672e 100644 --- a/packages/subagent/subagent-subprocess/README.md +++ b/packages/subagent/subagent-subprocess/README.md @@ -1,6 +1,6 @@ # @deepseek-ai/dsh-subagent-subprocess -Shared machinery for **out-of-process subagent backends** — providers that spawn an external agent as a child process, such as the [ACP backend](../subagent-acp/README.md). A pure library (no provider, no registration, no Config): what every spawn-a-CLI-child backend needs to keep the parent deployment's credentials out of the child, tear the child down to quiescence, and isolate it from the host user's on-disk CLI state. Design rationale: [the Claude Code / Codex subagent backends RFC](../../../docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md). +Shared machinery for **out-of-process subagent backends** — providers that spawn an external agent as a child process, such as the [ACP backend](../subagent-acp/README.md). A pure library (no provider, no registration, no Config): what every spawn-a-CLI-child backend needs to keep the parent deployment's credentials out of the child, tear the child down to quiescence, and isolate it from the host user's on-disk CLI state. Design rationale: [the Claude Code / Codex subagent backends Agent Note](../../../.agents/notes/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md). Every tunable is a **parameter**: the dispose ladder takes its grace periods per call, the config-dir helper takes an optional pinned path. Defaults live in each consuming plugin's Config (defaulted, validated fields changeable from `cordis.yml`), never in this library. diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index c810a1f6ce..5097ad914a 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -60,7 +60,7 @@ Provider additions and removals also emit `subagent/provider-added` and `subagen ## Collection model -The model-facing tool collects synchronously by default: it awaits the child result and disposes the run before returning. Background delegation does not change this seam; the consumer registers startup and the eventual run with the generic `ctx.tasks` runtime, then collection and cancellation use the shared task tools. See the [background subagent tasks RFC](../../../docs/rfc/implemented/feature/2026-07-08-background-subagent-tasks.md), the [capability-seam RFC](../../../docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.md), and `src/types.ts` for the complete contracts. +The model-facing tool collects synchronously by default: it awaits the child result and disposes the run before returning. Background delegation does not change this seam; the consumer registers startup and the eventual run with the generic `ctx.tasks` runtime, then collection and cancellation use the shared task tools. See the [background subagent tasks Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md), and `src/types.ts` for the complete contracts. ## Model Experience diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 8d382b6ce4..6355208a4d 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -8,9 +8,9 @@ Each plugin instance binds one `provider` to one `toolName`; the model receives A foreground call passes the execution signal through startup and execution, awaits `run.result`, and always awaits `run.dispose()` before returning. Only `completed` returns final text; abort, refusal, token limit, and other failures become errored tool results without partial output. -With `run_in_background: true`, the tool registers the parent-owned task before starting the provider. A task-owned signal covers pending startup and the child after the starting call returns. `task_kill` and owner disposal abort it. Settlement awaits startup rollback or child disposal, then maps completed final text, abort to `killed`, and other failures to `failed`. The task has no incremental read; generic task tools own later status, collection, cancellation, and notices. See the [background subagent RFC](../../../docs/rfc/implemented/feature/2026-07-08-background-subagent-tasks.md). +With `run_in_background: true`, the tool registers the parent-owned task before starting the provider. A task-owned signal covers pending startup and the child after the starting call returns. `task_kill` and owner disposal abort it. Settlement awaits startup rollback or child disposal, then maps completed final text, abort to `killed`, and other failures to `failed`. The task has no incremental read; generic task tools own later status, collection, cancellation, and notices. See the [background subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md). -`toolFilter` changes the child's global tool layer but is not a parent-derived authority ceiling. See the [agent-scope security non-goal](../../../docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals). +`toolFilter` changes the child's global tool layer but is not a parent-derived authority ceiling. See the [agent-scope security non-goal](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals). ## Config @@ -26,7 +26,7 @@ With `run_in_background: true`, the tool registers the parent-owned task before ## Concurrency -Foreground and background calls are exclusive. Children may share the parent's workspace or external resources, and a unary classifier cannot prove that sibling delegations have disjoint effects. See the [parallel tool-call RFC](../../../docs/rfc/implemented/feature/2026-07-10-parallel-tool-call-execution.md). +Foreground and background calls are exclusive. Children may share the parent's workspace or external resources, and a unary classifier cannot prove that sibling delegations have disjoint effects. See the [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md). ## Model Experience diff --git a/packages/support/acp-snapshot/README.md b/packages/support/acp-snapshot/README.md index 2f55ebdfaa..231ba0d6e8 100644 --- a/packages/support/acp-snapshot/README.md +++ b/packages/support/acp-snapshot/README.md @@ -6,7 +6,7 @@ Four layers, importable separately: - **`launchAcpTestAgent` (launcher)** — boots an unbuilt ACP agent from a temp cwd, pins tsx to the repo tsconfig, connects the SDK client over a raw-byte stdout tee, collects session updates and stderr, surfaces asynchronous spawn failures through its startup lifecycle, fails closed on unhandled permission requests, and owns graceful or signalled shutdown. Shutdown waits for process exit, inherited stdio closure, and ACP parser exhaustion before resolving or propagating a child error, so captures are complete and callers can remove owned paths after either outcome. Snapshot and ordinary e2e suites share this process boundary; a test supplies only agent paths, cwd, environment overrides, and any permission policy. - **`runScenario` (harness)** — boots the real agent bin as a subprocess via tsx (unbuilt, Loader path), drives it over ACP JSON-RPC stdio from a deterministic `input.json` script, tees raw stdout for the expected-output and purity checks, and harvests every persisted session JSONL (parent + subagent children, primary-first) after a graceful stdin-EOF shutdown. Parameterized by `AgentUnderTest` (`binScript`, `configPath`, `tsconfigPath` — absolute paths; the subprocess cwd is a temp dir outside the repo). Startup failures preserve captured agent stderr in the rejected diagnostic. -- **Normalizers** — pure functions turning the two captured surfaces into stable text: `normalizeStdout` (JSON-RPC ids → first-seen sequence; UUIDs/cwd → tokens; doubles as the stdout-purity check), `normalizeSessionLog` (times zeroed, `seq` kept), `scrubSystemPrompts` (prompt text → `{{system}}`), `scrubToolSchemas` (schema bulk → `{{tools}}`), and `scrubRequestHeaders` (all header bulk → `{{system}}`/`{{tools}}`/`{{messagePrefix}}` outside each pin, structure kept — [pinned-header RFC](../../../docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)). +- **Normalizers** — pure functions turning the two captured surfaces into stable text: `normalizeStdout` (JSON-RPC ids → first-seen sequence; UUIDs/cwd → tokens; doubles as the stdout-purity check), `normalizeSessionLog` (times zeroed, `seq` kept), `scrubSystemPrompts` (prompt text → `{{system}}`), `scrubToolSchemas` (schema bulk → `{{tools}}`), and `scrubRequestHeaders` (all header bulk → `{{system}}`/`{{tools}}`/`{{messagePrefix}}` outside each pin, structure kept — [pinned-header Agent Note](../../../.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)). - **`defineAcpSnapshotSuite` (factory)** — registers the whole describe/it tree for a scenario table: per-scenario expected-output and re-persisted-log comparisons, record/refresh fixture write-back, rejection of structured `UNKNOWN_TOOL` results, the per-header-class pin (`system-prompt.expected.md` plus `tool-schemas.expected.json`) with its live uniformity guard, and the fixture guard block (no orphan scenario dirs, required files present, exactly one pin per class, every JSONL prompt/schema-scrubbed, non-pinning fixtures fully header-scrubbed). Each scenario directory's `session.jsonl` plus contiguous `session.<n>.jsonl` siblings are the ordered primary/child inventory; the scenario table does not duplicate their count. Must be called at vitest collection time. A consuming `*.snapshot.ts` is the scenario table plus one factory call: @@ -38,7 +38,7 @@ defineAcpSnapshotSuite({ A scenario booting a differently-composed tree sets its own `configPath` (an overlay whose basename still ends in `cordis.yml`, so the bin's replay swap finds the sibling `*cordis.snapshot.yml`) and, when that composition changes the request header, its own `headerClass` with its own pinning scenario — the acp-agent example's Code Mode and filesystem scenarios are templates. Each pinning directory stores the normalized full prompt sequence in generated `system-prompt.expected.md` and the corresponding full tool-schema sequence in generated `tool-schemas.expected.json`; `session.jsonl` stores `"system":"{{system}}","tools":"{{tools}}"` while retaining config, reason, and any model-visible prefix. A pin with legitimate mid-run header changes declares `expectedHeaderChanges`, which fixes the length of both sidecar sequences. -The example also ships a `cordis.snapshot.yml` replay overlay next to its `cordis.yml` (the bin swaps them under `DSH_SNAPSHOT=replay` — [single-source replay config RFC](../../../docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.md)); replay fixtures are served by [`dsh-llm-replay`](../llm-replay/README.md), which this package points at via the `DSH_SNAPSHOT_*` env vars it sets on the child. `pnpm run test:snapshot:record` calls the live LLM and rewrites the recorded scenarios' model fixtures; `pnpm run test:snapshot:refresh` stays keyless, runs the replay overlay, and rewrites stdout, comparable session-log expected outputs, and each pin's prompt and tool-schema sidecars from the committed model scripts. Fixture roles, record/replay/refresh semantics, and scenario-table fields are documented on `Scenario` and in the [snapshot RFC](../../../docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md). +The example also ships a `cordis.snapshot.yml` replay overlay next to its `cordis.yml` (the bin swaps them under `DSH_SNAPSHOT=replay` — [single-source replay config Agent Note](../../../.agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md)); replay fixtures are served by [`dsh-llm-replay`](../llm-replay/README.md), which this package points at via the `DSH_SNAPSHOT_*` env vars it sets on the child. `pnpm run test:snapshot:record` calls the live LLM and rewrites the recorded scenarios' model fixtures; `pnpm run test:snapshot:refresh` stays keyless, runs the replay overlay, and rewrites stdout, comparable session-log expected outputs, and each pin's prompt and tool-schema sidecars from the committed model scripts. Fixture roles, record/replay/refresh semantics, and scenario-table fields are documented on `Scenario` and in the [snapshot Agent Note](../../../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md). Constraints: `suite.ts` imports vitest, so the package entry is importable only inside a vitest run (the launcher, harness, and normalizers have no such dependency but ship from the same entry). ACP-specific by design — the launcher speaks the SDK's `ClientSideConnection`. Permission round-trips are scriptable: `InputScript.permissionAnswers` is a FIFO queue of option-kind selections (`allow_once`, `reject_once`, …) the client maps to the agent-issued `optionId` at answer time; an absent or exhausted queue answers `cancelled`, and a kind the request never offered rejects the run (the agent is answered `cancelled`, so a tolerant agent cannot absorb the scenario bug). Session config options are scriptable too: the `setConfigOption` step switches a knob over `session/set_config_option`, and `setConfigOptionExpectError` asserts the bridge rejects an unknown id or out-of-vocabulary value (the error frame stays in the transcript). diff --git a/packages/support/acp-snapshot/src/harness.ts b/packages/support/acp-snapshot/src/harness.ts index 81e02da6a3..50d7763609 100644 --- a/packages/support/acp-snapshot/src/harness.ts +++ b/packages/support/acp-snapshot/src/harness.ts @@ -11,7 +11,7 @@ * shutdown flush. The pure normalizers in ./normalize.ts turn the captured * stdout frames and the session-log events into stable, snapshot-able text. * - * See docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md. + * See .agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md. * * @module @deepseek-ai/dsh-acp-snapshot/harness */ diff --git a/packages/support/invariants/README.md b/packages/support/invariants/README.md index bda9524bbe..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): @@ -40,13 +39,13 @@ Agent status (per agent): Model requests (on `llm/stream`): -- **a loop-built request is exactly what the log reconstructs** — a frozen request with a live `sessionId` (the loop-built marker; hand-built one-shots like compaction's summarize are unfrozen and skipped) must carry frozen `messages` deep-equal to the derivation over the log prefix strictly before the in-flight step's `step/start` (rebuilt through a FRESH `Session`, so the live cache cannot vouch for itself — and boundary-correct: content logged after `step/start` legitimately belongs to the next request), and every non-content field must equal the latest logged `request/header` (see [the reconstructability RFC](../../../docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)). Registered with `prepend: true` so a short-circuiting `llm/stream` listener (the replay adapter) cannot silence it; prepend orders it against append-registered listeners only — correctness rests on the seq-bounded rebuild, never listener timing. +- **a loop-built request is exactly what the log reconstructs** — a frozen request with a live `sessionId` (the loop-built marker; hand-built one-shots like compaction's summarize are unfrozen and skipped) must carry frozen `messages` deep-equal to the derivation over the log prefix strictly before the in-flight step's `step/start` (rebuilt through a FRESH `Session`, so the live cache cannot vouch for itself — and boundary-correct: content logged after `step/start` legitimately belongs to the next request), and every non-content field must equal the latest logged `request/header` (see [the reconstructability Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)). Registered with `prepend: true` so a short-circuiting `llm/stream` listener (the replay adapter) cannot silence it; prepend orders it against append-registered listeners only — correctness rests on the seq-bounded rebuild, never listener timing. On any violation it throws `InvariantError` (`code: 'INVARIANT'`). ## Why runtime assertions remain useful -Session enforces the per-record storage boundary at runtime, where a cast cannot bypass it. Pervasive `DeepReadonly<SessionEvent>` types would add noise across consumers without expressing relationships such as turn/step nesting, subject-correct scoped dispatch, or equality between a request and its log reconstruction. This plugin checks those relationships wherever it is mounted while `dsh-session` keeps history immutable in every composition. See [source-owned session immutability and dev-mode invariants](../../../docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md). +Session enforces the per-record storage boundary at runtime, where a cast cannot bypass it. Pervasive `DeepReadonly<SessionEvent>` types would add noise across consumers without expressing relationships such as turn/step nesting, subject-correct scoped dispatch, or equality between a request and its log reconstruction. This plugin checks those relationships wherever it is mounted while `dsh-session` keeps history immutable in every composition. See [source-owned session immutability and dev-mode invariants](../../../.agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md). ## Seeded sessions diff --git a/packages/support/invariants/src/index.ts b/packages/support/invariants/src/index.ts index cdb06edbac..91b4da4566 100644 --- a/packages/support/invariants/src/index.ts +++ b/packages/support/invariants/src/index.ts @@ -86,7 +86,7 @@ function validateEvent(trace: SessionTrace, event: SessionEvent): SessionTraceTr // Boundary/step-scoped events have explicit cases; every OTHER event type — // including plugin-added (merge-extensible) SessionEventMap keys — is caught - // by the `default` and must be turn-enclosed (the turn-enclosure RFC). No assertNever: an + // by the `default` and must be turn-enclosed (the turn-enclosure Agent Note). No assertNever: an // unknown variant is valid, not a compile error. switch (event.type) { case 'turn/start': { @@ -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 @@ -162,7 +172,7 @@ function validateEvent(trace: SessionTrace, event: SessionEvent): SessionTraceTr pendingCalls = { kind: 'delete', callId: event.data.callId } break } - // Turn-enclosure (the turn-enclosure RFC): EVERY session event not handled by a boundary + // Turn-enclosure (the turn-enclosure Agent Note): EVERY session event not handled by a boundary // case above must sit inside an open turn. The durable session log uses the // turn as its commit/replay boundary (the JSONL backend treats anything // after the last turn/end as a crash tail), so a bare event between turns is @@ -330,7 +340,7 @@ export function apply(ctx: Context): void { } }, { global: true }) - // Request-reconstruction cross-check (the reconstructability RFC): a + // Request-reconstruction cross-check (the reconstructability Agent Note): a // loop-built request — frozen envelope + live sessionId is the marker; a // hand-built one-shot (compaction summarize) is unfrozen and skipped — must // be EXACTLY what the session log reconstructs: diff --git a/packages/support/invariants/tests/invariants.spec.ts b/packages/support/invariants/tests/invariants.spec.ts index de0046c00e..d2c21fe020 100644 --- a/packages/support/invariants/tests/invariants.spec.ts +++ b/packages/support/invariants/tests/invariants.spec.ts @@ -149,7 +149,7 @@ describe('session-log invariants', () => { it('rejects a message event appended outside any open turn (turn-enclosure)', async () => { const { ctx } = await setup() const session = ctx.sessions.create() - // No turn open: every message-bearing event must be turn-enclosed (the turn-enclosure RFC). + // No turn open: every message-bearing event must be turn-enclosed (the turn-enclosure Agent Note). expect(() => session.append('user/message', { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, { surfaceOp: 'append' })) .toThrow(/outside any open turn/) expect(() => session.append('context/message', { content: [{ type: 'text', text: 'ctx' }], source: { kind: 'user' } }, { surfaceOp: 'append' })) @@ -160,7 +160,7 @@ describe('session-log invariants', () => { const { ctx } = await setup() const session = ctx.sessions.create() // steering/message is turn-scoped: outside a turn it would land past the - // commit boundary and be dropped on resume (the turn-enclosure RFC). + // commit boundary and be dropped on resume (the turn-enclosure Agent Note). expect(() => session.append('steering/message', { turn: 1, content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }, { surfaceOp: 'append' })) .toThrow(/outside any open turn/) // A PLUGIN-added (merge-extensible) event type is caught by the default too. @@ -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/tasks/README.md b/packages/tasks/README.md index 12e8f41a59..71c68ea250 100644 --- a/packages/tasks/README.md +++ b/packages/tasks/README.md @@ -1,6 +1,6 @@ # tasks/ — background task capability family -The shared home for background-task ids, owner isolation, reads, cancellation, waiting, and completion notices. Bash, subagents, and future long-running tools use one model-facing protocol. See the [background-task runtime RFC](../../docs/rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md). +The shared home for background-task ids, owner isolation, reads, cancellation, waiting, and completion notices. Bash, subagents, and future long-running tools use one model-facing protocol. See the [background-task runtime Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md). | Package | ctx key | Role | |---|---|---| diff --git a/packages/tasks/tasks/README.md b/packages/tasks/tasks/README.md index afe458c711..37d342e0fe 100644 --- a/packages/tasks/tasks/README.md +++ b/packages/tasks/tasks/README.md @@ -20,7 +20,7 @@ Tasks belong to their owner and backend, not the producer tool fiber, so produce Service disposal closes listeners, cancels all live tasks, awaits their records, and detaches effects from surviving owner scopes. If teardown cancellation throws, the service force-fails the record and warns that work may be orphaned instead of deadlocking. A cancellation that returns but never settles `done` remains indistinguishable from a slow stop and can stall teardown. -See the [task type catalog](../../../docs/core-data-structures/tasks.md) and [runtime RFC](../../../docs/rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md). +See the [task type catalog](../../../docs/core-data-structures/tasks.md) and [runtime Agent Note](../../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md). ## Model Experience diff --git a/packages/timeout/README.md b/packages/timeout/README.md index 36f52abaf3..321f21373c 100644 --- a/packages/timeout/README.md +++ b/packages/timeout/README.md @@ -6,4 +6,4 @@ The tool-call timeout policy plugin. A single **product** package: it is a deplo |---|---|---| | `timeout-policy/` | A `tools/execute` wrapper: for each configured tool it arms a per-call deadline on `exec.signal` and returns a structured `TOOL_TIMEOUT` result when that deadline wins | (registers a `tools/execute` listener; injects nothing) | -Timeout is split across three layers: [`dsh-timeout`](../util/timeout) owns the pure timing/classification primitive (`deadline`/`timeoutOf`), each capability owns termination (bash kills its process group, the fetch provider tears down its socket), and this package owns the *model-facing tool-call budget as deployment policy* — no model-facing timeout argument, no global default. It is the middleware the [timeout-library RFC](../../docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md) foresaw. `bash` and hook command execution keep their own `BASH_TIMEOUT` backend timeout and do not route through this policy. +Timeout is split across three layers: [`dsh-timeout`](../util/timeout) owns the pure timing/classification primitive (`deadline`/`timeoutOf`), each capability owns termination (bash kills its process group, the fetch provider tears down its socket), and this package owns the *model-facing tool-call budget as deployment policy* — no model-facing timeout argument, no global default. It is the middleware the [timeout-library Agent Note](../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md) foresaw. `bash` and hook command execution keep their own `BASH_TIMEOUT` backend timeout and do not route through this policy. diff --git a/packages/timeout/timeout-policy/README.md b/packages/timeout/timeout-policy/README.md index bb73090ef7..60b590a7f8 100644 --- a/packages/timeout/timeout-policy/README.md +++ b/packages/timeout/timeout-policy/README.md @@ -1,6 +1,6 @@ # dsh-timeout-policy -Tool-call timeout enforcer: a single `tools/execute` around-dispatch listener that arms a per-call cooperative deadline on `exec.signal` for a tool declaring `timeoutMs` on its `ToolDefinition` and returns a structured `TOOL_TIMEOUT` result when that deadline wins. The budget is read from the tool's own declaration (`ToolDefinition.timeoutMs`, set by the owning tool plugin), so this plugin is **zero-config**. It is the reference `tools/execute` wrapper and the enforcement home for model-facing tool-call budgets (the timeout-library RFC's foreseen middleware). +Tool-call timeout enforcer: a single `tools/execute` around-dispatch listener that arms a per-call cooperative deadline on `exec.signal` for a tool declaring `timeoutMs` on its `ToolDefinition` and returns a structured `TOOL_TIMEOUT` result when that deadline wins. The budget is read from the tool's own declaration (`ToolDefinition.timeoutMs`, set by the owning tool plugin), so this plugin is **zero-config**. It is the reference `tools/execute` wrapper and the enforcement home for model-facing tool-call budgets (the timeout-library Agent Note's foreseen middleware). ## Plugin (namespace: `timeout-policy`) diff --git a/packages/todo/tool-todo/README.md b/packages/todo/tool-todo/README.md index 45220706eb..6323d86247 100644 --- a/packages/todo/tool-todo/README.md +++ b/packages/todo/tool-todo/README.md @@ -10,7 +10,7 @@ Registers one tool, `todo_write(todos: [{ content, status }])`, on `ctx.tools`. ## Single owner -The list belongs to the ONE agent session that called the tool. There is no subagent/shared/swarm scope: a non-agent caller (no `exec.agent`) has nowhere to write the list and is rejected. This is a deliberate scope limit — see the RFC. +The list belongs to the ONE agent session that called the tool. There is no subagent/shared/swarm scope: a non-agent caller (no `exec.agent`) has nowhere to write the list and is rejected. This is a deliberate scope limit — see the Agent Note. ## Validation diff --git a/packages/ui/acp/README.md b/packages/ui/acp/README.md index 8642e33d6b..9841d1ce2e 100644 --- a/packages/ui/acp/README.md +++ b/packages/ui/acp/README.md @@ -2,7 +2,7 @@ Agent Client Protocol bridge over JSON-RPC stdio. Editors can create or resume agents, stream their events, answer questions and approvals, and render tool calls. One connection supports multiple isolated sessions; Zed is the primary compatibility target. -It is a **client-driver / UI plugin**, the structured analogue of the terminal `dsh-tui`/`dsh-stdio` channels — NOT a loop change and NOT a [capability seam](../../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md). It consumes the existing `agent/*` event taxonomy, the `dsh-agent` create/resume factory, and `dsh-session-persistence`. +It is a **client-driver / UI plugin**, the structured analogue of the terminal `dsh-tui`/`dsh-stdio` channels — NOT a loop change and NOT a [capability seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md). It consumes the existing `agent/*` event taxonomy, the `dsh-agent` create/resume factory, and `dsh-session-persistence`. ## Service / plugin @@ -37,13 +37,13 @@ The `initialize` handshake reports a fixed server identity (`agentInfo: { name: ## Multi-session -One id-keyed record map plus exact agent-object checks route every event, prompt, cancel, and approval to one session. Each session permits one in-flight prompt; teardown drains all sessions in parallel. See the [multi-session RFC](../../../docs/rfc/implemented/feature/2026-06-14-acp-multi-session.md). +One id-keyed record map plus exact agent-object checks route every event, prompt, cancel, and approval to one session. Each session permits one in-flight prompt; teardown drains all sessions in parallel. See the [multi-session Agent Note](../../../.agents/notes/implemented/feature/2026-06-14-acp-multi-session.md). ## Session config options The bridge advertises a `model`-category select in `session/new` and `session/load` when the session has a complete target whose provider is registered. Values encode the complete provider/model pair, are grouped by provider when more than one group is available, and come from `ctx.llm.listProviders()` / `listModels()`. The configured or last-requested model is added when absent because catalogs are advisory and private adapters may accept unlisted ids. A selection changes only that ACP session. Agent-scoped prompt assembly snapshots the selected pair for one step, supplies matching `{{provider}}` / `{{model}}` variables, and the `agent/request` waterfall applies the same pair; a concurrent selection therefore takes effect on the next step instead of splitting prompt text from routing. The resulting request header is the durable record restored by `session/load`; a selection never used by a request remains in-memory only. -When `ctx.permission` is composed, the bridge also advertises a `permission` select. Options come from the deployment's preset table; the current value comes from the session fold, with switch-away-only `custom` for unmatched knobs. `session/set_config_option` accepts advertised presets and writes both sandbox-mode and approval-policy events through `PermissionService.set()`. Open-turn switches append immediately; idle switches overlay responses and anchor at the next `agent/prompt-submit`, before request assembly. A crash before anchoring restores the durable fold. See the [model-catalog RFC](../../../docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md), [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md), [`dsh-permission`](../permission/README.md), and [protocol matrix](acp-feature-support.md#6-session-modes--config-options--models). +When `ctx.permission` is composed, the bridge also advertises a `permission` select. Options come from the deployment's preset table; the current value comes from the session fold, with switch-away-only `custom` for unmatched knobs. `session/set_config_option` accepts advertised presets and writes both sandbox-mode and approval-policy events through `PermissionService.set()`. Open-turn switches append immediately; idle switches overlay responses and anchor at the next `agent/prompt-submit`, before request assembly. A crash before anchoring restores the durable fold. See the [model-catalog Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md), [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md), [`dsh-permission`](../permission/README.md), and [protocol matrix](acp-feature-support.md#6-session-modes--config-options--models). The shared [`ctx.tasks` runtime](../../tasks/tasks/) fences access to predictable task ids by the owning session; ACP sessions therefore cannot read or stop one another's background work. @@ -57,7 +57,7 @@ Tools return provider-neutral `generic`, `terminal`, or `diff` render intents fr ## Terminal card (capability-gated) -When the client advertises `_meta.terminal_output`, terminal intents map to Zed's terminal info, output, and exit metadata. The bridge resolves relative cwd against the session, places the description before the terminal block, and omits result content because ACP updates replace call content. Other clients receive a generic card and bridge-derived fenced console fallback. Session creation snapshots the capability so call and result agree. The command still executes through the harness, not ACP terminal creation. See the [terminal-rendering RFC](../../../docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) and [render-intent RFC](../../../docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md). +When the client advertises `_meta.terminal_output`, terminal intents map to Zed's terminal info, output, and exit metadata. The bridge resolves relative cwd against the session, places the description before the terminal block, and omits result content because ACP updates replace call content. Other clients receive a generic card and bridge-derived fenced console fallback. Session creation snapshots the capability so call and result agree. The command still executes through the harness, not ACP terminal creation. See the [terminal-rendering Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) and [render-intent Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md). ## Settle-exactly-once @@ -73,7 +73,7 @@ Disposal and client disconnect share one memoized teardown. It cancels pending p ## stdout is the protocol -The JSON-RPC frames go on stdout, so this plugin MUST run in an example that loads **no stdout logger** (the console logger writes to stdout and would corrupt the frames). The guarantee is config-only — see `examples/acp-agent` (no console logger) and [ACP support risks](../../../docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.md#risks). A stderr exporter is fine for logging. +The JSON-RPC frames go on stdout, so this plugin MUST run in an example that loads **no stdout logger** (the console logger writes to stdout and would corrupt the frames). The guarantee is config-only — see `examples/acp-agent` (no console logger) and [ACP support risks](../../../.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md#risks). A stderr exporter is fine for logging. ## Running @@ -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 @@ -166,5 +166,5 @@ Loading does not rewrite the stored log, but the next request is reconstructed u - **`additionalDirectories`** — rejected. A session operates in its single `cwd` (see Per-session cwd); widening the tool/filesystem scope to extra roots is a separate sandbox concern, not yet implemented. - **Prompt content is `text` + `resource_link` only** — image, audio, and embedded-resource blocks are rejected, as is a non-empty `mcpServers` list at `session/new`. -- **Terminal cards render completed output** — live incremental streaming and command classification are named follow-ups of [the terminal-rendering RFC](../../../docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md). +- **Terminal cards render completed output** — live incremental streaming and command classification are named follow-ups of [the terminal-rendering Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md). - **Permission answers are one-shot only** — the bridge offers `allow_once` / `reject_once`; durable `allow_always` grants and their storage/revocation policy remain deferred to the approval seam. diff --git a/packages/ui/acp/acp-feature-support.md b/packages/ui/acp/acp-feature-support.md index 34e3caae8a..497973731e 100644 --- a/packages/ui/acp/acp-feature-support.md +++ b/packages/ui/acp/acp-feature-support.md @@ -82,17 +82,17 @@ 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. | -| `config_option_update` | S | ❌ | ✅ | ✅ | Config options exist (advertised in `session/new`/`session/load`, switched via `session/set_config_option`), but the bridge never pushes agent-initiated changes — an operator default drift is narrated to the MODEL, not echoed to the editor. Future work in the [sandbox RFC § Per-session mode switching](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md). | +| `config_option_update` | S | ❌ | ✅ | ✅ | Config options exist (advertised in `session/new`/`session/load`, switched via `session/set_config_option`), but the bridge never pushes agent-initiated changes — an operator default drift is narrated to the MODEL, not echoed to the editor. Future work in the [sandbox Agent Note § Per-session mode switching](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). | | `usage_update` | S | ❌ | ✅ | ✅ | Token/cost reporting not surfaced (the harness records token usage internally on `assistant/message`). | | `session_info_update` | S | ❌ | ⚠️ | ⚠️ | Session title/metadata not pushed. | ## 5. Tool-call rendering -Tool-call presentation is **owned by each tool** (`presentCall` / `presentResult` on the `dsh-tools` definition), not special-cased in the bridge — see the [terminal-and-tool-rendering RFC](../../../docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md). +Tool-call presentation is **owned by each tool** (`presentCall` / `presentResult` on the `dsh-tools` definition), not special-cased in the bridge — see the [terminal-and-tool-rendering Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md). | Feature | Stable | Bridge | Claude | Codex | Notes | |---|---|---|---|---|---| @@ -111,7 +111,7 @@ Tool-call presentation is **owned by each tool** (`presentCall` / `presentResult ## 6. Session modes / config options / models -Config options ✅: the bridge advertises a `model` select from the advisory LLM provider/model catalog, preserving each provider/model pair in an opaque value and grouping multiple providers. A selected pair is isolated to one session, snapshotted with the prompt for each step, applied through `agent/request`, and restored from the logged request header on load. When `ctx.permission` is composed, the bridge also advertises one `permission` select whose values come from the deployment preset table and whose current value derives from the session log; idle permission switches anchor at the next `agent/prompt-submit` inside its open turn. Session modes stay deliberately unmodeled because config options replace them in ACP v2. See the [model-catalog RFC](../../../docs/rfc/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md) and [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md). +Config options ✅: the bridge advertises a `model` select from the advisory LLM provider/model catalog, preserving each provider/model pair in an opaque value and grouping multiple providers. A selected pair is isolated to one session, snapshotted with the prompt for each step, applied through `agent/request`, and restored from the logged request header on load. When `ctx.permission` is composed, the bridge also advertises one `permission` select whose values come from the deployment preset table and whose current value derives from the session log; idle permission switches anchor at the next `agent/prompt-submit` inside its open turn. Session modes stay deliberately unmodeled because config options replace them in ACP v2. See the [model-catalog Agent Note](../../../.agents/notes/implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md) and [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). ## 7. Content blocks @@ -130,7 +130,7 @@ The bridge rejects unsupported prompt blocks rather than silently dropping them | Feature | Stable | Bridge | Notes | |---|---|---|---| | `StopReason` mapping | S | ✅ | `turnEndToStopReason` is total over harness turn-end reasons → `end_turn`/`max_tokens`/`cancelled`. | -| Multi-session (N per connection) | S | ✅ | Strict per-session demux; concurrent streams never interleave. See the [multi-session RFC](../../../docs/rfc/implemented/feature/2026-06-14-acp-multi-session.md). | +| Multi-session (N per connection) | S | ✅ | Strict per-session demux; concurrent streams never interleave. See the [multi-session Agent Note](../../../.agents/notes/implemented/feature/2026-06-14-acp-multi-session.md). | | Disconnect / disposal teardown | S | ✅ | Quiesces every live session on client disconnect or Cordis disposal. | | `_meta` extensibility | S | ⚠️ | Consumed (Zed terminal cap) and emitted (terminal `_meta`); no other custom extensions. | | Background-task ownership isolation | — | ✅ | Generic `task_output`/`task_kill` reject tasks whose branded owner `SessionId` belongs to another session. | @@ -156,4 +156,4 @@ Unstable/draft ACP features that **neither** reference adapter ships are not tra - Stable spec: `schema/v1/schema.json` (schema `1.14.0`) and `docs/protocol/v1/*.mdx` in the [agent-client-protocol](https://github.com/agentclientprotocol/agent-client-protocol) repo. - Reference adapters: [`claude-agent-acp`](https://github.com/zed-industries/claude-code-acp) and [`codex-acp`](https://github.com/zed-industries/codex-acp). -- Bridge: [`README.md`](README.md), [`src/index.ts`](src/index.ts), and the ACP RFCs under [`docs/rfc/`](../../../docs/rfc/README.md). +- Bridge: [`README.md`](README.md), [`src/index.ts`](src/index.ts), and the ACP Agent Notes under [`.agents/notes/`](../../../.agents/notes/README.md). diff --git a/packages/ui/acp/src/index.ts b/packages/ui/acp/src/index.ts index e20cd40f10..b190aeb47d 100644 --- a/packages/ui/acp/src/index.ts +++ b/packages/ui/acp/src/index.ts @@ -537,7 +537,7 @@ export function apply(ctx: Context, config: AcpConfig): void { // the fail-closed `unavailable` default) takes the question. A rejected // `requestPermission` (client gone, bridge torn down) propagates and the // ApprovalService contains it as `unavailable`. Options are one-shot only: - // allow_always is a grant-storage design the approval RFC defers, so the + // allow_always is a grant-storage design the approval Agent Note defers, so the // prompt never offers a durable grant the harness could not honor. ctx.on('approval/request', (req, next) => { const rec = ownedRecord(req.agent) @@ -627,7 +627,7 @@ export function apply(ctx: Context, config: AcpConfig): void { const protocolVersion = params.protocolVersion === PROTOCOL_VERSION ? params.protocolVersion : PROTOCOL_VERSION // Remember the Zed terminal-output `_meta` capability: when set, bash and // other shell tools render as a terminal card (see streamSessionEventUpdate - // + the terminal-rendering RFC). `_meta` is `{[k]: unknown} | null`, so + // + the terminal-rendering Agent Note). `_meta` is `{[k]: unknown} | null`, so // narrow defensively to a strict boolean true. terminalOutputCap = params.clientCapabilities?._meta?.['terminal_output'] === true return Promise.resolve({ @@ -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/jsonrpc/README.md b/packages/ui/jsonrpc/README.md index 92a8d290bf..856c933cbc 100644 --- a/packages/ui/jsonrpc/README.md +++ b/packages/ui/jsonrpc/README.md @@ -8,7 +8,7 @@ The `jsonrpc` plugin serves newline-delimited JSON-RPC over stdio so out-of-proc ## Config -There are no `cordis.yml` keys. `JsonRpcConfig.input`, `output`, and `exit` are runtime-only transport seams; production uses process stdio and `process.exit`. +`maxTokensAsSuccess` defaults to `false`. Set it to `true` for evaluation hosts that distinguish an accepted, token-limited agent result from an infrastructure failure. `JsonRpcConfig.input`, `output`, and `exit` are runtime-only transport seams; production uses process stdio and `process.exit`. ## stdout is the protocol diff --git a/packages/ui/jsonrpc/src/index.ts b/packages/ui/jsonrpc/src/index.ts index 29fb1aeff8..3da953400f 100644 --- a/packages/ui/jsonrpc/src/index.ts +++ b/packages/ui/jsonrpc/src/index.ts @@ -1,6 +1,6 @@ /** * SDK-facing JSON-RPC plugin over stdio. An external `cordis.yml` decides - * whether to load it; see the single-executable RFC and package README. + * whether to load it; see the single-executable Agent Note and package README. * Stdout is reserved for protocol frames, so the tree must not load a stdout logger. * This plugin answers `shutdown`, disposes its own fiber, and exits 0; the app bin * owns EOF and signal exits. Keep named plugin exports with no default export so @@ -22,8 +22,10 @@ export const name = 'jsonrpc' // Only the agent factory is required; initialize reads the optional LLM seam with ctx.get(). export const inject = ['agents'] -/** Runtime-only test seams; no field is configurable from `cordis.yml`. */ +/** JSON-RPC deployment config plus runtime-only test seams. */ export interface JsonRpcConfig { + /** Report max-token turn/subagent termination as a successful SDK result. */ + maxTokensAsSuccess?: boolean /** Transport input override; production uses `process.stdin`. */ input?: Readable /** Transport output override; production uses `process.stdout`. */ @@ -32,7 +34,9 @@ export interface JsonRpcConfig { exit?: (code: number) => void } -export const Config: Schema<JsonRpcConfig> = Schema.object({}) +export const Config: Schema<JsonRpcConfig> = Schema.object({ + maxTokensAsSuccess: Schema.boolean().default(false), +}) /** * Serve SDK requests over the configured streams. Effect disposal shuts down @@ -41,6 +45,8 @@ export const Config: Schema<JsonRpcConfig> = Schema.object({}) * owns root-context disposal for EOF and signals. */ export function apply(ctx: Context, config: JsonRpcConfig): void { + // Cordis applies the schema default before invoking the plugin. + const resolvedConfig = config as JsonRpcConfig & { maxTokensAsSuccess: boolean } // The later transport callback must dispose this plugin's fiber, not its ambient context. const fiber = ctx.fiber /* v8 ignore next -- production stdio wiring; tests always inject the runtime seams */ @@ -51,7 +57,9 @@ export function apply(ctx: Context, config: JsonRpcConfig): void { const exit = config.exit ?? ((code: number): void => { process.exit(code) }) const transport = new JsonRpcLineTransport(input, output) - const server = new HarnessSdkServer(ctx, transport) + const server = new HarnessSdkServer(ctx, transport, { + maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess, + }) // Share one exit task and attempt flush and disposal independently before exiting. let exitTask: Promise<void> | undefined diff --git a/packages/ui/jsonrpc/src/server.ts b/packages/ui/jsonrpc/src/server.ts index 8949ccb06a..f3a164340c 100644 --- a/packages/ui/jsonrpc/src/server.ts +++ b/packages/ui/jsonrpc/src/server.ts @@ -57,7 +57,22 @@ function subagentParentOf(carrier: Scoped<SubagentService>): Agent { return carrierKeyOf(carrier) as Agent } -/** SDK server whose subscriptions and created agents live until {@link shutdown}. */ +/** Deployment-specific status mapping for SDK turn and subagent outcomes. */ +export interface HarnessSdkServerOptions { + /** Report max-token termination as an accepted result instead of an infrastructure error. */ + maxTokensAsSuccess?: boolean +} + +function successStatus(reason: string, options: HarnessSdkServerOptions): 'ok' | 'error' { + if (reason === 'completed') return 'ok' + return reason === 'max-tokens' && options.maxTokensAsSuccess === true ? 'ok' : 'error' +} + +/** + * SDK server over one booted harness context and transport peer. Construction + * subscribes to session, agent, and subagent lifecycle events until shutdown; + * reinitialization is unsupported. + */ export class HarnessSdkServer { private cwd = process.cwd() private provider = 'deepseek' @@ -72,7 +87,9 @@ export class HarnessSdkServer { constructor( private readonly ctx: Context, private readonly transport: JsonRpcTransportPeer, + private readonly options: HarnessSdkServerOptions = {}, ) { + const serverOptions = this.options this.disposers.push(ctx.on('session/event', (session, event) => { if (event.type === 'turn/end') { const rec = this.sessions.get(String(session.id)) @@ -99,7 +116,7 @@ export class HarnessSdkServer { agentId: String(info.id), parentSessionId: String(parent.session.id), childSessionId: String(info.id), - status: info.stopReason === 'completed' ? 'ok' : 'error', + status: successStatus(info.stopReason, serverOptions), stopReason: info.stopReason, ...(info.lastAssistantMessage === undefined ? {} : { lastAssistantMessage: info.lastAssistantMessage }), }) @@ -233,7 +250,7 @@ export class HarnessSdkServer { private finishedStatus(reason: TurnEndReason | undefined): 'ok' | 'error' { if (!reason) return 'error' - return reason.kind === 'completed' ? 'ok' : 'error' + return successStatus(reason.kind, this.options) } private hasAdapterFor(provider: string): boolean { diff --git a/packages/ui/jsonrpc/tests/server.spec.ts b/packages/ui/jsonrpc/tests/server.spec.ts index 5b9259dd85..034577f105 100644 --- a/packages/ui/jsonrpc/tests/server.spec.ts +++ b/packages/ui/jsonrpc/tests/server.spec.ts @@ -656,7 +656,7 @@ describe('HarnessSdkServer', () => { signal: new AbortController().signal, }) const transport = new FakeTransport() - const server = new HarnessSdkServer(ctx, transport) + const server = new HarnessSdkServer(ctx, transport, { maxTokensAsSuccess: true }) missedStartResult.resolve({ output: [], stopReason: 'max-tokens' }) await missedStartRun.result @@ -692,7 +692,7 @@ describe('HarnessSdkServer', () => { agentId: 'fallback-child-session', parentSessionId: 'fallback-parent', childSessionId: 'fallback-child-session', - status: 'error', + status: 'ok', stopReason: 'max-tokens', lastAssistantMessage: [], }, @@ -782,6 +782,24 @@ describe('HarnessSdkServer', () => { } }) + it('can report max-token turn termination as an accepted evaluation result', async () => { + const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-max-tokens-success-')) + const ctx = await makeHarness(storageDir) + try { + const server = new HarnessSdkServer(ctx, new FakeTransport(), { maxTokensAsSuccess: true }) as unknown as { + finishedStatus(reason: unknown): 'ok' | 'error' + shutdown(): Promise<Record<string, never>> + } + + expect(server.finishedStatus({ kind: 'max-tokens' })).toBe('ok') + expect(server.finishedStatus({ kind: 'error' })).toBe('error') + await server.shutdown() + } finally { + await ctx.fiber.dispose() + await rm(storageDir, { recursive: true, force: true }) + } + }) + it('reports no adapter when the LLM service is absent', async () => { const ctx = new Context() try { diff --git a/packages/ui/permission/README.md b/packages/ui/permission/README.md index e5ee51b69d..12196d89a0 100644 --- a/packages/ui/permission/README.md +++ b/packages/ui/permission/README.md @@ -1,10 +1,10 @@ # @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. -The service requires a confining `ctx.bash` executor and `ctx.approval`. A table entry named `custom` throws at load; composition defaults outside the table instead make a zero-event session derive `custom`. See the [acp-agent composition](../../../examples/acp-agent/) and [sandbox switching design](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md). +The service requires a confining `ctx.bash` executor and `ctx.approval`. A table entry named `custom` throws at load; composition defaults outside the table instead make a zero-event session derive `custom`. See the [acp-agent composition](../../../examples/acp-agent/) and [sandbox switching design](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). ## Model Experience 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/ui/tui/README.md b/packages/ui/tui/README.md index 5da32cbc8d..6d2c7858e4 100644 --- a/packages/ui/tui/README.md +++ b/packages/ui/tui/README.md @@ -2,7 +2,7 @@ The interactive terminal front door for DeepSeek Harness agents, built on [`@earendil-works/pi-tui`](https://www.npmjs.com/package/@earendil-works/pi-tui). It requires stdin and stdout TTYs; scripts and Loader pipes should compose [`@deepseek-ai/dsh-stdio`](../stdio/README.md) instead. -The implemented [TUI feature RFC](../../../docs/rfc/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md) owns the front-door decision; the [terminal-state snapshot RFC](../../../docs/rfc/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md) owns its verification strategy. +The implemented [TUI feature Agent Note](../../../.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.md) owns the front-door decision; the [terminal-state snapshot Agent Note](../../../.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md) owns its verification strategy. This package owns interactive terminal presentation and input only. It injects `agents`, `tools`, and `userInteraction`, then drives an agent created or resumed by app or developer code. Agent lifecycle, persistence, and the model-facing [`ask_user_question`](../tool-ask-user/README.md) tool remain separate composition entries. diff --git a/packages/ui/user-approval/README.md b/packages/ui/user-approval/README.md index 94a30ee28e..77b368fac3 100644 --- a/packages/ui/user-approval/README.md +++ b/packages/ui/user-approval/README.md @@ -8,7 +8,7 @@ Answerers are `approval/request` waterfall listeners. Return an outcome to answe `ApprovalPolicy` is `'ask'` or `'never'`. The effective value is the last `approval/policy` event, falling back to config; `setApprovalPolicy()` is the write path. `'never'` rejects before interactive dispatch and is the only policy stated in the prompt. Switches produce at most one coalesced notice, attributed to the user when the override follows the last `request/header` and to operator/config otherwise. -The tools pipeline routes `ask` decisions through this seam and fails closed when it is absent; the sandboxed bash tool also uses it for escalated retries. The ACP bridge is the shipped human answerer for calls it owns. Audit events remain log-only, so the model sees only the asking consumer's result. See the [approval-seam RFC](../../../docs/rfc/implemented/feature/2026-07-06-approval-seam.md) and [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md). +The tools pipeline routes `ask` decisions through this seam and fails closed when it is absent; the sandboxed bash tool also uses it for escalated retries. The ACP bridge is the shipped human answerer for calls it owns. Audit events remain log-only, so the model sees only the asking consumer's result. See the [approval-seam Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-approval-seam.md) and [sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md). ## Model Experience diff --git a/packages/util/README.md b/packages/util/README.md index 4b4f23ddf7..954026c99c 100644 --- a/packages/util/README.md +++ b/packages/util/README.md @@ -14,6 +14,6 @@ Zero-dependency primitives shared across the other groups. A package lands here `dsh-home` gives every package the same configurable Harness home without assigning that cross-cutting fact to bash, skills, or a composition bundle. It resolves an explicit value before `$DSH_HOME`, falls back to `~/.dsh`, and returns an absolute path without caching, creating, or mutating anything. -`dsh-timeout` follows the same shape for the timeout family: `dsh-bash` and `dsh-web-fetch-local` each fuse a caller's cancellation with a deadline and later classify "timed out" vs "cancelled" by depending on `dsh-timeout` alone. It deliberately owns only the timing/classification half — the *termination* (SIGKILL a process group, tear down a fetch socket) stays in each capability, because no shared layer can own every capability's kill (see [the timeout-library RFC](../../docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md)). +`dsh-timeout` follows the same shape for the timeout family: `dsh-bash` and `dsh-web-fetch-local` each fuse a caller's cancellation with a deadline and later classify "timed out" vs "cancelled" by depending on `dsh-timeout` alone. It deliberately owns only the timing/classification half — the *termination* (SIGKILL a process group, tear down a fetch socket) stays in each capability, because no shared layer can own every capability's kill (see [the timeout-library Agent Note](../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)). -`dsh-retention` is the same split for bounded tool output: a tool (`glob`/`grep`/`bash`/`web_fetch`/`web_search`) feeds items or text into a retainer and gets back what it kept and exactly what it omitted — while grouping, exit codes, provider errors, and recovery prose stay tool-owned. It deliberately owns only the retention mechanic; `truncated` is a budget fact, never an "incomplete inspection" state (see [the retention-library RFC](../../docs/rfc/implemented/architecture/2026-07-06-tool-result-retention-library.md)). +`dsh-retention` is the same split for bounded tool output: a tool (`glob`/`grep`/`bash`/`web_fetch`/`web_search`) feeds items or text into a retainer and gets back what it kept and exactly what it omitted — while grouping, exit codes, provider errors, and recovery prose stay tool-owned. It deliberately owns only the retention mechanic; `truncated` is a budget fact, never an "incomplete inspection" state (see [the retention-library Agent Note](../../.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md)). diff --git a/packages/util/retention/README.md b/packages/util/retention/README.md index 21f8e240ba..e3bc6affc2 100644 --- a/packages/util/retention/README.md +++ b/packages/util/retention/README.md @@ -2,7 +2,7 @@ A dependency-light **retention** library: bounded model-facing output for tools that must cap how much context they return. A caller feeds items or text chunks into a bounded object, then gets the retained content plus exact omission metadata. -The library owns **only** the mechanical question *"what did we keep, and what did we omit?"*. Tool-specific code keeps its business semantics: file grouping, line numbering, exit codes, provider error states, per-line preview truncation, spill files, and the model-facing prose. This is the boundary the [RFC](../../../docs/rfc/implemented/architecture/2026-07-06-tool-result-retention-library.md) draws. +The library owns **only** the mechanical question *"what did we keep, and what did we omit?"*. Tool-specific code keeps its business semantics: file grouping, line numbering, exit codes, provider error states, per-line preview truncation, spill files, and the model-facing prose. This is the boundary the [Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md) draws. It is a **library, not a service or plugin**: no `ctx`, registers nothing, emits no events. The only state is per-retainer (one accumulation), never cross-call. Tool packages import it directly. diff --git a/packages/util/timeout/README.md b/packages/util/timeout/README.md index a14f385b2a..b5923a3a73 100644 --- a/packages/util/timeout/README.md +++ b/packages/util/timeout/README.md @@ -2,7 +2,7 @@ The **timing-and-classification** half of a timeout — a zero-dependency library of pure functions (no runtime harness deps) shared by every capability that clamps a caller's timeout hint, arms a deadline, and later has to tell "timed out" apart from "cancelled". -It owns **no termination**. The signal it hands out only *notifies*; actually stopping the work stays in each capability, because that mechanism differs — bash SIGKILLs an OS process group, web tears down a `fetch` socket — and no shared layer can own all of them. This is the boundary the [RFC](../../../docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md) draws: share the timing/classification, keep the hard kill local. +It owns **no termination**. The signal it hands out only *notifies*; actually stopping the work stays in each capability, because that mechanism differs — bash SIGKILLs an OS process group, web tears down a `fetch` socket — and no shared layer can own all of them. This is the boundary the [Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md) draws: share the timing/classification, keep the hard kill local. It is a **library, not a service or plugin**: no `ctx`, registers nothing, holds no state, emits no events. A "timeout service" would have to understand how to stop every capability's work — exactly the knowledge a microkernel keeps out of shared layers. diff --git a/packages/web/README.md b/packages/web/README.md index c2d34e615f..b465925c79 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -13,4 +13,4 @@ The web access capability seam: an abstract web interface, search/fetch provider The interface lives at `web/web/`. Unlike bash/fs, the seam spans **two capabilities** (search and fetch) with potentially multiple providers each: `ctx.web` is one web-access middle layer with one provider-selection policy, one abort/error vocabulary, and one product-facing "how this harness reaches the web" config surface. Providers register **capabilities**, not tools; `tool-web` is the only owner of model-facing names, schemas, prompt guidance, and presentation. A search provider swap does not change how the model asks for a query, and a fetch implementation swap does not change how the model asks for a URL. -See the [web capability seam RFC](../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md) for the design rationale, including why search and fetch are deliberately one seam and why `web_fetch`'s SSRF protection is deferred. +See the [web capability seam Agent Note](../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) for the design rationale, including why search and fetch are deliberately one seam and why `web_fetch`'s SSRF protection is deferred. diff --git a/packages/web/tool-web/README.md b/packages/web/tool-web/README.md index 399235467e..8cdb9ea737 100644 --- a/packages/web/tool-web/README.md +++ b/packages/web/tool-web/README.md @@ -123,5 +123,5 @@ Append-only; newly visible content follows the reusable request prefix and does ## Known Limitations and Deferred Work - **`htmlToMarkdown` is a minimal regex converter, not an HTML parser** — it strips script/style/noscript, keeps headings/bullets/links, and decodes about a dozen named entities; tables, images, and nested formatting are lost. -- **The model-facing surface is minimal by design, with promotions deferred** — `max_results` stays a config bound (not a model argument), and `web_fetch` takes only `url` (no `format`/`prompt`/LLM-summarization mode); both are named later steps in [the seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md). +- **The model-facing surface is minimal by design, with promotions deferred** — `max_results` stays a config bound (not a model argument), and `web_fetch` takes only `url` (no `format`/`prompt`/LLM-summarization mode); both are named later steps in [the seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md). - **No web-specific permission policy** — both tools execute without requesting `ctx.approval`; a deployment that needs confirmation must add a `tools/pre-execute` policy, and the package does not define persistent URL/domain grants. diff --git a/packages/web/tool-web/tests/spill.spec.ts b/packages/web/tool-web/tests/spill.spec.ts index 58599d2c54..a2b1f9889d 100644 --- a/packages/web/tool-web/tests/spill.spec.ts +++ b/packages/web/tool-web/tests/spill.spec.ts @@ -1,7 +1,7 @@ /** * Showcase integration: the real `web_fetch` tool + the real spill stack * (`dsh-spill-local` backend + `dsh-spill-policy`), exercised through - * `ctx.tools.execute()`. Proves the RFC's default local-backend path — a large + * `ctx.tools.execute()`. Proves the Agent Note's default local-backend path — a large * formatted fetch result is automatically retained and spilled with NO * tool-specific spill code, and the model-facing text changes ONLY by the * deliberate spill notice (the full formatted result lands in the spill file). @@ -48,7 +48,7 @@ beforeEach(async () => { await ctx.plugin(ToolRegistry) await ctx.plugin(WebService, { fetchProvider: WebFetchLocal.LOCAL_FETCH_PROVIDER_ID }) // Provider cap generous so the tool returns a large formatted result; the - // policy cap is what triggers the spill (the RFC's separation of concerns). + // policy cap is what triggers the spill (the Agent Note's separation of concerns). await ctx.plugin(WebFetchLocal, { maxBodyChars: 500_000 }) await ctx.plugin(LocalSpillStore, { root: spillRoot }) await ctx.plugin(SpillPolicy, { maxInlineBytes: MAX_INLINE_BYTES }) diff --git a/packages/web/web-fetch-local/README.md b/packages/web/web-fetch-local/README.md index 94c23e9c66..160d1c6abd 100644 --- a/packages/web/web-fetch-local/README.md +++ b/packages/web/web-fetch-local/README.md @@ -44,6 +44,6 @@ No direct invalidation; the named consumer owns any request-prefix changes. ## Known Limitations and Deferred Work -- **SSRF / private-network protection is deferred** — no blocking of private, loopback, link-local, multicast, or otherwise non-public destinations, no DNS-resolve-then-validate, no per-hop re-validation (see [the web capability seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). Until it lands, this provider is an SSRF primitive and **must not be enabled** in a deployment that can reach sensitive internal network targets. +- **SSRF / private-network protection is deferred** — no blocking of private, loopback, link-local, multicast, or otherwise non-public destinations, no DNS-resolve-then-validate, no per-hop re-validation (see [the web capability seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)). Until it lands, this provider is an SSRF primitive and **must not be enabled** in a deployment that can reach sensitive internal network targets. - **Only textual content decodes** — html/xhtml and `text/*`-plus-JSON/XML families; a missing `Content-Type` or any binary type throws `WEB_UNSUPPORTED_CONTENT_TYPE`, and text-extractable PDF decoding is named deferred work. - **Charset comes only from the `Content-Type` header** (UTF-8 default) — an HTML `<meta charset>` declaration is ignored, and a declared-but-unrecognized charset label throws rather than falling back. diff --git a/packages/web/web-fetch-local/src/policy.ts b/packages/web/web-fetch-local/src/policy.ts index f37697765a..fda9937e47 100644 --- a/packages/web/web-fetch-local/src/policy.ts +++ b/packages/web/web-fetch-local/src/policy.ts @@ -15,7 +15,7 @@ export type FetchableKind = 'html' | 'text' * Validate a request URL against the basic transport hygiene the provider * enforces before any network access: http(s) only, no embedded credentials, * bounded length. Returns the parsed `URL`. Throws {@link WebError} otherwise. - * (SSRF / private-network blocking is deferred — see the package RFC.) + * (SSRF / private-network blocking is deferred — see the package Agent Note.) * * @param input - the raw URL string from the fetch request. * @param maxUrlLength - inclusive upper bound on `input`'s length. diff --git a/packages/web/web-search-exa/README.md b/packages/web/web-search-exa/README.md index e4636b7b89..d8ab206e7a 100644 --- a/packages/web/web-search-exa/README.md +++ b/packages/web/web-search-exa/README.md @@ -36,5 +36,5 @@ No direct invalidation; the named consumer owns any request-prefix changes. ## Known Limitations and Deferred Work - **A result with no non-blank highlight is dropped entirely** — no portable snippet to map, so fewer sources than the requested count can return. -- **Only `searchType`/`numResults`/`highlightsPerResult` are exposed** — Exa's other controls (livecrawl, category, domain/date filters, full-text contents) wait on provider-neutral seam fields ([seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). +- **Only `searchType`/`numResults`/`highlightsPerResult` are exposed** — Exa's other controls (livecrawl, category, domain/date filters, full-text contents) wait on provider-neutral seam fields ([seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)). - **Abort classification is error-shape-based** — only a `DOMException` named `AbortError` maps to `WEB_ABORTED`; an abort carrying a custom reason (e.g. `dsh-timeout`'s `TimeoutReason`) surfaces as `WEB_PROVIDER_ERROR`. diff --git a/packages/web/web-search-perplexity/README.md b/packages/web/web-search-perplexity/README.md index 32d6fc53cc..a3728e0197 100644 --- a/packages/web/web-search-perplexity/README.md +++ b/packages/web/web-search-perplexity/README.md @@ -59,5 +59,5 @@ Append-only; newly visible content follows the reusable request prefix and does - **Citation-fallback sources are URL-only** — when Perplexity omits structured `search_results[]`, sources carry no `title`/`snippet`/`publishedAt`, so the tool renders bare hostname labels. - **Over-returned sources still cost tokens and latency** — with no result-count control on the wire, `maxResults` is enforced only post-hoc by seam truncation. -- **Only `model`/`maxTokens`/`searchRecency` are exposed** — Perplexity's other search controls (domain filters, `web_search_options` context size, images) wait on provider-neutral seam fields ([seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). +- **Only `model`/`maxTokens`/`searchRecency` are exposed** — Perplexity's other search controls (domain filters, `web_search_options` context size, images) wait on provider-neutral seam fields ([seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)). - **Abort classification is error-shape-based** — only a `DOMException` named `AbortError` maps to `WEB_ABORTED`; an abort carrying a custom reason (e.g. `dsh-timeout`'s `TimeoutReason`) surfaces as `WEB_PROVIDER_ERROR`. diff --git a/packages/web/web/README.md b/packages/web/web/README.md index 29e6092ff8..507dd1772e 100644 --- a/packages/web/web/README.md +++ b/packages/web/web/README.md @@ -53,7 +53,7 @@ No direct invalidation; the named consumer owns any request-prefix changes. ## Known Limitations and Deferred Work -- **No observation surface** — no provider-change event and no capability-status query; availability is observed only by executing `search()`/`fetch()` and routing the thrown `WebError` codes, and the no-provider failure is the generic `WEB_PROVIDER_UNAVAILABLE` with no per-provider reason enumeration ([RFC](../../../docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)). -- **`WebSearchRequest` carries only `query` + `maxResults`** — provider-neutral controls (recency, domain filters, regional hints, search depth) are deferred until Exa and Perplexity can both honor them honestly ([seam RFC](../../../docs/rfc/implemented/architecture/2026-06-24-web-capability-seam.md)). +- **No observation surface** — no provider-change event and no capability-status query; availability is observed only by executing `search()`/`fetch()` and routing the thrown `WebError` codes, and the no-provider failure is the generic `WEB_PROVIDER_UNAVAILABLE` with no per-provider reason enumeration ([Agent Note](../../../.agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)). +- **`WebSearchRequest` carries only `query` + `maxResults`** — provider-neutral controls (recency, domain filters, regional hints, search depth) are deferred until Exa and Perplexity can both honor them honestly ([seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)). - **`WebFetchBody` has no `pdf` arm** — text-extractable PDF support is named deferred work; the closed union makes adding it a compile-enforced change across the three web packages. - **Provider-backed page extraction is out of scope of `fetch()`** — a Firecrawl/Tavily-style `web_extract` capability is deferred rather than widening the fetch seam. diff --git a/packages/workflow/README.md b/packages/workflow/README.md index 98fa3cfe04..c89e05c6a7 100644 --- a/packages/workflow/README.md +++ b/packages/workflow/README.md @@ -1,6 +1,6 @@ # workflow/ — dynamic-workflow capability family -The workflow seam: a model-written JavaScript orchestration script that fans out subagents at scale (phases, structured per-agent results, concurrency caps), modeled on Claude Code's dynamic workflows. A capability seam (see [capability seams](../../docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)) in the bash shape: ONE engine implementation per context registers as `ctx.workflows`; the model-facing tool consumes it. +The workflow seam: a model-written JavaScript orchestration script that fans out subagents at scale (phases, structured per-agent results, concurrency caps), modeled on Claude Code's dynamic workflows. A capability seam (see [capability seams](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)) in the bash shape: ONE engine implementation per context registers as `ctx.workflows`; the model-facing tool consumes it. | Package | Role | ctx key | |---|---|---| @@ -10,4 +10,4 @@ The workflow seam: a model-written JavaScript orchestration script that fans out The interface lives at `workflow/workflow/`. The engine's `agent()` hook rides the [subagent seam](../subagent/README.md) (any registered provider; the shipped examples use `spawn`), and `agent({ schema })` rides the structured-output support the in-process backends implement. The worker thread isolates the SCRIPT — the host never blocks on it, and a cancelled run's post-grace termination is real — but it is NOT a security boundary; an isolated-vm/separate-process engine (actual sandboxing) swaps in behind the same interface if that ever matters. -The proposal, decisions, and deferred work: [docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md](../../docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md). +The proposal, decisions, and deferred work: [.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md](../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md). diff --git a/packages/workflow/tool-workflow/README.md b/packages/workflow/tool-workflow/README.md index dc86a38b9d..016f885914 100644 --- a/packages/workflow/tool-workflow/README.md +++ b/packages/workflow/tool-workflow/README.md @@ -12,7 +12,7 @@ Collection is SYNCHRONOUS this cut (like [`dsh-tool-subagent`](../../subagent/to ## Render intent -Decided up front (per the [render-intent RFC](../../../docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md)): a `generic` card titled `workflow: <meta.name>`, read directly from `args.meta.name` (presentation is a pure function of args and does not ask the engine to parse); the script text rides as `rawInput`. The result keeps the generic card. +Decided up front (per the [render-intent Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md)): a `generic` card titled `workflow: <meta.name>`, read directly from `args.meta.name` (presentation is a pure function of args and does not ask the engine to parse); the script text rides as `rawInput`. The result keeps the generic card. ## Config diff --git a/packages/workflow/workflow-workerthread/src/realm.ts b/packages/workflow/workflow-workerthread/src/realm.ts index b16e2f6860..cdcc86f8a0 100644 --- a/packages/workflow/workflow-workerthread/src/realm.ts +++ b/packages/workflow/workflow-workerthread/src/realm.ts @@ -4,7 +4,7 @@ * lossy JSON shapes but trusts model-written workflow scripts: getters and proxy traps may * run, and the vm is not a security boundary. The worker provides host-loop isolation and * forced termination, not hostile-value containment. See - * docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md for the isolation rationale. + * .agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md for the isolation rationale. * @module @deepseek-ai/dsh-workflow-workerthread/realm */ diff --git a/packages/workflow/workflow/README.md b/packages/workflow/workflow/README.md index 9f2b557811..331ae36e8d 100644 --- a/packages/workflow/workflow/README.md +++ b/packages/workflow/workflow/README.md @@ -54,4 +54,4 @@ No direct invalidation; the named consumer owns any request-prefix changes. - **No token-budget vocabulary** — engines cap concurrency, items, and children, but neither the request nor result accounts for model tokens across children. - **Runs are holder-owned, not service-tracked** — unloading the engine does not discover independent live handles; every consumer must dispose the run it started. -See the [dynamic-workflows RFC](../../../docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md) for the deferred workflow surface. +See the [dynamic-workflows Agent Note](../../../.agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md) for the deferred workflow surface. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42f5763fdf..7712cf7732 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: '@deepseek-ai/dsh-acp-demo': specifier: workspace:* version: link:../packages/examples/acp-demo + '@deepseek-ai/dsh-agent-spine-demo': + specifier: workspace:* + version: link:../packages/examples/agent-spine-demo '@deepseek-ai/dsh-bash-local': specifier: workspace:* version: link:../packages/bash/bash-local @@ -116,18 +119,27 @@ 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-hooks-claude': specifier: workspace:* version: link:../packages/hooks/hooks-claude '@deepseek-ai/dsh-hooks-codex': specifier: workspace:* version: link:../packages/hooks/hooks-codex + '@deepseek-ai/dsh-jsonrpc': + specifier: workspace:* + version: link:../packages/ui/jsonrpc '@deepseek-ai/dsh-llm': specifier: workspace:* version: link:../packages/llm/llm @@ -146,6 +158,12 @@ 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 '@deepseek-ai/dsh-spill-local': specifier: workspace:* version: link:../packages/spill/spill-local @@ -212,9 +230,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) @@ -236,10 +251,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:^ @@ -253,6 +264,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) @@ -290,6 +304,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 @@ -373,6 +390,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 @@ -392,6 +412,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: @@ -861,6 +906,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) @@ -893,6 +941,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: @@ -926,6 +992,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 @@ -935,6 +1007,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) @@ -1218,6 +1293,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': @@ -2043,6 +2134,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 @@ -2480,6 +2574,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 @@ -2531,6 +2628,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 @@ -2774,15 +2874,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: @@ -3045,6 +3163,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==} @@ -3563,6 +3684,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 +4810,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'} @@ -4753,9 +4873,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==} @@ -4805,13 +4922,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'} @@ -4826,18 +4936,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'} @@ -4905,17 +5007,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==} @@ -5133,26 +5228,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} @@ -5192,9 +5270,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'} @@ -5231,10 +5306,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==} @@ -5277,10 +5348,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} @@ -5543,12 +5610,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'} @@ -5753,11 +5814,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==} @@ -5956,9 +6012,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==} @@ -5976,10 +6029,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==} @@ -6023,9 +6072,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'} @@ -6033,9 +6079,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==} @@ -6128,11 +6171,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} @@ -6154,9 +6192,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'} @@ -6252,21 +6287,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==} @@ -6334,12 +6360,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==} @@ -6620,9 +6640,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'} @@ -6642,10 +6659,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==} @@ -6736,9 +6749,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'} @@ -6890,10 +6900,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'} @@ -6983,6 +6989,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 @@ -7052,17 +7064,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'} @@ -7075,9 +7080,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'} @@ -7088,9 +7090,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'} @@ -7569,6 +7568,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': @@ -7969,6 +7971,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 @@ -8992,8 +9005,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: {} @@ -9054,8 +9065,6 @@ snapshots: transitivePeerDependencies: - supports-color - boolbase@1.0.0: {} - bowser@2.14.1: {} brace-expansion@2.1.2: @@ -9094,24 +9103,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 @@ -9124,12 +9115,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: {} @@ -9197,21 +9184,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): @@ -9441,32 +9418,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 @@ -9495,8 +9450,6 @@ snapshots: encodeurl@2.0.0: {} - entities@2.2.0: {} - entities@7.0.1: {} entities@8.0.0: {} @@ -9568,8 +9521,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: {} @@ -9643,8 +9594,6 @@ snapshots: transitivePeerDependencies: - supports-color - esm@3.2.25: {} - espree@10.4.0: dependencies: acorn: 8.17.0 @@ -9956,20 +9905,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 @@ -10156,16 +10091,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 @@ -10340,13 +10265,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: {} @@ -10355,13 +10273,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 @@ -10480,8 +10391,6 @@ snapshots: media-typer@1.1.0: {} - mensch@0.3.4: {} - merge-descriptors@2.0.0: {} mermaid@11.16.0: @@ -10508,8 +10417,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 @@ -10707,8 +10614,6 @@ snapshots: dependencies: mime-db: 1.54.0 - mime@2.6.0: {} - minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 @@ -10725,8 +10630,6 @@ snapshots: mitt@3.0.1: {} - mj-context-menu@0.6.1: {} - mri@1.2.0: {} ms@2.1.3: {} @@ -10801,19 +10704,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: {} @@ -10915,12 +10813,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 @@ -11279,8 +11171,6 @@ snapshots: sisteransi@1.0.5: {} - slick@1.12.2: {} - smol-toml@1.6.1: {} source-map-js@1.2.1: {} @@ -11291,12 +11181,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: {} @@ -11377,8 +11261,6 @@ snapshots: dependencies: tldts: 7.4.5 - tr46@0.0.3: {} - tr46@6.0.0: dependencies: punycode: 2.3.1 @@ -11508,8 +11390,6 @@ snapshots: uuid@14.0.1: {} - valid-data-url@3.0.1: {} - vary@1.1.2: {} vfile-message@4.0.3: @@ -11572,7 +11452,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) @@ -11593,7 +11480,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' @@ -11697,21 +11583,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: {} @@ -11724,11 +11597,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 @@ -11738,8 +11606,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/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8f93814899..26bfaeeb0b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,7 +7,7 @@ packages: # plain-node (`:lib`) boot of any leaf (examples/<leaf>/cordis.yml) resolves its # plugins through real package `exports`→lib by walking up to examples/node_modules. # Members for DEPENDENCY RESOLUTION only — NOT build targets: tsdown's explicit - # globs (vendor/*, packages/*/*) exclude them. See the example-execute-over-tsx RFC. + # globs (vendor/*, packages/*/*) exclude them. See the example-execute-over-tsx Agent Note. - examples # Deploy root of the single-exe build: a pure dependency manifest whose # closure is what the exe bundles and what the Python runtime distributes. diff --git a/python/README.i18n.yaml b/python/README.i18n.yaml index 1470a34443..5df1ca291d 100644 --- a/python/README.i18n.yaml +++ b/python/README.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 -README.md: 30bca971f1bfc6f694302c8f7eb8ce80843ed9b2 -README.zh.md: d2eea59d148b6de1bdf36b2f2e9c96fc1c933be7 +README.md: d2b6a1cfe9897026d567b2def301799069c350fb +README.zh.md: 2ffccef922d52923f7c6e373a01ed8b19d9a55c1 diff --git a/python/README.md b/python/README.md index 30bca971f1..d2b6a1cfe9 100644 --- a/python/README.md +++ b/python/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Python packages for driving DeepSeek Harness as a subprocess: a client SDK that spawns the `dsh-jsonrpc-agent` binary and talks newline-delimited JSON-RPC over stdio. The runtime carrier is the single-file executable produced by this repo; design, build, and acceptance details live in [docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md](../docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md). +Python packages for driving DeepSeek Harness as a subprocess: a client SDK that spawns the `dsh-jsonrpc-agent` binary and talks newline-delimited JSON-RPC over stdio. The runtime carrier is the single-file executable produced by this repo; design, build, and acceptance details live in [.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md](../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md). ## Packages diff --git a/python/README.zh.md b/python/README.zh.md index d2eea59d14..2ffccef922 100644 --- a/python/README.zh.md +++ b/python/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -以子进程方式驱动 DeepSeek Harness 的 Python 包:客户端 SDK spawn `dsh-jsonrpc-agent` 二进制,并通过 stdio 上按行分隔的 JSON-RPC 与之通信。运行时载体是本仓库产出的单文件可执行文件;设计、构建与验收细节见 [docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md](../docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md)。 +以子进程方式驱动 DeepSeek Harness 的 Python 包:客户端 SDK spawn `dsh-jsonrpc-agent` 二进制,并通过 stdio 上按行分隔的 JSON-RPC 与之通信。运行时载体是本仓库产出的单文件可执行文件;设计、构建与验收细节见 [.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md](../.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md)。 ## 包 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/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index bb012a90db..181df4986e 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.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 -README.md: 80c9d1f50d26fc4f4670800fd7d7f5ea442ad891 -README.zh.md: ffedb5eb30f17388fe589863dbc654b22716b40c +README.md: 5fd1bc7cd89152a28d3da17100fd62eed4f8cb14 +README.zh.md: 247a2ca5ea5c1c3afc19335a6bbcba356c823211 diff --git a/python/sdk/README.md b/python/sdk/README.md index 80c9d1f50d..5fd1bc7cd8 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -27,7 +27,7 @@ from deepseek_harness import DeepSeekHarness with DeepSeekHarness( provider="deepseek", model="deepseek-v4-flash", - cordis="examples/dsbench-coding-agent/cordis.yml", + cordis="examples/jsonrpc-agent/cordis.yml", ) as harness: result = harness.run("Make the requested code change.") ``` diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index ffedb5eb30..247a2ca5ea 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -23,7 +23,7 @@ from deepseek_harness import DeepSeekHarness with DeepSeekHarness( provider="deepseek", model="deepseek-v4-flash", - cordis="examples/dsbench-coding-agent/cordis.yml", + cordis="examples/jsonrpc-agent/cordis.yml", ) as harness: result = harness.run("Make the requested code change.") ``` diff --git a/scripts/agent-note-tree.ts b/scripts/agent-note-tree.ts new file mode 100644 index 0000000000..1dff5aab22 --- /dev/null +++ b/scripts/agent-note-tree.ts @@ -0,0 +1,78 @@ +/** + * Shared structural source of truth for the Agent Note tree. Lifecycle and class + * sets are closed under `.agents/notes/README.md`; importing this module is pure. + */ + +import { globSync, readdirSync } from 'node:fs' +import { resolve, sep } from 'node:path' + +export const agentNoteRoot = resolve(import.meta.dirname, '../.agents/notes') + +/** The closed set of Agent Note lifecycles (top-level folders under .agents/notes/). */ +const LIFECYCLES = ['proposed', 'implemented', 'rejected'] as const + +/** + * The closed set of Agent Note classes (nested folder under each lifecycle). Adding a + * class is a deliberate act: extend this list AND the README's Classification + * section. The gate rejects any folder not listed here. + */ +const CLASSES = ['feature', 'bug-fix', 'simplification', 'architecture', 'process', 'testing'] as const + +/** Non-Agent Note Markdown allowed to sit directly at a lifecycle root. */ +const ROOT_ALLOWLIST = new Set(['AGENTS.md', 'CLAUDE.md']) + +/** One Agent Note file, as discovered by the walker. */ +export interface AgentNote { + lifecycle: string + /** Path relative to .agents/notes. */ + rel: string + /** `yyyy-mm-dd` from the filename. */ + date: string +} + +/** + * Walk the Agent Note tree, enforcing the structure rules. Returns every valid Agent Note + * plus one error string per violation (unknown lifecycle or class folder, bad + * depth, or bad filename). Callers treat a non-empty error list as fatal. + */ +export function walkAgentNoteTree(): { notes: AgentNote[]; errors: string[] } { + const notes: AgentNote[] = [] + const errors: string[] = [] + // The lifecycle set is closed too: any directory under .agents/notes/ that is not + // a known lifecycle would otherwise hold Agent Notes invisible to the walk below. + for (const entry of readdirSync(agentNoteRoot, { withFileTypes: true })) { + if (entry.name === 'INDEX.md') { + errors.push('structure: INDEX.md — centralized Agent Note indexes are forbidden; browse the lifecycle/class tree or search the repository') + continue + } + if (entry.isDirectory() && !(LIFECYCLES as readonly string[]).includes(entry.name)) { + errors.push(`structure: ${entry.name}/ — unknown lifecycle folder (allowed: ${LIFECYCLES.join(', ')})`) + } + } + for (const lifecycle of LIFECYCLES) { + for (const match of globSync(`${lifecycle}/**/*.md`, { cwd: agentNoteRoot }).map(path => path.split(sep).join('/')).sort()) { + const segs = match.split('/') + // Allowlisted file directly at the lifecycle root (e.g. implemented/AGENTS.md). + if (segs.length === 2 && ROOT_ALLOWLIST.has(segs[1] ?? '')) continue + // A Chinese counterpart (foo.zh.md, docs/i18n/README.md) is the SAME Agent Note, + // indexed via its English filename; the pairing gate owns its consistency. + if (match.endsWith('.zh.md')) continue + const cls = segs[1] + const base = segs[2] + if (segs.length !== 3 || cls === undefined || base === undefined) { + errors.push(`structure: ${match} — expected {lifecycle}/{class}/file.md (got depth ${segs.length})`) + continue + } + if (!(CLASSES as readonly string[]).includes(cls)) { + errors.push(`structure: ${match} — unknown class folder "${cls}" (allowed: ${CLASSES.join(', ')})`) + continue + } + if (!/^\d{4}-\d{2}-\d{2}-.+\.md$/.test(base)) { + errors.push(`structure: ${match} — filename must be yyyy-mm-dd-topic.md`) + continue + } + notes.push({ lifecycle, rel: match, date: base.slice(0, 10) }) + } + } + return { notes, errors } +} diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index ebca6cebd7..b72f73671e 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -1,7 +1,7 @@ /** * Build the SDK runtime executables and Python node carrier. The fixed * `@yao-pkg/pkg --sea` route, deploy flags, and artifact layout are owned by - * docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md. + * .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md. * The staged closure is symlink-free, and whole-tree assets cover Cordis's * runtime imports that pkg cannot discover statically. */ @@ -69,7 +69,7 @@ class Target { readonly nodeRange: string, /** * pkg platform tag. Windows is a documented non-goal - * (docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md). + * (.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md). */ readonly platform: Platform, /** pkg CPU tag. */ @@ -190,7 +190,7 @@ class BuildCli { ' --dry-run print every command and config patch without executing.', ' --help print this help.', '', - `Build route: ${PKG_SPEC} --sea; see docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.`, + `Build route: ${PKG_SPEC} --sea; see .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.`, `Stages the node carrier in ${PYTHON_RUNTIME_DIR}/${PYTHON_NODE_SUBDIR} and writes executables to ${OUT_DIR}/.`, ].join('\n') } 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.<name>`.', + 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<string, { sf: ts.SourceFile; text: string }> + 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<string, (ts.MethodSignature | ts.PropertySignature | ts.MethodDeclaration)[]>, +): 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<string>() + 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<string, (ts.MethodSignature | ts.PropertySignature | ts.MethodDeclaration)[]>() + 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<string, Member[]>() + const statics = new Map<string, Member[]>() + 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<string, Member[]>, 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 = [ + '<!-- Generated by scripts/gen-cordis-catalog.ts — do not edit by hand.', + ' Run `pnpm run gen-cordis-catalog` to regenerate. -->', + '', + `# ${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<string, string> { + 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 34af82b48b..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', '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-config-catalog.ts b/scripts/gen-config-catalog.ts index a0f3909a36..b4c20d596a 100644 --- a/scripts/gen-config-catalog.ts +++ b/scripts/gen-config-catalog.ts @@ -837,7 +837,7 @@ export function render(entries: CatalogEntry[]): string { '', '## Seam packages (not directly loadable)', '', - 'Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](rfc/implemented/architecture/2026-06-13-capability-seams.md)).', + 'Abstract service classes — a deployment loads a concrete implementation package instead ([capability seams](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)).', '', ...entries.filter(e => e.kind === 'seam').map(e => renderTerse(e, ` — abstract \`${e.className ?? ''}\``)), '', diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 15780b17e0..97556dd019 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<string, string> = { CodeRunResult: 'code-runtime.md', CompactionResult: 'compaction.md', CompactionTrigger: 'compaction.md', + PruneResult: 'compaction.md', FileReadOutcome: 'filesystem.md', FsDirEntry: 'filesystem.md', FsEditOutcome: 'filesystem.md', @@ -265,8 +267,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 { @@ -504,7 +505,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`).', '', @@ -539,7 +540,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)) @@ -563,6 +564,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[] = [] @@ -579,15 +581,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 4e3f049ac9..c05aa1e54f 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -95,6 +95,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', @@ -194,6 +202,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', @@ -226,10 +243,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', @@ -432,7 +449,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', @@ -900,7 +917,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.', '', @@ -1046,7 +1063,7 @@ function renderIndex(docs: GraphDoc[]): string { ...generatedHeader('Documentation Graph Index'), 'These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the generated [events](cordis-catalog/events.md) / [services](cordis-catalog/services.md) catalogs, [tool-catalog.md](tool-catalog.md), and [core-data-structures/](core-data-structures/core.md).', '', - 'The process decision behind this index is recorded in [the documentation graph RFC](rfc/implemented/process/2026-07-03-documentation-graph-atlas.md).', + 'The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md).', '', '| Graph | Mode |', '| --- | --- |', diff --git a/scripts/gen-persistence-catalog.ts b/scripts/gen-persistence-catalog.ts index 8762b8529e..167fd3c7f1 100644 --- a/scripts/gen-persistence-catalog.ts +++ b/scripts/gen-persistence-catalog.ts @@ -349,7 +349,7 @@ export function render(events: AnnotatedLogEventEntry[], envelopeTypes: EventEnv '', 'Every event type that can appear in a session\'s durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](core-data-structures/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](core-data-structures/persistence.md) (how the log is made durable), and the [cordis events catalog](cordis-catalog/events.md) (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single `session/event` emit).', '', - 'This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog RFC](rfc/implemented/process/2026-07-04-persistence-log-catalog.md).', + 'This file is GENERATED from source (`scripts/gen-persistence-catalog.ts`) and verified fresh by `pnpm run verify-persistence-catalog` (part of `doc-sync`) — do not edit it by hand. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md).', '', 'The envelope declarations below compose each event\'s `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at `Session.append`), and the whole format is pinned at `SESSION_FORMAT_VERSION = 0` — pre-release, no compatibility implied ([the version stance](core-data-structures/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.', '', diff --git a/scripts/gen-rfc-index.ts b/scripts/gen-rfc-index.ts deleted file mode 100644 index 37ad96d73a..0000000000 --- a/scripts/gen-rfc-index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Regenerate `docs/rfc/INDEX.md` — the fully generated RFC index — from the - * RFC tree (see [rfc-index.ts](./rfc-index.ts) for the layout contract and - * rendering rules). The whole file is generated state; the curated prose lives - * in `docs/rfc/README.md`. Freshness is asserted by - * `verify-rfc-classification.ts` (a `doc-sync` member), so a stale committed - * index fails CI. - * - * Run: `pnpm run gen-rfc-index`. - */ - -import { readFileSync, writeFileSync } from 'node:fs' -import { resolve } from 'node:path' -import { renderIndex, rfcRoot, walkRfcTree } from './rfc-index.ts' - -const { rfcs, errors } = walkRfcTree() -if (errors.length > 0) { - console.error('gen-rfc-index: refusing to generate from a structurally invalid tree:') - for (const e of errors) console.error(` ${e}`) - process.exit(1) -} - -const indexPath = resolve(rfcRoot, 'INDEX.md') -const next = renderIndex(rfcs) -let current: string | undefined -try { - current = readFileSync(indexPath, 'utf8') -} catch { - // Missing INDEX.md is the fresh-generation case, not an error: fall through and write it. -} -if (next === current) { - console.log(`gen-rfc-index: docs/rfc/INDEX.md is up to date (${rfcs.length} RFCs).`) -} else { - writeFileSync(indexPath, next) - console.log(`gen-rfc-index: docs/rfc/INDEX.md regenerated (${rfcs.length} RFCs).`) -} diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 8606d30895..287133454a 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -3,7 +3,7 @@ * plugin. Runtime registration is the source of truth for computed schemas; * the manifest is checked against every on-disk `tool-*` package. `--check` * verifies the committed artifact. Rationale and ownership live in - * `docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.md`. + * `.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md`. */ import { globSync, readFileSync, writeFileSync } from 'node:fs' @@ -12,6 +12,8 @@ import { Context } from 'cordis' import type { ToolSchema } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools' +import { BashExecutor } from '@deepseek-ai/dsh-bash' +import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash' import LocalBashExecutor from '@deepseek-ai/dsh-bash-local' import LocalFileSystem from '@deepseek-ai/dsh-fs-local' import UserInteractionService from '@deepseek-ai/dsh-user-interaction' @@ -38,6 +40,44 @@ import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow' const root = resolve(import.meta.dirname, '..') const OUT = 'docs/tool-catalog.md' +const CATALOG_RG_PROBE_COMMAND = 'command -v rg >/dev/null 2>&1' + +/** + * Minimal bash service for harvesting `dsh-tool-fs-search` schemas. The search + * plugin now probes `rg` at registration time, but the generated catalog must + * remain independent of the host PATH and never execute a real search. + */ +class CatalogSearchBashExecutor extends BashExecutor { + override resolve(request: BashExecRequest): BashExecSpec { + return { + command: request.command, + workdir: request.workdir ?? root, + timeoutMs: request.timeoutMs ?? 60_000, + stdoutMaxBytes: request.stdoutMaxBytes ?? 64_000, + signal: request.signal, + sandboxMode: request.sandboxMode, + } + } + + override run(spec: BashExecSpec): Promise<BashRunResult> { + if (spec.command !== CATALOG_RG_PROBE_COMMAND) { + throw new Error(`gen-tool-catalog: unexpected search bash command during schema harvest: ${spec.command}`) + } + return Promise.resolve({ + exitCode: 0, + signal: null, + timedOut: false, + aborted: false, + timeoutMs: spec.timeoutMs, + stdout: { text: '', truncated: false }, + stderr: { text: '', truncated: false }, + }) + } + + override start(): BashProcess { + throw new Error('gen-tool-catalog: search schema harvest must not start background processes') + } +} /** Register the descriptor needed to mount schema-producing consumers. */ function registerCatalogSubagentProvider(ctx: Context, name: string): void { @@ -119,7 +159,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ toolsConfig: { mode: 'code' }, async mount() {}, note: - 'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode RFC). Under `code` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.', + 'Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry\'s only wire contribution; the other visible capabilities are declared in a generated TypeScript SDK section, and a program calls them through serialized bindings that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.', }, { pkg: '@deepseek-ai/dsh-tool-bash', @@ -144,7 +184,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ await ctx.plugin(ToolCordis) }, note: - 'Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; a full changed request header logs those tool-set changes.', + 'Ships in examples/cordis-agent only (a deliberate opt-in — mounted code gets the real ctx, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). Plugins the model mounts may register ADDITIONAL model-visible tools at runtime; a full changed request header logs those tool-set changes.', }, { pkg: '@deepseek-ai/dsh-tool-fs', @@ -169,14 +209,15 @@ const TOOL_PACKAGES: ToolPackage[] = [ writes: ['tool/call', 'tool/result'], async mount(ctx) { // The tools inject `bash` (search executes fixed `rg` commands through - // the executor seam, not ctx.fs); boot the local executor to satisfy it. - // `ctx.spillStore` is optional (read via ctx.get) and does not affect the - // schemas, so no spill backend is mounted. - await ctx.plugin(LocalBashExecutor) + // the executor seam, not ctx.fs). Use a catalog-only executor so the + // registration-time `rg` probe stays deterministic and the generator + // never depends on the host PATH. `ctx.spillStore` is optional (read via + // ctx.get) and does not affect the schemas, so no spill backend is mounted. + await ctx.plugin(CatalogSearchBashExecutor) await ctx.plugin(ToolFsSearch) }, note: - 'glob and grep are bash-backed discovery tools: they run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.', + 'glob and grep are conditional bash-backed discovery tools: they register only when ctx.bash can find `rg`, then run fixed ripgrep commands through ctx.bash as ordinary foreground calls (never background tasks). Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.', }, { pkg: '@deepseek-ai/dsh-tool-skill', @@ -367,7 +408,7 @@ export function render(catalog: ToolCatalog): string { '', 'Every model-facing tool a shipped plugin contributes to `ctx.tools`: the `name`, `description`, and JSON-Schema `parameters` the model receives via the system-prompt assembly. It complements the cordis [events](cordis-catalog/events.md) & [services](cordis-catalog/services.md) catalogs (the wiring a plugin listens to and calls) and [core-data-structures/](core-data-structures/core.md) (the types those signatures move) — this page is the *tools* the agent is offered.', '', - 'This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator\'s boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog RFC](rfc/implemented/process/2026-07-02-tool-schema-catalog.md).', + 'This file is GENERATED and verified fresh by `pnpm run verify-tool-catalog` (part of `doc-sync`) — do not edit it by hand. Unlike the cordis catalog (a pure source-AST pass), this generator BOOTS each tool plugin on a real context and reads `ctx.tools.schemas()`, because a tool schema is not statically knowable (runtime-spread enums, concatenated descriptions, config-driven names, raw-JSON-Schema MCP tools). A completeness guard globs `packages/*/tool-*` and fails if any package is missing from the generator\'s boot manifest, so a new tool cannot be silently undocumented. See [the tool-schema-catalog Agent Note](../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md).', '', 'Scope: shipped product tools under `packages/*/tool-*`, each booted with its DEFAULT config. The registered tool NAME can be a load-time config (e.g. `tool-subagent`\'s `toolName`), so a deployment may surface a package under a different or additional name — a per-package note records those shipped aliases where they exist. The `examples/` demo tools (e.g. `echo`) are excluded, matching the cordis catalog\'s packages-only scope.', '', 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.<key>` harness service (walked from - * every `declare module 'cordis'` Context merge under `packages/<group>/<pkg>/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.<name>`.', - sections: [ - { kind: 'class', file: 'vendor/cordis/src/service.ts', symbol: 'Service' }, - ], - }, -] -// --------------------------------------------------------------------------- -// Extraction -// --------------------------------------------------------------------------- - -const sfCache = new Map<string, { sf: ts.SourceFile; text: string }>() - -/** 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<Class, 'a' | 'b'>` heritage clause on the Context - * merge to the named members of `Class` declared in the same file — the fiber - * merge (`interface Context extends Pick<Fiber, 'effect'>`) is the motivating - * case: without this, `ctx.effect` had no documented signature anywhere. */ -function heritageMembers( - stmt: ts.InterfaceDeclaration, - sf: ts.SourceFile, - groups: Map<string, (ts.MethodSignature | ts.PropertySignature | ts.MethodDeclaration)[]>, -): 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<string>() - 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<string, (ts.MethodSignature | ts.PropertySignature | ts.MethodDeclaration)[]>() - 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<string, Renderable[]>() - const statics = new Map<string, (ts.MethodDeclaration | ts.PropertyDeclaration)[]>() - 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<string, Renderable[]>, 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<string, (ts.MethodDeclaration | ts.PropertyDeclaration | ts.GetAccessorDeclaration)[]>() - 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 = '<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. -->' - -/** 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<string, string> { - const violations: string[] = [] - const files = new Map<string, string>() - - 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'), '<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<Nodes, { type: 'link' | 'image' | 'definition' }> + +/** 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<string, Map<DocsLocale, DocsPage>> { + const map = new Map<string, Map<DocsLocale, DocsPage>>() + for (const page of pages) { + for (const source of [page.source, ...(page.sourceAliases ?? [])]) { + const localized = map.get(source) ?? new Map<DocsLocale, DocsPage>() + 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<string>() + 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/rfc-index.ts b/scripts/rfc-index.ts deleted file mode 100644 index a8d7868bce..0000000000 --- a/scripts/rfc-index.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Shared source of truth for the RFC index: the tree walker (structure rules) and the README - * table renderer. `gen-rfc-index.ts` writes the generated regions; - * `verify-rfc-classification.ts` checks structure and asserts the committed regions are fresh. - * Lifecycle and class sets are closed under `docs/rfc/README.md`; rows derive - * from path, H1, and filename date and sort deterministically. Import is pure. - */ - -import { readFileSync, readdirSync } from 'node:fs' -import { resolve, sep } from 'node:path' -import { globSync } from 'node:fs' - -export const rfcRoot = resolve(import.meta.dirname, '../docs/rfc') - -/** The closed set of RFC lifecycles (top-level folders under docs/rfc/). */ -const LIFECYCLES = ['proposed', 'implemented', 'rejected'] as const - -/** - * The closed set of RFC classes (nested folder under each lifecycle). Adding a - * class is a deliberate act: extend this list AND the README's Classification - * section. The gate rejects any folder not listed here. - */ -const CLASSES = ['feature', 'bug-fix', 'simplification', 'architecture', 'process', 'testing'] as const - -/** Non-RFC Markdown allowed to sit directly at a lifecycle root. */ -const ROOT_ALLOWLIST = new Set(['AGENTS.md', 'CLAUDE.md']) - -/** Title-case a class/lifecycle folder name for a README heading. */ -const heading = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1) - -/** One RFC file, as discovered by the walker. */ -export interface Rfc { - lifecycle: string - cls: string - base: string - /** Path relative to docs/rfc — the README link target. */ - rel: string - /** H1 text with any `RFC: ` prefix stripped — the README row title. */ - title: string - /** `yyyy-mm-dd` from the filename — the "First proposed" column. */ - date: string -} - -/** - * Walk the RFC tree, enforcing the structure rules. Returns every valid RFC - * plus one error string per violation (unknown lifecycle or class folder, bad - * depth, bad filename, missing/malformed H1). Callers treat a non-empty error - * list as fatal — the index is only generated from a structurally valid tree. - */ -export function walkRfcTree(): { rfcs: Rfc[]; errors: string[] } { - const rfcs: Rfc[] = [] - const errors: string[] = [] - // The lifecycle set is closed too: any directory under docs/rfc/ that is not - // a known lifecycle would otherwise hold RFCs invisible to the walk below. - for (const entry of readdirSync(rfcRoot, { withFileTypes: true })) { - if (entry.isDirectory() && !(LIFECYCLES as readonly string[]).includes(entry.name)) { - errors.push(`structure: ${entry.name}/ — unknown lifecycle folder (allowed: ${LIFECYCLES.join(', ')})`) - } - } - for (const lifecycle of LIFECYCLES) { - for (const match of globSync(`${lifecycle}/**/*.md`, { cwd: rfcRoot }).map(path => path.split(sep).join('/')).sort()) { - const segs = match.split('/') - // Allowlisted file directly at the lifecycle root (e.g. implemented/AGENTS.md). - if (segs.length === 2 && ROOT_ALLOWLIST.has(segs[1] ?? '')) continue - // A Chinese counterpart (foo.zh.md, docs/i18n/README.md) is the SAME RFC, - // indexed via its English filename; the pairing gate owns its consistency. - if (match.endsWith('.zh.md')) continue - const cls = segs[1] - const base = segs[2] - if (segs.length !== 3 || cls === undefined || base === undefined) { - errors.push(`structure: ${match} — expected {lifecycle}/{class}/file.md (got depth ${segs.length})`) - continue - } - if (!(CLASSES as readonly string[]).includes(cls)) { - errors.push(`structure: ${match} — unknown class folder "${cls}" (allowed: ${CLASSES.join(', ')})`) - continue - } - if (!/^\d{4}-\d{2}-\d{2}-.+\.md$/.test(base)) { - errors.push(`structure: ${match} — filename must be yyyy-mm-dd-topic.md`) - continue - } - const firstLine = readFileSync(resolve(rfcRoot, match), 'utf8').split('\n', 1)[0] ?? '' - const h1 = /^#\s+(?:RFC:\s+)?(.+?)\s*$/.exec(firstLine) - if (!h1?.[1]) { - errors.push(`title: ${match} — first line must be an H1 (\`# RFC: <title>\` or \`# <title>\`), got: ${JSON.stringify(firstLine)}`) - continue - } - rfcs.push({ lifecycle, cls, base, rel: match, title: h1[1], date: base.slice(0, 10) }) - } - } - return { rfcs, errors } -} - -/** - * Render one lifecycle's section body: a `### {Class}` heading plus a - * `| Title | First proposed |` table for every non-empty class, in CLASSES - * order, rows sorted by date then filename. - */ -function renderLifecycle(rfcs: Rfc[], lifecycle: string): string { - const sections: string[] = [] - for (const cls of CLASSES) { - const rows = rfcs - .filter(r => r.lifecycle === lifecycle && r.cls === cls) - .sort((a, b) => a.date.localeCompare(b.date) || a.base.localeCompare(b.base)) - if (rows.length === 0) continue - const table = rows.map(r => `| [${r.title}](${r.rel}) | ${r.date} |`).join('\n') - sections.push(`### ${heading(cls)}\n\n| Title | First proposed |\n|---|---|\n${table}`) - } - return sections.join('\n\n') -} - -/** - * Render the complete `docs/rfc/INDEX.md` content: a generated-file banner - * followed by one `## {Lifecycle}` section per lifecycle in canonical order. - * The whole file is generated state — there is no curated region to preserve. - */ -export function renderIndex(rfcs: Rfc[]): string { - const parts = [ - '# RFC index', - '', - 'Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; `verify-rfc-classification` fails when this file is stale. The curated front door — layout, classification, when to write one, and the in-file format — is [README.md](README.md).', - ] - for (const lifecycle of LIFECYCLES) { - parts.push('', `## ${heading(lifecycle)}`, '', renderLifecycle(rfcs, lifecycle)) - } - return `${parts.join('\n')}\n` -} - -/** Matches an index-shaped table row (a `| [title](lifecycle/…) |` line) — generated state that must not appear in curated prose. */ -export const INDEX_ROW = /^\|\s*\[[^\]]+\]\((?:proposed|implemented|rejected)\// diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index f46de4475a..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,21 +335,21 @@ 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' }), pnpmScript('package-paths', 'verify-package-paths', { label: 'package paths' }), pnpmScript('package-readme-model-experience', 'verify-package-readme-model-experience', { label: 'package README model experience' }), pnpmScript('mermaid', 'verify-mermaid'), - pnpmScript('rfc-classification', 'verify-rfc-classification', { label: 'rfc classification' }), - pnpmScript('rfc-format', 'verify-rfc-format', { label: 'rfc format' }), + pnpmScript('agent-note-classification', 'verify-agent-note-classification', { label: 'agent note classification' }), + pnpmScript('agent-note-format', 'verify-agent-note-format', { label: 'agent note format' }), pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }), 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 35a957e57d..748f3fa13c 100644 --- a/scripts/translation-pairing.manifest.json +++ b/scripts/translation-pairing.manifest.json @@ -11,13 +11,27 @@ "docs/development.md", "docs/i18n/README.md", "docs/i18n/translation-rules.md", - "docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md", - "docs/rfc/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.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", "python/sdk-runtime/README.md", "python/sdk/README.md" ], "excluded": [ + ".agents/notes/AGENTS.md", + ".agents/notes/implemented/AGENTS.md", "docs/AGENTS.md", "docs/config-catalog.md", "docs/cordis-catalog/", diff --git a/scripts/translation-pairing.spec.ts b/scripts/translation-pairing.spec.ts index 39051969fc..2bdc4f4e15 100644 --- a/scripts/translation-pairing.spec.ts +++ b/scripts/translation-pairing.spec.ts @@ -50,13 +50,13 @@ describe('date-based pairing frontier', () => { const cutoff = '2026-07-14' it('enforces the cutoff day and every later day, but not the preceding day', () => { - expect(requiresPairByDate('docs/rfc/2026-07-13-before.md', cutoff)).toBe(false) - expect(requiresPairByDate('docs/rfc/2026-07-14-at-cutoff.md', cutoff)).toBe(true) - expect(requiresPairByDate('docs/rfc/2026-07-15-after.md', cutoff)).toBe(true) + expect(requiresPairByDate('.agents/notes/2026-07-13-before.md', cutoff)).toBe(false) + expect(requiresPairByDate('.agents/notes/2026-07-14-at-cutoff.md', cutoff)).toBe(true) + expect(requiresPairByDate('.agents/notes/2026-07-15-after.md', cutoff)).toBe(true) }) it('matches only a date at the start of the basename', () => { - expect(datedDocumentDate('docs/rfc/2026-07-14-proposal.md')).toBe('2026-07-14') + expect(datedDocumentDate('.agents/notes/2026-07-14-proposal.md')).toBe('2026-07-14') expect(datedDocumentDate('docs/release-notes-2026-07-14-alpha.md')).toBeUndefined() expect(requiresPairByDate('docs/release-notes-2026-07-14-alpha.md', cutoff)).toBe(false) }) diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 202f2efb6e..7daf60e344 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -41,7 +41,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" }, @@ -158,6 +157,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-agent-note-classification.ts b/scripts/verify-agent-note-classification.ts new file mode 100644 index 0000000000..776e155f4f --- /dev/null +++ b/scripts/verify-agent-note-classification.ts @@ -0,0 +1,27 @@ +/** + * Enforce Agent Note lifecycle/class paths and dated filenames. Structural rules + * are shared with `agent-note-tree.ts`; the closed classification contract lives + * in `.agents/notes/README.md`. + */ + +import { existsSync } from 'node:fs' +import { resolve } from 'node:path' +import { walkAgentNoteTree } from './agent-note-tree.ts' + +const { notes, errors } = walkAgentNoteTree() + +// Keep the former homes unavailable so new notes cannot silently escape this tree. +for (const legacyRoot of ['docs/rfc', 'docs/rfcs']) { + if (existsSync(resolve(import.meta.dirname, '..', legacyRoot))) { + errors.push(`legacy-path: ${legacyRoot}/ is forbidden — put Agent Notes under .agents/notes/`) + } +} + +if (errors.length === 0) { + console.log(`verify-agent-note-classification: ${notes.length} Agent Note(s) checked, structure consistent.`) + process.exit(0) +} + +console.error('verify-agent-note-classification: violations found:') +for (const e of errors) console.error(` ${e}`) +process.exit(1) diff --git a/scripts/verify-rfc-format.ts b/scripts/verify-agent-note-format.ts similarity index 60% rename from scripts/verify-rfc-format.ts rename to scripts/verify-agent-note-format.ts index 341f184abb..39018588e6 100644 --- a/scripts/verify-rfc-format.ts +++ b/scripts/verify-agent-note-format.ts @@ -1,22 +1,22 @@ /** - * Enforce RFC headers, lifecycle-specific sections, alternatives, and retired + * Enforce Agent Note headers, lifecycle-specific sections, alternatives, and retired * marker rules. Classification and filenames belong to the sibling tree gate; * translation structure belongs to the pairing gate. Exact format and - * grandfathering rules live in `docs/rfc/README.md`. + * grandfathering rules live in `.agents/notes/README.md`. */ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' -import { rfcRoot, walkRfcTree } from './rfc-index.ts' +import { agentNoteRoot, walkAgentNoteTree } from './agent-note-tree.ts' /** The date the format contract landed; the grandfather comment is valid only before it. */ const FORMAT_ADOPTED = '2026-07-05' -/** The exact comment a pre-format RFC carries in place of `## Alternatives considered`. */ -const GRANDFATHER = '<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->' +/** The exact comment a pre-format Agent Note carries in place of `## Alternatives considered`. */ +const GRANDFATHER = '<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->' /** The retired debt marker that flagged pre-format bodies; banned so it cannot creep back. */ -const LEGACY_MARKER = 'XXX: legacy ADR/RFC body format' +const LEGACY_MARKERS = ['XXX: legacy ADR/RFC body format', 'XXX: legacy ADR/Agent Note body format'] /** Status-line grammar per lifecycle folder. */ const STATUS: Record<string, RegExp> = { @@ -35,13 +35,13 @@ const REQUIRED: Record<string, string[]> = { /** Headings banned in `implemented/` — proposal-era spec-speak per the slop checklist. */ const BANNED_IMPLEMENTED = /^## (?:Proposal\b|Plan\b|Migration plan\b|Acceptance criteria\b)/i -const { rfcs, errors } = walkRfcTree() +const { notes, errors } = walkAgentNoteTree() -for (const rfc of rfcs) { +for (const note of notes) { const fail = (msg: string): void => { - errors.push(`format: ${rfc.rel} — ${msg}`) + errors.push(`format: ${note.rel} — ${msg}`) } - const lines = readFileSync(resolve(rfcRoot, rfc.rel), 'utf8').split('\n') + const lines = readFileSync(resolve(agentNoteRoot, note.rel), 'utf8').split('\n') // Format tokens inside fenced examples are not document structure. let inFence = false const prose = lines.filter((l) => { @@ -52,11 +52,11 @@ for (const rfc of rfcs) { return !inFence }) - if (!/^# RFC: \S/.test(lines[0] ?? '')) fail('line 1 must be `# RFC: <title>`') + if (!/^# Agent Note: \S/.test(lines[0] ?? '')) fail('line 1 must be `# Agent Note: <title>`') if (lines[1] !== '') fail('line 2 must be blank') - const status = STATUS[rfc.lifecycle] + const status = STATUS[note.lifecycle] if (status !== undefined && !status.test(lines[2] ?? '')) { - fail(`line 3 must match the ${rfc.lifecycle} status grammar (${String(status)})`) + fail(`line 3 must match the ${note.lifecycle} status grammar (${String(status)})`) } if (lines[3] !== '') fail('line 4 must be blank') const statusLines = prose.filter(l => l.startsWith('Status:') && l !== lines[2]) @@ -66,29 +66,29 @@ for (const rfc of rfcs) { const h2s = prose.filter(l => l.startsWith('## ')).map(l => l.trimEnd()) if (h2s[0] !== '## Problem') fail(`the first section must be \`## Problem\` (got ${JSON.stringify(h2s[0] ?? '<none>')})`) - for (const required of REQUIRED[rfc.lifecycle] ?? []) { + for (const required of REQUIRED[note.lifecycle] ?? []) { if (!h2s.includes(required)) fail(`missing the required \`${required}\` section`) } - if (rfc.lifecycle === 'implemented') { + if (note.lifecycle === 'implemented') { for (const h2 of h2s.filter(h => BANNED_IMPLEMENTED.test(h))) { - fail(`\`${h2}\` is a proposal-era heading; an implemented RFC states what is (fold it into Decision/Consequences/Testing)`) + fail(`\`${h2}\` is a proposal-era heading; an implemented Agent Note states what is (fold it into Decision/Consequences/Testing)`) } } const hasSection = h2s.includes('## Alternatives considered') const hasGrandfather = prose.includes(GRANDFATHER) if (hasSection && hasGrandfather) fail('carries both `## Alternatives considered` and the grandfather comment — drop the comment') - if (!hasSection && !hasGrandfather) fail('missing `## Alternatives considered` (a pre-format RFC whose alternatives are not reconstructible carries the grandfather comment instead — see docs/rfc/README.md § The file format)') - if (hasGrandfather && rfc.date >= FORMAT_ADOPTED) fail(`the grandfather comment is only valid for RFCs dated before ${FORMAT_ADOPTED}`) + if (!hasSection && !hasGrandfather) fail('missing `## Alternatives considered` (a pre-format Agent Note whose alternatives are not reconstructible carries the grandfather comment instead — see .agents/notes/README.md § The file format)') + if (hasGrandfather && note.date >= FORMAT_ADOPTED) fail(`the grandfather comment is only valid for Agent Notes dated before ${FORMAT_ADOPTED}`) - if (prose.some(l => l.includes(LEGACY_MARKER))) fail('carries the retired legacy-format debt marker') + if (prose.some(line => LEGACY_MARKERS.some(marker => line.includes(marker)))) fail('carries the retired legacy-format debt marker') } if (errors.length === 0) { - console.log(`verify-rfc-format: ${rfcs.length} RFC(s) checked, all conform to docs/rfc/README.md § The file format.`) + console.log(`verify-agent-note-format: ${notes.length} Agent Note(s) checked, all conform to .agents/notes/README.md § The file format.`) process.exit(0) } -console.error('verify-rfc-format: violations found:') +console.error('verify-agent-note-format: violations found:') for (const e of errors) console.error(` ${e}`) process.exit(1) diff --git a/scripts/verify-doc-refs.ts b/scripts/verify-doc-refs.ts index e1885f7b6e..8e181a1d85 100644 --- a/scripts/verify-doc-refs.ts +++ b/scripts/verify-doc-refs.ts @@ -1,7 +1,8 @@ /** - * Verify root-relative `docs/*.md` tokens in repo-authored TypeScript. The - * textual scan requires the extension, checks matching string literals too, - * and excludes built declarations and vendored source. + * Verify root-relative documentation paths in repo-authored TypeScript. The + * textual scan covers `docs/*.md` and `.agents/notes/*.md`, requires the + * extension, checks matching string literals too, and excludes built + * declarations and vendored source. */ import { existsSync } from 'node:fs' @@ -18,9 +19,9 @@ const isExcluded = (p: string): boolean => p.includes('/lib/') || p.endsWith('.d.ts') || p.startsWith('vendor/') /** Root-relative Markdown path token, excluding trailing prose. */ -const DOC_REF = /\bdocs\/[A-Za-z0-9._/-]+\.md/g +const DOC_REF = /(?:\bdocs|\.agents\/notes)\/[A-Za-z0-9._/-]+\.md/g -/** Find every broken `docs/….md` reference in one TypeScript file. */ +/** Find every broken root-relative documentation reference in one TypeScript file. */ function findViolations(absPath: string): Violation[] { return findReferenceViolations(root, absPath, DOC_REF, ref => ref, ref => !existsSync(resolve(root, ref))) } @@ -30,11 +31,11 @@ const all = files.flatMap(file => findViolations(file.abs)) const checked = files.length if (all.length === 0) { - console.log(`verify-doc-refs: ${checked} file(s) checked, all docs/*.md references resolve.`) + console.log(`verify-doc-refs: ${checked} file(s) checked, all documentation references resolve.`) process.exit(0) } -console.error('verify-doc-refs: broken docs/*.md references found in source comments (target does not exist):') +console.error('verify-doc-refs: broken documentation references found in source comments (target does not exist):') for (const v of all) { console.error(` ${v.file}:${v.line} ${v.ref}`) } diff --git a/scripts/verify-md-links.ts b/scripts/verify-md-links.ts index f0d61feb6a..23da09db5f 100644 --- a/scripts/verify-md-links.ts +++ b/scripts/verify-md-links.ts @@ -17,6 +17,7 @@ const root = resolve(import.meta.dirname, '..') const PATTERNS = [ 'README.md', 'README.zh.md', + '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', diff --git a/scripts/verify-md-wrap.ts b/scripts/verify-md-wrap.ts index f9679e5537..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' @@ -17,6 +18,7 @@ const root = resolve(import.meta.dirname, '..') const PATTERNS = [ 'README.md', 'README.zh.md', + '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', @@ -34,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-mermaid.ts b/scripts/verify-mermaid.ts index c487c2fd5c..d44ca9bcdf 100644 --- a/scripts/verify-mermaid.ts +++ b/scripts/verify-mermaid.ts @@ -17,6 +17,7 @@ const root = resolve(import.meta.dirname, '..') const PATTERNS = [ 'README.md', 'README.zh.md', + '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', diff --git a/scripts/verify-package-paths.ts b/scripts/verify-package-paths.ts index bf03da4247..0cc0f63536 100644 --- a/scripts/verify-package-paths.ts +++ b/scripts/verify-package-paths.ts @@ -14,6 +14,7 @@ const root = resolve(import.meta.dirname, '..') /** Markdown + repo-authored TypeScript that may cite package paths. */ const PATTERNS = [ 'README.md', + '.agents/notes/**/*.md', 'docs/**/*.md', 'packages/*/*.md', 'packages/*/*/*.md', diff --git a/scripts/verify-package-readme-limitations.ts b/scripts/verify-package-readme-limitations.ts index 042db1787c..22ac66a278 100644 --- a/scripts/verify-package-readme-limitations.ts +++ b/scripts/verify-package-readme-limitations.ts @@ -2,7 +2,7 @@ * Doc-sync gate for the canonical package-README limitations section. It scans * package manifests, rejects missing or variant sections, and requires one * top-level bullet; audited packages in {@link NO_LIMITATIONS} must omit it. - * See the [limitations RFC](../docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md). + * See the [limitations Agent Note](../.agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md). */ import { existsSync, globSync, readFileSync } from 'node:fs' diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 93f30621c5..41a13cfbb8 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -2,7 +2,7 @@ * Doc-sync gate for package README Model Experience sections. It validates * audited package classifications, model/token/KV-cache fields, package-owned * text blocks, generated-catalog links, and final-section order. See the - * [Model Experience RFC](../docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.md). + * [Model Experience Agent Note](../.agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md). */ import { existsSync, globSync, readFileSync } from 'node:fs' @@ -48,10 +48,12 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = { '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-rfc-classification.ts b/scripts/verify-rfc-classification.ts deleted file mode 100644 index 0511efd2e7..0000000000 --- a/scripts/verify-rfc-classification.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Enforce RFC lifecycle/class paths, dated filenames, and titles; verify the - * generated index and reject index rows in the curated README. Structural rules - * and rendering are shared with `rfc-index.ts`; the closed classification - * contract lives in `docs/rfc/README.md`. - */ - -import { readFileSync } from 'node:fs' -import { resolve } from 'node:path' -import { INDEX_ROW, renderIndex, rfcRoot, walkRfcTree } from './rfc-index.ts' - -const { rfcs, errors } = walkRfcTree() - -if (errors.length === 0) { - let index: string | undefined - try { - index = readFileSync(resolve(rfcRoot, 'INDEX.md'), 'utf8') - } catch { - // A missing INDEX.md is reported below as staleness, exactly like a drifted one. - } - if (renderIndex(rfcs) !== index) { - errors.push('index: docs/rfc/INDEX.md is stale or missing — run `pnpm run gen-rfc-index` and commit the result') - } - const readme = readFileSync(resolve(rfcRoot, 'README.md'), 'utf8') - for (const line of readme.split('\n')) { - if (INDEX_ROW.test(line)) { - errors.push(`readme: index-shaped row in the curated README (the list lives in INDEX.md): ${JSON.stringify(line.slice(0, 80))}`) - } - } -} - -if (errors.length === 0) { - console.log(`verify-rfc-classification: ${rfcs.length} RFC(s) checked, structure and index consistent.`) - process.exit(0) -} - -console.error('verify-rfc-classification: violations found:') -for (const e of errors) console.error(` ${e}`) -process.exit(1) diff --git a/scripts/verify-translation-pairing.ts b/scripts/verify-translation-pairing.ts index ced55d5be1..860e193b4f 100644 --- a/scripts/verify-translation-pairing.ts +++ b/scripts/verify-translation-pairing.ts @@ -24,8 +24,18 @@ const root = resolve(import.meta.dirname, '..') const listMode = process.argv.includes('--list') const writeMode = process.argv.includes('--write') -/** Scope of the bilingual contract: the root README, the docs tree, and the Python SDK tree. */ -const SCOPE_PATTERNS = ['README.md', 'README.zh.md', 'README.i18n.yaml', 'docs/**/*.md', 'docs/**/*.i18n.yaml', 'python/**/*.md', 'python/**/*.i18n.yaml'] +/** Scope of the bilingual contract: root docs, Agent Notes, the docs tree, and the Python SDK tree. */ +const SCOPE_PATTERNS = [ + 'README.md', + 'README.zh.md', + 'README.i18n.yaml', + '.agents/notes/**/*.md', + '.agents/notes/**/*.i18n.yaml', + 'docs/**/*.md', + 'docs/**/*.i18n.yaml', + 'python/**/*.md', + 'python/**/*.i18n.yaml', +] const manifest = parseTranslationPairingManifest(readFileSync(join(root, 'scripts/translation-pairing.manifest.json'), 'utf8')) @@ -121,8 +131,8 @@ for (const req of manifest.required) { } } -// 2. Date-named documents (RFCs) dated on/after the requiredSince cutoff merge -// bilingual: a new RFC lands with its pair or not at all. Deterministic from +// 2. Date-named documents (Agent Notes) dated on/after the requiredSince cutoff merge +// bilingual: a new Agent Note lands with its pair or not at all. Deterministic from // the filename alone — no git history, so it holds on shallow CI checkouts. for (const source of sources) { if (isExcluded(source)) continue diff --git a/scripts/verify-type-equiv.ts b/scripts/verify-type-equiv.ts index d703536f17..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', '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/<group>/<pkg>` and `vendor/<pkg>`. */ -function knownPackages(): Set<string> { - const names = new Set<string>() - 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<string, CatalogEntry> | 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<string> { - const sf = ts.createSourceFile('paste.ts', paste, ts.ScriptTarget.Latest, true) - const keys = new Set<string>() - const addMembers = (members: ts.NodeArray<ts.TypeElement>): 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<string, Set<string>>() -function allowedConfigKeys(entry: CatalogEntry): Set<string> { - 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<string, unknown> | null { - if (typeof value !== 'object' || value === null || Array.isArray(value)) return null - if ('__jsExpr' in value) return null - return value as Record<string, unknown> -} - -/** Check one cordis.yml entry list (recursing into `group:` sub-lists). */ -function checkEntryList( - items: unknown[], - known: Set<string>, - 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 c056aa43db..9e87410441 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -43,16 +43,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 52337c6a71..1fbd7e362c 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" }, @@ -57,15 +59,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/vitest.config.ts b/vitest.config.ts index c0946e2e10..4f4baa06ba 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -19,8 +19,8 @@ export default defineConfig({ exclude: ['packages/*/*/src/types.ts', 'packages/*/*/src/bin.ts', 'packages/*/*/src/worker.ts'], // 100% or it doesn't merge (docs/testing.md: excessive tests are welcome). // Per-file so a well-covered big file can't subsidize a bare one. - // Every v8 ignore comment must carry a reason — see the quality-gates RFC - // (docs/rfc/implemented/process/2026-06-11-quality-gates.md). + // Every v8 ignore comment must carry a reason — see the quality-gates Agent Note + // (.agents/notes/implemented/process/2026-06-11-quality-gates.md). thresholds: { perFile: true, statements: 100, 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<string, DocsPage[]>() + 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<DefaultTheme.Config, 'search' | 'socialLinks' | 'editLink'> = { + 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 3a2b11db3c..0000000000 --- a/website/.vitepress/config/api-sidebar.json +++ /dev/null @@ -1,126 +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.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 <style> tag - // per formula, which Vue's template compiler rejects ("Tags with side - // effect … are ignored in client component templates"); v4 emits pure SVG. - markdown: { math: true }, - - locales: { - 'zh-CN': zhCN, - }, - - themeConfig: { - socialLinks: [ - { icon: 'github', link: 'https://github.com/deepseek-harness/deepseek-harness' }, - ], - }, -}) diff --git a/website/.vitepress/config/zh-CN.ts b/website/.vitepress/config/zh-CN.ts deleted file mode 100644 index 0cea119777..0000000000 --- a/website/.vitepress/config/zh-CN.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress' -import apiSidebarData from './api-sidebar.json' - -const guideSidebar: DefaultTheme.SidebarItem[] = [ - { - text: '入门', - items: [ - { text: '介绍', link: '/zh-CN/guide/' }, - { text: '快速开始', link: '/zh-CN/guide/quickstart' }, - { text: '配置文件', link: '/zh-CN/guide/config' }, - ], - }, -] - -const developSidebar: DefaultTheme.SidebarItem[] = [ - { - text: '基础', - items: [ - { text: '第一个插件', link: '/zh-CN/develop/basic/' }, - { text: '开发一个 Tool', link: '/zh-CN/develop/basic/tool' }, - { text: '插件配置', link: '/zh-CN/develop/basic/config' }, - ], - }, - { - text: '框架能力', - items: [ - { text: '插件与生命周期', link: '/zh-CN/develop/framework/' }, - { text: '服务与依赖', link: '/zh-CN/develop/framework/service' }, - { text: '事件系统', link: '/zh-CN/develop/framework/events' }, - ], - }, - { - text: '实战', - items: [ - { text: '能力的三层拆分', link: '/zh-CN/develop/practice/' }, - { text: 'LLM 适配器', link: '/zh-CN/develop/practice/llm-adapter' }, - ], - }, -] - -// The API section sidebar is GENERATED (scripts/gen-website-api.ts writes -// api-sidebar.json alongside the pages), so navigation can never drift from -// the generated page set. Only the hand-written hub link lives here. -const apiSidebar: DefaultTheme.SidebarItem[] = [ - { - text: '框架 API', - items: [ - { text: '总览', link: '/zh-CN/api/' }, - ...apiSidebarData.cordis, - ], - }, - { - text: 'Harness API', - items: apiSidebarData.harness, - }, -] - -const designSidebar: DefaultTheme.SidebarItem[] = [ - { - text: '系统设计', - items: [ - { text: '概述', link: '/zh-CN/design/' }, - { text: '可组合性与插件系统', link: '/zh-CN/design/composability' }, - { text: '作用与余作用', link: '/zh-CN/design/effects-coeffects' }, - { text: '可逆作用', link: '/zh-CN/design/revertible-effects' }, - { text: '响应式余作用', link: '/zh-CN/design/reactive-coeffects' }, - { text: '上下文模型', link: '/zh-CN/design/context-model' }, - ], - }, -] - -export const zhCN: LocaleSpecificConfig<DefaultTheme.Config> = { - label: '简体中文', - lang: 'zh-CN', - themeConfig: { - nav: [ - { text: '入门', link: '/zh-CN/guide/', activeMatch: '/zh-CN/guide/' }, - { text: '开发', link: '/zh-CN/develop/basic/', activeMatch: '/zh-CN/develop/' }, - { text: 'API', link: '/zh-CN/api/', activeMatch: '/zh-CN/api/' }, - { text: '设计', link: '/zh-CN/design/', activeMatch: '/zh-CN/design/' }, - ], - sidebar: { - '/zh-CN/guide/': guideSidebar, - '/zh-CN/develop/': developSidebar, - '/zh-CN/api/': apiSidebar, - '/zh-CN/design/': designSidebar, - }, - // level [2,3]: the generated API pages put each member at h3 (### ctx.foo) - // under an h2 scope/statics group — both belong in the page outline. - outline: { label: '本页目录', level: [2, 3] }, - docFooter: { prev: '上一篇', next: '下一篇' }, - }, -} diff --git a/website/docs.ts b/website/docs.ts new file mode 100644 index 0000000000..528fa36110 --- /dev/null +++ b/website/docs.ts @@ -0,0 +1,305 @@ +/** + * Canonical publication manifest for the documentation website. + * + * Markdown stays in its owning repository tier. This manifest maps each + * canonical source into matching route trees for both site locales; when a + * translation is absent, both routes intentionally project the available + * source instead of copying Markdown. + */ + +/** Locale key used by the VitePress site. */ +export type DocsLocale = 'root' | 'en' + +/** Sidebar collection rendered for one locale and top-level module. */ +type DocsSidebar = + | 'zh-guide' + | 'zh-develop' + | 'zh-reference' + | 'en-guide' + | 'en-develop' + | 'en-reference' + +/** A page projected into the VitePress source tree. */ +export interface DocsPage { + /** VitePress locale whose route tree owns this projection. */ + locale: DocsLocale + /** Language of the canonical source currently projected at this route. */ + contentLocale: 'zh-CN' | 'en-US' + /** Repository-relative canonical Markdown source. */ + source: string + /** VitePress route, including the `.md` suffix. */ + route: string + /** Navigation label shown in the sidebar. */ + label: string + /** Sidebar collection that owns the page, or null for a locale home page. */ + sidebar: DocsSidebar | null + /** Section label within the sidebar. */ + section: string + /** Stable order within the section. */ + order: number + /** Additional repository paths that resolve to this page. */ + sourceAliases?: string[] +} + +interface MirroredPage { + source: string | Record<DocsLocale, string> + route: string + contentLocale: DocsPage['contentLocale'] | Record<DocsLocale, DocsPage['contentLocale']> + label: Record<DocsLocale, string> + sidebar: Record<DocsLocale, DocsSidebar | null> + section: Record<DocsLocale, string> + order: number + sourceAliases?: string[] | Partial<Record<DocsLocale, string[]>> +} + +type PairedPage = Omit<MirroredPage, 'source' | 'contentLocale' | 'sourceAliases'> & { + /** English side of a sibling `foo.md` / `foo.zh.md` pair. */ + source: string + /** Language-neutral repository aliases, such as the directory of an index page. */ + sourceAliases?: string[] +} + +function localized<T>(value: T | Record<DocsLocale, T>, locale: DocsLocale): T { + return typeof value === 'object' && value !== null && !Array.isArray(value) + ? (value as Record<DocsLocale, T>)[locale] + : value +} + +function mirroredPages(pages: MirroredPage[]): DocsPage[] { + return pages.flatMap(page => (['root', 'en'] as const).map((locale) => { + const aliases = page.sourceAliases === undefined + ? undefined + : Array.isArray(page.sourceAliases) ? page.sourceAliases : page.sourceAliases[locale] + return { + locale, + contentLocale: localized(page.contentLocale, locale), + source: localized(page.source, locale), + route: locale === 'root' ? page.route : `en/${page.route}`, + label: page.label[locale], + sidebar: page.sidebar[locale], + section: page.section[locale], + order: page.order, + ...(aliases === undefined ? {} : { sourceAliases: aliases }), + } + })) +} + +function pairedPages(pages: PairedPage[]): DocsPage[] { + return mirroredPages(pages.map((page) => { + const chineseSource = page.source.replace(/\.md$/, '.zh.md') + const sharedAliases = page.sourceAliases ?? [] + return { + ...page, + source: { root: chineseSource, en: page.source }, + contentLocale: { root: 'zh-CN', en: 'en-US' }, + sourceAliases: { + root: [...sharedAliases, page.source], + en: [...sharedAliases, chineseSource], + }, + } + })) +} + +const homeAndGuide = pairedPages([ + { + source: 'docs/user/index.md', + route: 'index.md', + label: { root: 'DeepSeek Harness', en: 'DeepSeek Harness' }, + sidebar: { root: null, en: null }, + section: { root: '首页', en: 'Home' }, + order: 0, + }, + { + source: 'docs/user/guide/index.md', + route: 'guide/index.md', + label: { root: '介绍', en: 'Introduction' }, + sidebar: { root: 'zh-guide', en: 'en-guide' }, + section: { root: '入门', en: 'Guide' }, + order: 1, + sourceAliases: ['docs/user/guide'], + }, + { + source: 'docs/user/guide/quickstart.md', + route: 'guide/quickstart.md', + label: { root: '快速开始', en: 'Quick start' }, + sidebar: { root: 'zh-guide', en: 'en-guide' }, + section: { root: '入门', en: 'Guide' }, + order: 2, + }, + { + source: 'docs/user/guide/config.md', + route: 'guide/config.md', + label: { root: '配置文件', en: 'Configuration' }, + sidebar: { root: 'zh-guide', en: 'en-guide' }, + section: { root: '入门', en: 'Guide' }, + order: 3, + }, +]) + +const develop = pairedPages([ + { + source: 'docs/user/develop/basic/index.md', + route: 'develop/basic/index.md', + label: { root: '第一个插件', en: 'First plugin' }, + sidebar: { root: 'zh-develop', en: 'en-develop' }, + section: { root: '基础', en: 'Basics' }, + order: 1, + sourceAliases: ['docs/user/develop/basic'], + }, + { + source: 'docs/user/develop/basic/tool.md', + route: 'develop/basic/tool.md', + label: { root: '开发一个 Tool', en: 'Build a tool' }, + sidebar: { root: 'zh-develop', en: 'en-develop' }, + section: { root: '基础', en: 'Basics' }, + order: 2, + }, + { + source: 'docs/user/develop/basic/config.md', + route: 'develop/basic/config.md', + label: { root: '插件配置', en: 'Plugin configuration' }, + sidebar: { root: 'zh-develop', en: 'en-develop' }, + section: { root: '基础', en: 'Basics' }, + order: 3, + }, + { + source: 'docs/user/develop/framework/index.md', + route: 'develop/framework/index.md', + label: { root: '插件与生命周期', en: 'Plugin lifecycle' }, + sidebar: { root: 'zh-develop', en: 'en-develop' }, + section: { root: '框架能力', en: 'Framework' }, + order: 1, + sourceAliases: ['docs/user/develop/framework'], + }, + { + source: 'docs/user/develop/framework/service.md', + route: 'develop/framework/service.md', + label: { root: '服务与依赖', en: 'Services and dependencies' }, + sidebar: { root: 'zh-develop', en: 'en-develop' }, + section: { root: '框架能力', en: 'Framework' }, + order: 2, + }, + { + source: 'docs/user/develop/framework/events.md', + route: 'develop/framework/events.md', + label: { root: '事件系统', en: 'Event system' }, + sidebar: { root: 'zh-develop', en: 'en-develop' }, + section: { root: '框架能力', en: 'Framework' }, + order: 3, + }, + { + source: 'docs/user/develop/practice/index.md', + route: 'develop/practice/index.md', + label: { root: '能力的三层拆分', en: 'Capability layering' }, + sidebar: { root: 'zh-develop', en: 'en-develop' }, + section: { root: '实战', en: 'Practice' }, + order: 1, + sourceAliases: ['docs/user/develop/practice'], + }, + { + source: 'docs/user/develop/practice/llm-adapter.md', + route: 'develop/practice/llm-adapter.md', + label: { root: 'LLM 适配器', en: 'LLM adapter' }, + sidebar: { root: 'zh-develop', en: 'en-develop' }, + section: { root: '实战', en: 'Practice' }, + order: 2, + }, +]) + +const reference = mirroredPages([ + ...([ + ['docs/architecture.md', 'reference/index.md', '架构', 'Architecture'], + ['docs/cordis-primer.md', 'reference/cordis-primer.md', 'Cordis 入门', 'Cordis primer'], + ['docs/capability-seams.md', 'reference/capability-seams.md', '能力服务', 'Capability services'], + ['docs/agent-lifecycle.md', 'reference/agent-lifecycle.md', 'Agent 生命周期', 'Agent lifecycle'], + ['docs/tool-execution-pipeline.md', 'reference/tool-execution-pipeline.md', 'Tool 执行', 'Tool execution'], + ] as const).map(([source, route, rootLabel, enLabel], order): MirroredPage => ({ + source, + route, + contentLocale: 'en-US', + label: { root: rootLabel, en: enLabel }, + sidebar: { root: 'zh-reference', en: 'en-reference' }, + section: { root: '概念', en: 'Concepts' }, + order, + })), + ...([ + ['docs/config-catalog.md', 'reference/config-catalog.md', '插件配置', 'Plugin configuration'], + ['docs/tool-catalog.md', 'reference/tool-catalog.md', 'Tool Schema', 'Tool schemas'], + ['docs/cordis-catalog/services.md', 'reference/cordis-catalog/services.md', '服务', 'Services'], + ['docs/cordis-catalog/events.md', 'reference/cordis-catalog/events.md', '事件', 'Events'], + ['docs/persistence-catalog.md', 'reference/persistence-catalog.md', '持久化事件', 'Persistence events'], + ] as const).map(([source, route, rootLabel, enLabel], order): MirroredPage => ({ + source, + route, + contentLocale: 'en-US', + label: { root: rootLabel, en: enLabel }, + sidebar: { root: 'zh-reference', en: 'en-reference' }, + section: { root: '生成参考', en: 'Generated reference' }, + order, + })), + ...([ + ['context.md', 'Context', 'Context'], + ['events.md', 'Events', 'Events'], + ['fiber.md', 'Fiber', 'Fiber'], + ['registry.md', 'Plugin Registry', 'Plugin Registry'], + ['service.md', 'Service', 'Service'], + ] as const).map(([file, rootLabel, enLabel], order): MirroredPage => ({ + source: `docs/cordis-catalog/core/${file}`, + route: `reference/cordis-api/${file}`, + contentLocale: 'en-US', + label: { root: rootLabel, en: enLabel }, + sidebar: { root: 'zh-reference', en: 'en-reference' }, + section: { root: 'Cordis API', en: 'Cordis Core API' }, + order, + })), + ...([ + ['core.md', '核心数据结构', 'Core data structures'], + ['scope.md', '作用域', 'Scopes'], + ['session.md', '会话', 'Sessions'], + ['system-prompt.md', '系统提示词', 'System prompts'], + ['tools.md', '工具', 'Tools'], + ['llm-streaming.md', 'LLM 流式响应', 'LLM streaming'], + ['bash.md', 'Bash 执行', 'Bash execution'], + ['filesystem.md', '文件系统', 'Filesystem'], + ['code-runtime.md', '代码运行时', 'Code runtime'], + ['compaction.md', '上下文压缩', 'Compaction'], + ['subagent.md', '子代理', 'Subagents'], + ['workflow.md', '工作流', 'Workflows'], + ['skills.md', '技能', 'Skills'], + ['approval.md', '审批', 'Approvals'], + ['user-interaction.md', '用户交互', 'User interaction'], + ['sandbox.md', '沙箱', 'Sandboxing'], + ['web.md', 'Web 访问', 'Web access'], + ['persistence.md', '会话持久化', 'Session persistence'], + ] as const).map(([file, rootLabel, enLabel], order): MirroredPage => ({ + source: `docs/core-data-structures/${file}`, + route: `reference/core-data-structures/${file}`, + contentLocale: 'en-US', + label: { root: rootLabel, en: enLabel }, + sidebar: { root: 'zh-reference', en: 'en-reference' }, + section: { root: '数据结构', en: 'Data structures' }, + order, + ...(file === 'core.md' ? { sourceAliases: ['docs/core-data-structures'] } : {}), + })), + ...([ + ['adding-a-package.md', '新增 Package', 'Adding a package'], + ['adding-a-tool.md', '新增 Tool', 'Adding a tool'], + ['adding-an-llm-adapter.md', '新增 LLM Adapter', 'Adding an LLM adapter'], + ['extension-cookbook.md', '扩展模式', 'Extension patterns'], + ] as const).map(([file, rootLabel, enLabel], order): MirroredPage => ({ + source: `docs/cookbook/${file}`, + route: `reference/cookbook/${file}`, + contentLocale: 'en-US', + label: { root: rootLabel, en: enLabel }, + sidebar: { root: 'zh-reference', en: 'en-reference' }, + section: { root: '开发手册', en: 'Cookbook' }, + order, + })), +]) + +/** Every canonical page published by the documentation website. */ +export const docsPages: DocsPage[] = [ + ...homeAndGuide, + ...develop, + ...reference, +] diff --git a/website/package.json b/website/package.json index 3c582ed0a9..a2279418bb 100644 --- a/website/package.json +++ b/website/package.json @@ -4,13 +4,19 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "vitepress dev . --port 5173 --open", + "dev": "vitepress dev . --host 127.0.0.1 --port 5173", "build": "vitepress build .", - "preview": "vitepress preview ." + "preview": "vitepress preview . --host 127.0.0.1 --port 4173" }, "devDependencies": { - "markdown-it-mathjax3": "^4.3.2", - "vitepress": "^1.6.3", - "vue": "^3.5.13" + "@braintree/sanitize-url": "7.1.2", + "cytoscape": "3.34.0", + "cytoscape-cose-bilkent": "4.1.0", + "dayjs": "1.11.21", + "debug": "4.4.3", + "mermaid": "11.16.0", + "vite": "^5.4.14", + "vitepress": "^1.6.4", + "vitepress-plugin-mermaid": "^2.0.17" } } diff --git a/website/zh-CN/api/harness/agent-loop.md b/website/zh-CN/api/harness/agent-loop.md deleted file mode 100644 index 9a43a1ba14..0000000000 --- a/website/zh-CN/api/harness/agent-loop.md +++ /dev/null @@ -1,76 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.agentLoop - -`AgentLoop` — provided by `@deepseek-ai/dsh-agent-loop`. - -Concrete agent factory and driver service. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L407) - -### ctx.agentLoop.create(id, options?, meta?) - -```ts website-api -/** - * Create an agent and session under one caller-supplied identity, owned by - * the accessing fiber. Constructor-driven config calls mint a fresh combined - * id before entering this boundary. - * @param id - shared agent/session identity. - * @param options - concrete loop options. - * @param meta - optional fresh-session workspace metadata. - * @returns the published running agent. - */ -create(id: SessionId, options: AgentOptions = {}, meta: Pick<SessionHeader, 'cwd'> = {}): Agent -``` - -Create an agent and session under one caller-supplied identity, owned by the accessing fiber. Constructor-driven config calls mint a fresh combined id before entering this boundary. - -- `id` — shared agent/session identity. -- `options` — concrete loop options. -- `meta` — optional fresh-session workspace metadata. - -**Returns** the published running agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L542) - -### ctx.agentLoop.createAgent(ownerCtx, options) - -```ts website-api -/** - * Create an owned agent on a caller-supplied session id. - * @param ownerCtx - caller context that structurally owns the transaction. - * @param options - identities, session seed/metadata, loop options, setup, and cancellation. - * @returns the published handle. - */ -async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle> -``` - -Create an owned agent on a caller-supplied session id. - -- `ownerCtx` — caller context that structurally owns the transaction. -- `options` — identities, session seed/metadata, loop options, setup, and cancellation. - -**Returns** the published handle. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L564) - -### ctx.agentLoop.resume(ownerCtx, options) - -```ts website-api -/** - * Resume an owned agent from the configured persistence service. - * @param ownerCtx - caller context that owns load, setup, and the live lifecycle. - * @param options - persisted identity, loop options, setup, and cancellation. - * @returns the published handle. - */ -async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle> -``` - -Resume an owned agent from the configured persistence service. - -- `ownerCtx` — caller context that owns load, setup, and the live lifecycle. -- `options` — persisted identity, loop options, setup, and cancellation. - -**Returns** the published handle. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L596) diff --git a/website/zh-CN/api/harness/agents.md b/website/zh-CN/api/harness/agents.md deleted file mode 100644 index bba6a7a5d4..0000000000 --- a/website/zh-CN/api/harness/agents.md +++ /dev/null @@ -1,331 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.agents - -`AgentRegistry` — provided by `@deepseek-ai/dsh-agent`. - -Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory. -Initiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L217) - -### ctx.agents.currentInitiator() - -```ts website-api -/** - * Read the Agent that initiated the inherited asynchronous driver chain. - * Use this optional form for logging, tracing, metrics, or host attribution - * that also supports agentless calls. When a parent creates a child, setup - * reports the causal parent while `agentCtx.agent` identifies the child. - * @returns the inherited Agent, or `undefined` outside an initiator boundary - * and inside an explicit clearing boundary. - * @throws when this service instance has been disposed. - */ -currentInitiator(): Agent | undefined -``` - -Read the Agent that initiated the inherited asynchronous driver chain. Use this optional form for logging, tracing, metrics, or host attribution that also supports agentless calls. When a parent creates a child, setup reports the causal parent while `agentCtx.agent` identifies the child. - -**Returns** the inherited Agent, or `undefined` outside an initiator boundary and inside an explicit clearing boundary. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L256) - -### ctx.agents.requireInitiator() - -```ts website-api -/** - * Read the initiating Agent and fail when no initiator boundary is active. - * Use this for private helpers contractually below a driver, or for a - * deployment-owned outbound request whose contract forbids agentless calls. - * Generic or direct-call seams use optional lookup or explicit request fields. - * @returns the inherited Agent. - * @throws when no initiator is active or this service instance has been disposed. - */ -requireInitiator(): Agent -``` - -Read the initiating Agent and fail when no initiator boundary is active. Use this for private helpers contractually below a driver, or for a deployment-owned outbound request whose contract forbids agentless calls. Generic or direct-call seams use optional lookup or explicit request fields. - -**Returns** the inherited Agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L269) - -### ctx.agents.withInitiator(agent, operation) - -```ts website-api -/** - * Run an operation with one exact Agent as its process-local initiator. The - * exact synchronous value or Promise returned by the operation is preserved. - * Custom drivers and test harnesses wrap their complete returned foreground - * lifetime. - * A queue or wire receiver may establish this boundary only after validating - * explicit identity and resolving the exact live Agent; this method does neither. - * Detached work remains owned by the subsystem that starts it. - * @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization. - * @param operation - synchronous or asynchronous operation to invoke. - * @returns the exact value returned by `operation`. - * @throws when the initiator scope is closing/disposed, or when `operation` throws. - */ -withInitiator<T>(agent: Agent, operation: () => T): T -``` - -Run an operation with one exact Agent as its process-local initiator. The exact synchronous value or Promise returned by the operation is preserved. Custom drivers and test harnesses wrap their complete returned foreground lifetime. A queue or wire receiver may establish this boundary only after validating explicit identity and resolving the exact live Agent; this method does neither. Detached work remains owned by the subsystem that starts it. - -- `agent` — initiating Agent to inherit; presence is neither liveness proof nor authorization. -- `operation` — synchronous or asynchronous operation to invoke. - -**Returns** the exact value returned by `operation`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L288) - -### ctx.agents.withoutInitiator(operation) - -```ts website-api -/** - * Run an operation inside a boundary that hides any inherited initiating - * Agent. The exact synchronous value or Promise is preserved. - * Use this while creating lazy shared timers, queue pumps, pool maintenance, - * watchers, or exporters so they do not inherit the first Agent that happens - * to initialize them. It clears only initiator attribution, not explicit - * fields, and does not own or drain detached resources. - * @param operation - synchronous or asynchronous operation to invoke without an initiator. - * @returns the exact value returned by `operation`. - * @throws when the initiator scope is closing/disposed, or when `operation` throws. - */ -withoutInitiator<T>(operation: () => T): T -``` - -Run an operation inside a boundary that hides any inherited initiating Agent. The exact synchronous value or Promise is preserved. Use this while creating lazy shared timers, queue pumps, pool maintenance, watchers, or exporters so they do not inherit the first Agent that happens to initialize them. It clears only initiator attribution, not explicit fields, and does not own or drain detached resources. - -- `operation` — synchronous or asynchronous operation to invoke without an initiator. - -**Returns** the exact value returned by `operation`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L303) - -### ctx.agents.setFactory(factory) - -```ts website-api -/** - * Register the agent-creation factory (the loop calls this on construction, - * effect-scoped). A traced Cordis service is canonicalized to its concrete - * target; each create/resume call is then traced through that caller's - * context so ownership follows the caller without stacking proxy layers. - * Throws if a factory is already registered. Returns the disposer; on - * dispose the factory slot is cleared. - * @param factory - the loop-owned factory {@link create}/{@link resume} delegate to. - * @returns the disposer that clears the factory slot. The exact - * Cordis effect disposer (single-shot): composite (generator) effects may - * yield it directly — exact identity nests the teardown in order. - */ -setFactory(factory: AgentFactory): () => void -``` - -Register the agent-creation factory (the loop calls this on construction, effect-scoped). A traced Cordis service is canonicalized to its concrete target; each create/resume call is then traced through that caller's context so ownership follows the caller without stacking proxy layers. Throws if a factory is already registered. Returns the disposer; on dispose the factory slot is cleared. - -- `factory` — the loop-owned factory `create`/`resume` delegate to. - -**Returns** the disposer that clears the factory slot. The exact Cordis effect disposer (single-shot): composite (generator) effects may yield it directly — exact identity nests the teardown in order. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L319) - -### ctx.agents.create(options) - -```ts website-api -/** - * Create and publish a new agent through the registered factory. - * Distinct from {@link register} (which records an already-constructed - * agent): this constructs the agent and its session. Rejects if no factory is - * registered or creation/setup fails. The resolved {@link AgentHandle} lets - * the owner tear down exactly this agent. - * @param options - shared identity, session seed/metadata, and agent options. - * @returns the handle after setup, rollback-covered publication, and loop start complete. - */ -async create(options: CreateAgentOptions): Promise<AgentHandle> -``` - -Create and publish a new agent through the registered factory. Distinct from register (which records an already-constructed agent): this constructs the agent and its session. Rejects if no factory is registered or creation/setup fails. The resolved AgentHandle lets the owner tear down exactly this agent. - -- `options` — shared identity, session seed/metadata, and agent options. - -**Returns** the handle after setup, rollback-covered publication, and loop start complete. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L352) - -### ctx.agents.resume(options) - -```ts website-api -/** - * Load a persisted session and resume an agent on it through the registered - * factory. Rejects if no factory is registered; the factory rejects if - * session persistence is not configured or persistence/setup fails. - * @param options - persisted identity, configuration, and optional setup. - * @returns the handle after setup, rollback-covered publication, and loop start complete. - */ -async resume(options: ResumeAgentOptions): Promise<AgentHandle> -``` - -Load a persisted session and resume an agent on it through the registered factory. Rejects if no factory is registered; the factory rejects if session persistence is not configured or persistence/setup fails. - -- `options` — persisted identity, configuration, and optional setup. - -**Returns** the handle after setup, rollback-covered publication, and loop start complete. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L371) - -### ctx.agents.register(agent) - -```ts website-api -/** - * Register a live agent. Throws if an agent with the same id is already - * registered. Emits `agent/created` on registration and `agent/disposed` - * when the calling fiber is disposed — both with the agent's scope carrier - * (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the - * emits are scope-filtered regardless of which context invoked `register` - * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always - * requires passing the carrier). Returns the disposer. - * @param agent - the already-constructed agent to record in the store. - * @returns the EXACT Cordis effect disposer (single-shot; a repeat call - * returns undefined without awaiting an in-flight teardown). Exact - * identity is load-bearing: a composite (generator) effect that owns a - * teardown ORDER — the agent factory's lifecycle chain — must yield THIS - * function so Cordis nests the unregistration at that yield position; - * yielding a wrapper would leave it disposing as a concurrent sibling on - * owner unload, unregistering the agent (and emitting `agent/disposed`) - * while its final turn is still draining. - */ -register(agent: Agent): () => void -``` - -Register a live agent. Throws if an agent with the same id is already registered. Emits `agent/created` on registration and `agent/disposed` when the calling fiber is disposed — both with the agent's scope carrier (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the emits are scope-filtered regardless of which context invoked `register` (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always requires passing the carrier). Returns the disposer. - -- `agent` — the already-constructed agent to record in the store. - -**Returns** the EXACT Cordis effect disposer (single-shot; a repeat call returns undefined without awaiting an in-flight teardown). Exact identity is load-bearing: a composite (generator) effect that owns a teardown ORDER — the agent factory's lifecycle chain — must yield THIS function so Cordis nests the unregistration at that yield position; yielding a wrapper would leave it disposing as a concurrent sibling on owner unload, unregistering the agent (and emitting `agent/disposed`) while its final turn is still draining. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L397) - -### ctx.agents.enter(agent, owner) - -```ts website-api -/** - * Insert an already-constructed agent without announcing it. This is the - * advanced ordered-lifecycle primitive used by the async agent factory: it - * first completes setup while the agent is unpublished, then assigns the - * returned detach closure into its pre-installed composite teardown before - * calling {@link announce}. Ordinary callers use {@link register}. - * @param agent - the prepared, unpublished agent. - * @param owner - live agent whose scoped context created this agent, or - * undefined for a top-level runtime root. This is runtime ownership, not - * the resumed session's durable parent lineage. - * @returns an idempotent closure that removes this exact entry and emits - * `agent/disposed` with listener failures contained. When called from a - * synchronous `agent/created` listener, removal and disposal wait until - * that creation dispatch unwinds. - */ -enter(agent: Agent, owner: Agent | undefined): () => void -``` - -Insert an already-constructed agent without announcing it. This is the advanced ordered-lifecycle primitive used by the async agent factory: it first completes setup while the agent is unpublished, then assigns the returned detach closure into its pre-installed composite teardown before calling announce. Ordinary callers use register. - -- `agent` — the prepared, unpublished agent. -- `owner` — live agent whose scoped context created this agent, or undefined for a top-level runtime root. This is runtime ownership, not the resumed session's durable parent lineage. - -**Returns** an idempotent closure that removes this exact entry and emits `agent/disposed` with listener failures contained. When called from a synchronous `agent/created` listener, removal and disposal wait until that creation dispatch unwinds. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L421) - -### ctx.agents.announce(agent) - -```ts website-api -/** - * Announce an agent previously inserted with {@link enter}. - * @param agent - the live inserted agent to announce. - * @throws if `agent` is not the exact live registry entry for its id, or its - * creation announcement already began (including a reentrant call from a - * creation listener). - */ -announce(agent: Agent): void -``` - -Announce an agent previously inserted with enter. - -- `agent` — the live inserted agent to announce. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L496) - -### ctx.agents.get(id) - -```ts website-api -/** - * Look up a live agent. - * @param id - the shared agent/session id to look up. - * @returns the agent, or undefined when no live agent has that id. - */ -get(id: SessionId): Agent | undefined -``` - -Look up a live agent. - -- `id` — the shared agent/session id to look up. - -**Returns** the agent, or undefined when no live agent has that id. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L530) - -### ctx.agents.isOwnedBy(id, owner) - -```ts website-api -/** - * Test whether a live agent was created through one exact parent agent's - * scoped context. Runtime ownership is independent of durable session - * lineage and remains unambiguous when unrelated providers reuse an id. - * @param id - the candidate child agent's shared agent/session id. - * @param owner - the expected runtime creator agent. - * @returns true only while the exact child entry is live under that owner. - */ -isOwnedBy(id: SessionId, owner: Agent): boolean -``` - -Test whether a live agent was created through one exact parent agent's scoped context. Runtime ownership is independent of durable session lineage and remains unambiguous when unrelated providers reuse an id. - -- `id` — the candidate child agent's shared agent/session id. -- `owner` — the expected runtime creator agent. - -**Returns** true only while the exact child entry is live under that owner. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L542) - -### ctx.agents.list() - -```ts website-api -/** - * All live agents, in registration order. - * @returns a fresh array; mutating it does not affect the registry. - */ -list(): Agent[] -``` - -All live agents, in registration order. - -**Returns** a fresh array; mutating it does not affect the registry. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L550) - -### ctx.agents.roots() - -```ts website-api -/** - * All live top-level agents in registration order. A top-level agent was - * created without an owning agent context; durable session lineage does not - * affect this runtime relation, so a resumed fork may still be a root. - * @returns a fresh array; mutating it does not affect the registry. - */ -roots(): Agent[] -``` - -All live top-level agents in registration order. A top-level agent was created without an owning agent context; durable session lineage does not affect this runtime relation, so a resumed fork may still be a root. - -**Returns** a fresh array; mutating it does not affect the registry. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L560) diff --git a/website/zh-CN/api/harness/approval.md b/website/zh-CN/api/harness/approval.md deleted file mode 100644 index 4aa2b8ad2d..0000000000 --- a/website/zh-CN/api/harness/approval.md +++ /dev/null @@ -1,41 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.approval - -`ApprovalService` — provided by `@deepseek-ai/dsh-user-approval`. - -Approval service that applies session policy before answerers and logs every ask/outcome pair to the requesting session. It exposes deterministic policy changes to the model through prompt and pre-step notices. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-approval/src/index.ts#L229) - -### ctx.approval.request(req) - -```ts website-api -/** - * Ask the composed answerers to decide one readonly same-process request. - * The service borrows the request, agent, session, and live signal directly. - * The request requires an open turn because the audit pair must be enclosed - * by the durable log's commit/replay boundary; an idle ask rejects before - * appending anything. The answerer phase always produces an outcome: an - * aborted signal yields `'cancelled'`, a missing or throwing answerer yields - * `'unavailable'` (fail closed), and a rogue non-vocabulary return value is - * normalized to `'unavailable'`. A failure that prevents either audit append - * from committing still rejects because returning an unlogged decision would - * violate the pair. Session contains post-commit observer failures, so an - * authoritative append cannot reject the request or suppress its matching - * audit event. - * @param req - the pending decision (agent, tool identity, reason, signal). - * @returns the closed outcome; `'allowed-once'` is the only grant. - * @throws when no turn is open or either audit event fails before the session - * append commit point. - */ -async request(req: ApprovalRequest): Promise<ApprovalOutcome> -``` - -Ask the composed answerers to decide one readonly same-process request. The service borrows the request, agent, session, and live signal directly. The request requires an open turn because the audit pair must be enclosed by the durable log's commit/replay boundary; an idle ask rejects before appending anything. The answerer phase always produces an outcome: an aborted signal yields `'cancelled'`, a missing or throwing answerer yields `'unavailable'` (fail closed), and a rogue non-vocabulary return value is normalized to `'unavailable'`. A failure that prevents either audit append from committing still rejects because returning an unlogged decision would violate the pair. Session contains post-commit observer failures, so an authoritative append cannot reject the request or suppress its matching audit event. - -- `req` — the pending decision (agent, tool identity, reason, signal). - -**Returns** the closed outcome; `'allowed-once'` is the only grant. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-approval/src/index.ts#L313) diff --git a/website/zh-CN/api/harness/bash-env.md b/website/zh-CN/api/harness/bash-env.md deleted file mode 100644 index 906464080e..0000000000 --- a/website/zh-CN/api/harness/bash-env.md +++ /dev/null @@ -1,64 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.bashEnv - -`BashEnvRegistry` — provided by `@deepseek-ai/dsh-tool-bash`. - -Registry (`ctx.bashEnv`) for trusted, per-execution `DSH_*` variables. The namespace is rebuilt for every model bash call: ambient `DSH_*` values are discarded by the executor, then the registry's current snapshot is injected. Built-in shell facts remain owned by the registry itself while plugins can register additional, enumerable facts with effect-scoped disposal. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/tool-bash/src/index.ts#L102) - -### ctx.bashEnv.register(contributor) - -```ts website-api -/** - * Register one environment contributor. Names and keys are unique; built-in - * keys are reserved. Registration is disposed with the calling plugin fiber. - * @param contributor - declared key ownership and per-execution resolver. - * @returns the disposer that unregisters the contribution. - */ -register(contributor: BashEnvContributor): () => void -``` - -Register one environment contributor. Names and keys are unique; built-in keys are reserved. Registration is disposed with the calling plugin fiber. - -- `contributor` — declared key ownership and per-execution resolver. - -**Returns** the disposer that unregisters the contribution. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/tool-bash/src/index.ts#L123) - -### ctx.bashEnv.collect(execution) - -```ts website-api -/** - * Build the trusted `DSH_*` snapshot for one bash tool execution. - * @param execution - the current tool execution. - * @returns an immutable environment overlay containing built-ins and current contributions. - */ -collect(execution: ToolExecution): DshEnvironment -``` - -Build the trusted `DSH_*` snapshot for one bash tool execution. - -- `execution` — the current tool execution. - -**Returns** an immutable environment overlay containing built-ins and current contributions. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/tool-bash/src/index.ts#L165) - -### ctx.bashEnv.list() - -```ts website-api -/** - * Enumerate plugin-contributed variables without executing their resolvers. - * @returns declarations sorted by environment variable name. - */ -list(): BashEnvVariableInfo[] -``` - -Enumerate plugin-contributed variables without executing their resolvers. - -**Returns** declarations sorted by environment variable name. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/tool-bash/src/index.ts#L197) diff --git a/website/zh-CN/api/harness/bash.md b/website/zh-CN/api/harness/bash.md deleted file mode 100644 index f340697063..0000000000 --- a/website/zh-CN/api/harness/bash.md +++ /dev/null @@ -1,88 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.bash - -`BashExecutor` (abstract seam) — provided by `@deepseek-ai/dsh-bash`. - -Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.bash` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior). -Implementations must honor these semantics: -- run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a BashRunResult. -- start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr. -- BashProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files. -- Disposal kills all running background processes and awaits their exit. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L49) - -### ctx.bash.sandboxMode - -```ts website-api -/** - * The sandbox mode this executor applies by default, or `undefined` when it - * does not sandbox commands. - * @returns the configured default sandbox mode, when supported. - */ -get sandboxMode(): SandboxMode | undefined -``` - -The sandbox mode this executor applies by default, or `undefined` when it does not sandbox commands. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L59) - -### ctx.bash.resolve(request) - -```ts website-api -/** - * Apply implementation-owned defaults and caps to a request before execution. - * @param request - the caller's request; omitted fields get this - * implementation's defaults, capped fields are clamped. - * @returns the fully-specified spec to hand to {@link run}/{@link start}. - */ -abstract resolve(request: BashExecRequest): BashExecSpec -``` - -Apply implementation-owned defaults and caps to a request before execution. - -- `request` — the caller's request; omitted fields get this implementation's defaults, capped fields are clamped. - -**Returns** the fully-specified spec to hand to `run`/`start`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L69) - -### ctx.bash.run(spec) - -```ts website-api -/** - * Run a command in the foreground; resolves when it finishes. - * @param spec - a resolved spec from {@link resolve}, never a raw request. - * @returns the outcome; nonzero exits, timeout kills, and abort kills - * resolve with a descriptive result rather than reject. - */ -abstract run(spec: BashExecSpec): Promise<BashRunResult> -``` - -Run a command in the foreground; resolves when it finishes. - -- `spec` — a resolved spec from `resolve`, never a raw request. - -**Returns** the outcome; nonzero exits, timeout kills, and abort kills resolve with a descriptive result rather than reject. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L77) - -### ctx.bash.start(spec) - -```ts website-api -/** - * Start a background process and return its handle immediately. - * @param spec - a resolved spec from {@link resolve}, never a raw request. - * @returns the live process handle (reads, kill, quiescence promise). - */ -abstract start(spec: BashExecSpec): BashProcess -``` - -Start a background process and return its handle immediately. - -- `spec` — a resolved spec from `resolve`, never a raw request. - -**Returns** the live process handle (reads, kill, quiescence promise). - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/bash/bash/src/index.ts#L84) diff --git a/website/zh-CN/api/harness/code-runtime.md b/website/zh-CN/api/harness/code-runtime.md deleted file mode 100644 index fef72ce525..0000000000 --- a/website/zh-CN/api/harness/code-runtime.md +++ /dev/null @@ -1,65 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.codeRuntime - -`CodeRuntime` (abstract seam) — provided by `@deepseek-ai/dsh-code-runtime`. - -Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and substrate failures resolve in CodeRunResult; only seam misuse rejects. Implementations bridge structured-cloneable bindings while treating programs as hostile peers, isolate runs from one another, and terminate and await in-flight runs during disposal. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L30) - -### ctx.codeRuntime.language - -```ts website-api -/** - * The source language {@link run} expects `program` to be written in, as a - * lowercase identifier. Informational, not gating — a consumer that - * generates language-specific presentation (typed SDK stubs, usage - * instructions) switches on it and fails loud on a language it cannot - * present. Well-known value: `'typescript'`. - */ -abstract readonly language: string -``` - -The source language run expects `program` to be written in, as a lowercase identifier. Informational, not gating — a consumer that generates language-specific presentation (typed SDK stubs, usage instructions) switches on it and fails loud on a language it cannot present. Well-known value: `'typescript'`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L38) - -### ctx.codeRuntime.isolation - -```ts website-api -/** - * The execution substrate, as a lowercase identifier. Informational, not - * gating — a descriptor so deployments and diagnostics can tell backends - * apart, not a security claim. Well-known values: `'worker-thread'`, - * `'process'`, `'container'`. - */ -abstract readonly isolation: string -``` - -The execution substrate, as a lowercase identifier. Informational, not gating — a descriptor so deployments and diagnostics can tell backends apart, not a security claim. Well-known values: `'worker-thread'`, `'process'`, `'container'`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L46) - -### ctx.codeRuntime.run(request) - -```ts website-api -/** - * Execute one program against the request's bindings and capture what it - * emitted. See the class doc for the resolution contract (error is a result - * field; rejection means seam misuse only). - * @param request - the program, its bindings, and the abort signal; the - * request carries everything the runtime acts on, with no hidden defaults. - * @returns the run's outcome: completion value (when transferable), the - * ordered log capture, and the failure (if any). - */ -abstract run(request: CodeRunRequest): Promise<CodeRunResult> -``` - -Execute one program against the request's bindings and capture what it emitted. See the class doc for the resolution contract (error is a result field; rejection means seam misuse only). - -- `request` — the program, its bindings, and the abort signal; the request carries everything the runtime acts on, with no hidden defaults. - -**Returns** the run's outcome: completion value (when transferable), the ordered log capture, and the failure (if any). - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/code-runtime/code-runtime/src/index.ts#L61) diff --git a/website/zh-CN/api/harness/compact.md b/website/zh-CN/api/harness/compact.md deleted file mode 100644 index 71d34ff7ba..0000000000 --- a/website/zh-CN/api/harness/compact.md +++ /dev/null @@ -1,71 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.compact - -`CompactService` (abstract seam) — provided by `@deepseek-ai/dsh-compact`. - -Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. Load one implementation per context as `ctx.compact`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L40) - -### ctx.compact.compactIfNeeded(agent, trigger, signal) - -```ts website-api -/** - * Consider automatic compaction for one explicit trigger. Pressure policy - * uses the latest durable routed request, while context-overflow policy may - * force a useful balanced reduction even below the normal threshold. Return - * `null` when no safe range can be compacted. A single oversized retained - * unit or request envelope cannot be repaired through surface compaction. - * - * @param agent - agent context owning the session surface and routing options. - * @param trigger - normal pressure or provider-confirmed context overflow. - * @param signal - cancellation signal; model-backed implementations must forward it. - * @returns the compaction result, or `null` if no compaction was needed. - */ -abstract compactIfNeeded( agent: CompactAgentContext, trigger: CompactionTrigger, signal: AbortSignal, ): Promise<CompactionResult | null> -``` - -Consider automatic compaction for one explicit trigger. Pressure policy uses the latest durable routed request, while context-overflow policy may force a useful balanced reduction even below the normal threshold. Return `null` when no safe range can be compacted. A single oversized retained unit or request envelope cannot be repaired through surface compaction. - -- `agent` — agent context owning the session surface and routing options. -- `trigger` — normal pressure or provider-confirmed context overflow. -- `signal` — cancellation signal; model-backed implementations must forward it. - -**Returns** the compaction result, or `null` if no compaction was needed. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L57) - -### ctx.compact.compactRegion(start, end, agent, signal?) - -```ts website-api -/** - * Forcibly compact a range of surface nodes into a single summary node. - * `start` and `end` name an inclusive span by surface position, not numeric seq - * order; replacements can make visible seqs non-monotonic. Both edges must be - * balanced so assistant tool calls remain paired with their results. A model- - * backed implementation forwards cancellation and rejects active, missing, - * reversed, or unbalanced ranges. The target session is `agent.session`. - * Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter} - * for the edge checks. - * - * @param start - first surface seq, inclusive. - * @param end - last surface seq, inclusive. - * @param agent - context whose session is mutated and whose routing options guide summarization. - * @param signal - optional cancellation; model-backed implementations must forward it. - * @throws when compaction is active or the range is missing, reversed, or unbalanced. - * @returns the appended event seqs, summary, replaced range, and token accounting. - */ -abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise<CompactionResult> -``` - -Forcibly compact a range of surface nodes into a single summary node. `start` and `end` name an inclusive span by surface position, not numeric seq order; replacements can make visible seqs non-monotonic. Both edges must be balanced so assistant tool calls remain paired with their results. A model- backed implementation forwards cancellation and rejects active, missing, reversed, or unbalanced ranges. The target session is `agent.session`. Use toolPairingBalancedBefore and toolPairingBalancedAfter for the edge checks. - -- `start` — first surface seq, inclusive. -- `end` — last surface seq, inclusive. -- `agent` — context whose session is mutated and whose routing options guide summarization. -- `signal` — optional cancellation; model-backed implementations must forward it. - -**Returns** the appended event seqs, summary, replaced range, and token accounting. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L80) diff --git a/website/zh-CN/api/harness/events.md b/website/zh-CN/api/harness/events.md deleted file mode 100644 index c3afe6b3aa..0000000000 --- a/website/zh-CN/api/harness/events.md +++ /dev/null @@ -1,1023 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# Harness events - -Every event the harness packages declare on the cordis event bus (42 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). - -## agent/* - -### agent/created - -**Mode:** `emit` - -```ts website-api -/** - * A fully configured agent and live session were published. Setup is - * composition-only; `agent/session-start` is the first startup-driving seam. - * Synchronous listener failure vetoes publication, while returned-promise - * rejection is reported. Detach requested during dispatch waits until every - * creation listener has observed the stable entry. - * @param agent - the newly registered agent with its live session and completed setup. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode emit - */ -'agent/created'(this: Scoped<Agent>, agent: Agent): void -``` - -A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving seam. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry. - -- `agent` — the newly registered agent with its live session and completed setup. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L147) - -### agent/disposed - -**Mode:** `emit` - -```ts website-api -/** - * An agent left the registry; AgentLoop emits this after driver quiescence - * but before session detachment and scoped-registration unwind. Custom - * registry users own their driver-ordering contract. - * @param agent - the exact agent removed from the registry. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode emit - */ -'agent/disposed'(this: Scoped<Agent>, agent: Agent): void -``` - -An agent left the registry; AgentLoop emits this after driver quiescence but before session detachment and scoped-registration unwind. Custom registry users own their driver-ordering contract. - -- `agent` — the exact agent removed from the registry. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L156) - -### agent/error - -**Mode:** `emit` - -```ts website-api -/** - * A step or turn errored. The loop reports a failure here (plus the logger) - * even when the error has no in-turn position for a session `error` event. - * @param agent - the agent whose turn errored. - * @param turn - the turn in which the failure surfaced. - * @param step - the step at which the failure surfaced. - * @param error - the failure, verbatim. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode emit - */ -'agent/error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: Error): void -``` - -A step or turn errored. The loop reports a failure here (plus the logger) even when the error has no in-turn position for a session `error` event. - -- `agent` — the agent whose turn errored. -- `turn` — the turn in which the failure surfaced. -- `step` — the step at which the failure surfaced. -- `error` — the failure, verbatim. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L311) - -### agent/post-step - -**Mode:** `serial` - -```ts website-api -/** - * Awaited serial checkpoint after the response, real or synthetic tool - * results, injected context, and steering are durable but before `step/end`. - * A cancelled tool batch reaches this checkpoint with an aborted signal. - * @param agent - the agent whose step is settling. - * @param turn - the open turn number. - * @param step - the open step number. - * @param signal - the turn abort signal. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode serial - */ -'agent/post-step'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal): Promise<void> | void -``` - -Awaited serial checkpoint after the response, real or synthetic tool results, injected context, and steering are durable but before `step/end`. A cancelled tool batch reaches this checkpoint with an aborted signal. - -- `agent` — the agent whose step is settling. -- `turn` — the open turn number. -- `step` — the open step number. -- `signal` — the turn abort signal. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L264) - -### agent/pre-step - -**Mode:** `serial` - -```ts website-api -/** - * Awaited serial checkpoint before `step/start`; appends land outside the - * pending step and are included when the loop derives request history. - * `signal` cancels listener work. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @param agent - the agent opening the step. - * @param turn - the open turn number. - * @param step - the pending step number. - * @param signal - the turn abort signal. - * @mode serial - */ -'agent/pre-step'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal): Promise<void> | void -``` - -Awaited serial checkpoint before `step/start`; appends land outside the pending step and are included when the loop derives request history. `signal` cancels listener work. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -- `agent` — the agent opening the step. -- `turn` — the open turn number. -- `step` — the pending step number. -- `signal` — the turn abort signal. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L204) - -### agent/prompt-submit - -**Mode:** `waterfall` - -```ts website-api -/** - * Allow, rewrite, or block one drained 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 source - the message's resolved source. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode waterfall - */ -'agent/prompt-submit'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise<PromptDecision>): Promise<PromptDecision> -``` - -Allow, rewrite, or block one drained prompt before it becomes a user message. Call `next()` for the unchanged default. - -- `agent` — the agent draining its inbox. -- `content` — the drained message's blocks, as queued. -- `source` — the message's resolved source. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L214) - -### agent/queued - -**Mode:** `emit` - -```ts website-api -/** - * Detached, frozen content entered the agent's inbox. Source defaults have - * already been applied, so these are the exact values retained for the log. - * @param agent - the agent whose inbox received the message. - * @param content - the accepted content blocks retained by the inbox. - * @param info - the accepted source plus whether it entered as steering. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode emit - */ -'agent/queued'(this: Scoped<Agent>, agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void -``` - -Detached, frozen content entered the agent's inbox. Source defaults have already been applied, so these are the exact values retained for the log. - -- `agent` — the agent whose inbox received the message. -- `content` — the accepted content blocks retained by the inbox. -- `info` — the accepted source plus whether it entered as steering. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L175) - -### agent/request - -**Mode:** `waterfall` - -```ts website-api -/** - * Replace the frozen call configuration. Model-visible content must use - * logged channels; this seam cannot mutate messages. Injection here joins - * the next request because the current step boundary is already fixed. - * @param agent - the agent making the model call. - * @param turn - the open turn number. - * @param step - the step whose request this is. - * @param config - the config the loop would use (frozen); return a replacement to switch. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode waterfall - */ -'agent/request'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig> -``` - -Replace the frozen call configuration. Model-visible content must use logged channels; this seam cannot mutate messages. Injection here joins the next request because the current step boundary is already fixed. - -- `agent` — the agent making the model call. -- `turn` — the open turn number. -- `step` — the step whose request this is. -- `config` — the config the loop would use (frozen); return a replacement to switch. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L226) - -### agent/request-error - -**Mode:** `waterfall` - -```ts website-api -/** - * Recover a model-request failure after its failed step has closed. `retry` - * opens a new numbered step; `fail` preserves the original request error. - * Call `next()` to delegate to the next recovery listener or the default. - * @param agent - the agent whose request failed. - * @param turn - the open turn number. - * @param step - the failed step number. - * @param error - the original model-request failure. - * @param retryAttempt - zero-based number of prior recovery retries. - * @param signal - the turn abort signal. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode waterfall - */ -'agent/request-error'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, error: RequestError, retryAttempt: number, signal: AbortSignal, next: () => Promise<RequestErrorDecision>): Promise<RequestErrorDecision> -``` - -Recover a model-request failure after its failed step has closed. `retry` opens a new numbered step; `fail` preserves the original request error. Call `next()` to delegate to the next recovery listener or the default. - -- `agent` — the agent whose request failed. -- `turn` — the open turn number. -- `step` — the failed step number. -- `error` — the original model-request failure. -- `retryAttempt` — zero-based number of prior recovery retries. -- `signal` — the turn abort signal. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L278) - -### agent/session-prefix - -**Mode:** `waterfall` - -```ts website-api -/** - * Compose request-only messages placed before derived history. The frozen - * result is computed once per loop instance, logged on its anchoring request - * header, and reused so the provider prefix remains stable. Interrupted - * composition is discarded. Composition precedes the first `agent/pre-step` - * and request boundary, so listener appends join the current request. - * Changing context belongs in history; contributors should prepend to - * `await next()` to preserve registration order. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @param agent - the agent whose session prefix is being composed. - * @param prefix - the frozen seed; return an extended replacement. - * @param signal - aborts composition when the step is torn down. - * @mode waterfall - */ -'agent/session-prefix'(this: Scoped<Agent>, agent: Agent, prefix: Message[], signal: AbortSignal, next: () => Promise<Message[]>): Promise<Message[]> -``` - -Compose request-only messages placed before derived history. The frozen result is computed once per loop instance, logged on its anchoring request header, and reused so the provider prefix remains stable. Interrupted composition is discarded. Composition precedes the first `agent/pre-step` and request boundary, so listener appends join the current request. Changing context belongs in history; contributors should prepend to `await next()` to preserve registration order. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -- `agent` — the agent whose session prefix is being composed. -- `prefix` — the frozen seed; return an extended replacement. -- `signal` — aborts composition when the step is torn down. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L241) - -### agent/session-start - -**Mode:** `emit` - -```ts website-api -/** - * The session lifecycle began, once before the first turn. Use - * `agent.inject()` to seed model-facing context. This is a notification, not - * a veto; disposal requested by a lifecycle owner is rechecked before the - * driver starts. - * @param agent - the agent whose session lifecycle began. - * @param source - why the session started (fresh startup, resume, …). - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode emit - */ -'agent/session-start'(this: Scoped<Agent>, agent: Agent, source: SessionStartSource): void -``` - -The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts. - -- `agent` — the agent whose session lifecycle began. -- `source` — why the session started (fresh startup, resume, …). Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L188) - -### agent/status - -**Mode:** `emit` - -```ts website-api -/** - * Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does - * not enter `running` synchronously; drive lifecycle from this event. - * @param agent - the agent whose status flipped. - * @param status - the status just entered (the transition's destination). - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode emit - */ -'agent/status'(this: Scoped<Agent>, agent: Agent, status: AgentStatus): void -``` - -Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does not enter `running` synchronously; drive lifecycle from this event. - -- `agent` — the agent whose status flipped. -- `status` — the status just entered (the transition's destination). Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L165) - -### agent/step-result - -**Mode:** `waterfall` - -```ts website-api -/** - * Waterfall: post-process the assembled assistant {@link Message} before - * tool dispatch (validation, content rewriting, …). - * @param agent - the agent that received the step's response. - * @param turn - the open turn number. - * @param step - the step that produced the message. - * @param message - the assistant message as assembled from the stream. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode waterfall - */ -'agent/step-result'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, message: Message, next: () => Promise<Message>): Promise<Message> -``` - -Waterfall: post-process the assembled assistant Message before tool dispatch (validation, content rewriting, …). - -- `agent` — the agent that received the step's response. -- `turn` — the open turn number. -- `step` — the step that produced the message. -- `message` — the assistant message as assembled from the stream. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L252) - -### agent/turn-continuation - -**Mode:** `waterfall` - -```ts website-api -/** - * Override whether the turn continues. The default continues after tool - * calls or steering and stops otherwise; a continue reason becomes steering. - * @param agent - the agent deciding whether to run another step. - * @param turn - the turn being continued or stopped. - * @param defaultDecision - what the loop would do absent an override. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode waterfall - */ -'agent/turn-continuation'(this: Scoped<Agent>, agent: Agent, turn: number, defaultDecision: ContinuationDecision, next: () => Promise<ContinuationDecision>): Promise<ContinuationDecision> -``` - -Override whether the turn continues. The default continues after tool calls or steering and stops otherwise; a continue reason becomes steering. - -- `agent` — the agent deciding whether to run another step. -- `turn` — the turn being continued or stopped. -- `defaultDecision` — what the loop would do absent an override. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L288) - -### agent/turn-stop - -**Mode:** `serial` - -```ts website-api -/** - * Monotonic terminal-stop checkpoint after continuation and steering are - * folded; a stop remains authoritative through turn close and flush: - * steering queued in that window is discarded, while ordinary sends survive. - * @param agent - the agent whose composed continuation outcome may be stopped. - * @param turn - the turn at its terminal-stop checkpoint. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @mode serial - */ -'agent/turn-stop'(this: Scoped<Agent>, agent: Agent, turn: number): ContinuationStop | undefined -``` - -Monotonic terminal-stop checkpoint after continuation and steering are folded; a stop remains authoritative through turn close and flush: steering queued in that window is discarded, while ordinary sends survive. - -- `agent` — the agent whose composed continuation outcome may be stopped. -- `turn` — the turn at its terminal-stop checkpoint. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/types.ts#L298) - -## agent-loop/* - -### agent-loop/config-start-failed - -**Mode:** `emit` - -```ts website-api -/** - * A declarative agent entry failed before it could publish a live agent. - * Consumers that buffer work for the configured identity use this - * transient signal to reject that work instead of waiting forever. Normal - * factory teardown suppresses failures from the cancelled startup attempt. - * @param sessionId - exact shared agent/session identity that failed startup. - * @param error - persistence, setup, or publication failure. - * @mode emit - */ -'agent-loop/config-start-failed'(sessionId: SessionId, error: unknown): void -``` - -A declarative agent entry failed before it could publish a live agent. Consumers that buffer work for the configured identity use this transient signal to reject that work instead of waiting forever. Normal factory teardown suppresses failures from the cancelled startup attempt. - -- `sessionId` — exact shared agent/session identity that failed startup. -- `error` — persistence, setup, or publication failure. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L362) - -## approval/* - -### approval/request - -**Mode:** `waterfall` - -```ts website-api -/** - * Ask composed answerers for one decision. Return an outcome to claim the - * request or call `next()`; failure yields the fail-closed default. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - * @param req - the pending decision (agent, tool identity, reason, signal). - * @mode waterfall - */ -'approval/request'(this: Scoped<ApprovalService>, req: ApprovalRequest, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome> -``` - -Ask composed answerers for one decision. Return an outcome to claim the request or call `next()`; failure yields the fail-closed default. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. - -- `req` — the pending decision (agent, tool identity, reason, signal). - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-approval/src/index.ts#L31) - -## fs/* - -### fs/edit-intent - -**Mode:** `waterfall` - -```ts website-api -/** - * Single-slot decision for the next {@link FileSystem.editText}. Calling - * `next()` yields an unconditional edit; the first returned guard wins. - * @param target - the resolved target about to be edited. - * @param actor - the opaque tool-execution context the decider keys off. - * @mode waterfall - */ -'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined> -``` - -Single-slot decision for the next FileSystem.editText. Calling `next()` yields an unconditional edit; the first returned guard wins. - -- `target` — the resolved target about to be edited. -- `actor` — the opaque tool-execution context the decider keys off. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L61) - -### fs/observed - -**Mode:** `emit` - -```ts website-api -/** - * Record a successful observation. Listeners must be synchronous recorders: - * throws fail the tool call and returned promises are not awaited. - * @param target - the target that was read/written/edited. - * @param version - the version the actor now holds as its observation. - * @param actor - the observing tool-execution context; undefined records nothing useful. - * @mode emit - */ -'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void -``` - -Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited. - -- `target` — the target that was read/written/edited. -- `version` — the version the actor now holds as its observation. -- `actor` — the observing tool-execution context; undefined records nothing useful. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L70) - -### fs/write-intent - -**Mode:** `waterfall` - -```ts website-api -/** - * Single-slot decision for the next {@link FileSystem.writeText}. Calling - * `next()` yields the bare provider's unconditional write; the first listener - * that returns an intent owns the decision rather than composing with peers. - * @param target - the resolved target about to be written. - * @param actor - the opaque tool-execution context the decider keys off. - * @mode waterfall - */ -'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined> -``` - -Single-slot decision for the next FileSystem.writeText. Calling `next()` yields the bare provider's unconditional write; the first listener that returns an intent owns the decision rather than composing with peers. - -- `target` — the resolved target about to be written. -- `actor` — the opaque tool-execution context the decider keys off. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L53) - -## llm/* - -### llm/stream - -**Mode:** `waterfall` - -```ts website-api -/** - * Waterfall around every streaming model call (retry, replay, routing). - * Bound to the {@link LlmService}; call `next()` to reach the resolved - * adapter's stream, or yield your own chunks to short-circuit. - * @param options - the full request. A LOOP-built request arrives - * deep-frozen (mutation throws): its content is a pure function of the - * session log (the reconstructability RFC), so listeners read it, never - * rewrite it. A hand-built one-shot (compaction summarize) is the - * caller's own object and stays mutable here. - * @mode waterfall - */ -'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk> -``` - -Waterfall around every streaming model call (retry, replay, routing). Bound to the LlmService; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit. - -- `options` — the full request. A LOOP-built request arrives deep-frozen (mutation throws): its content is a pure function of the session log (the reconstructability RFC), so listeners read it, never rewrite it. A hand-built one-shot (compaction summarize) is the caller's own object and stays mutable here. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L43) - -## session/* - -### session/created - -**Mode:** `emit` - -```ts website-api -/** - * Creation announcement during session publication. A synchronous throw vetoes and rolls - * back with a paired disposal; detach requested during dispatch is deferred. - * A returned-promise rejection is logged but cannot retroactively veto this - * synchronous boundary. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners - * receive only sessions entered through that agent's context. - * @param session - the session just entered and announced. - * @dshScopeScan unsupported - * @mode emit - */ -'session/created'(this: Scoped<Session>, session: Session): void -``` - -Creation announcement during session publication. A synchronous throw vetoes and rolls back with a paired disposal; detach requested during dispatch is deferred. A returned-promise rejection is logged but cannot retroactively veto this synchronous boundary. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only sessions entered through that agent's context. - -- `session` — the session just entered and announced. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L47) - -### session/disposed - -**Mode:** `emit` - -```ts website-api -/** - * Emitted once when an announced session leaves the store, including - * publication rollback, but never for an entry whose creation announcement - * did not begin. Listener failures are logged and contained. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope. - * @param session - the session that is no longer live in the store. - * @dshScopeScan unsupported - * @mode emit - */ -'session/disposed'(this: Scoped<Session>, session: Session): void -``` - -Emitted once when an announced session leaves the store, including publication rollback, but never for an entry whose creation announcement did not begin. Listener failures are logged and contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope. - -- `session` — the session that is no longer live in the store. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L57) - -### session/event - -**Mode:** `emit` - -```ts website-api -/** - * Post-commit, fire-and-forget append feed. The listener snapshot resolves - * before the log push, but callbacks run after it; observer failures are - * logged and contained without making the committed append fail. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners - * receive only events from sessions entered through that agent's context. - * @param session - the session whose log grew. - * @param event - the appended event, exactly as recorded. - * @dshScopeScan unsupported - * @mode emit - */ -'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void -``` - -Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context. - -- `session` — the session whose log grew. -- `event` — the appended event, exactly as recorded. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L69) - -### session/flush - -**Mode:** `parallel` - -```ts website-api -/** - * Awaited parallel durability checkpoint: every listener runs and the - * caller awaits all of them, with no waterfall veto. Dispatch through - * {@link SessionStore.flush}. Scope-filtered dispatch - * (`@deepseek-ai/dsh-scope`) reuses the session's owner scope. - * @param session - the session whose buffered events must reach durable storage. - * @dshScopeScan unsupported - * @mode parallel - */ -'session/flush'(this: Scoped<Session>, session: Session): Promise<void> | void -``` - -Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto. Dispatch through SessionStore.flush. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the session's owner scope. - -- `session` — the session whose buffered events must reach durable storage. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L79) - -## subagent/* - -### subagent/end - -**Mode:** `emit` - -```ts website-api -/** - * A ready child settled. Scope-filtered dispatch uses the same delegating - * parent carrier as `subagent/start`, so the lifecycle pair reaches the - * same scoped audience. - * @param info - the run identity and terminal outcome. - * @dshScopeScan unsupported - * @mode emit - */ -'subagent/end'(this: Scoped<SubagentService>, info: SubagentRunEndInfo): void -``` - -A ready child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience. - -- `info` — the run identity and terminal outcome. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L112) - -### subagent/provider-added - -**Mode:** `emit` - -```ts website-api -/** - * A provider became resolvable in the registry. - * @param provider - the registered provider. - * @mode emit - */ -'subagent/provider-added'(provider: SubagentProvider): void -``` - -A provider became resolvable in the registry. - -- `provider` — the registered provider. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L86) - -### subagent/provider-removed - -**Mode:** `emit` - -```ts website-api -/** - * A provider left the registry. Accepted runs remain holder-owned. - * @param name - the provider name that no longer resolves. - * @mode emit - */ -'subagent/provider-removed'(name: string): void -``` - -A provider left the registry. Accepted runs remain holder-owned. - -- `name` — the provider name that no longer resolves. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L92) - -### subagent/start - -**Mode:** `emit` - -```ts website-api -/** - * A provider established a ready child. For in-process providers, - * `ctx.agents.get(info.id)` resolves during this notification. - * Scope-filtered dispatch keys the carrier by the delegating parent, so a - * parent-scoped listener observes only its own delegations. Paired with - * `subagent/end`. - * @param info - the provider and ready child identity. - * @dshScopeScan unsupported - * @mode emit - */ -'subagent/start'(this: Scoped<SubagentService>, info: SubagentRunInfo): void -``` - -A provider established a ready child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with `subagent/end`. - -- `info` — the provider and ready child identity. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L103) - -## system-prompt/* - -### system-prompt/assemble - -**Mode:** `waterfall` - -```ts website-api -/** - * Expert waterfall over the assembled sections, tools, and variables. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners - * receive only that scope's assemblies. The returned value is authoritative. - * @param assembly - the mutable assembly built from registered providers. - * @param context - the caller's per-assembly context. - * @mode waterfall - */ -'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly> -``` - -Expert waterfall over the assembled sections, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. - -- `assembly` — the mutable assembly built from registered providers. -- `context` — the caller's per-assembly context. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L27) - -### system-prompt/change - -**Mode:** `emit` - -```ts website-api -/** - * Emitted when any prompt provider changes. This registry notification is - * unfiltered because a global change affects every scope. - * @mode emit - */ -'system-prompt/change'(): void -``` - -Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L33) - -## tools/* - -### tools/change - -**Mode:** `emit` - -```ts website-api -/** - * A tool was registered or unregistered, or a scoped restriction changed - * (the available tool set changed — possibly for one scope only). An - * UNFILTERED registry-subject notification, deliberately not scope-filtered - * dispatch: a global change concerns every agent's next assembly, so a - * scoped listener subscribing here sees every change, not just its own - * scope's. - * @mode emit - */ -'tools/change'(): void -``` - -A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only). An UNFILTERED registry-subject notification, deliberately not scope-filtered dispatch: a global change concerns every agent's next assembly, so a scoped listener subscribing here sees every change, not just its own scope's. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L116) - -### tools/execute - -**Mode:** `waterfall` - -```ts website-api -/** - * Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns - * a normalized result; wrappers may change only `exec.signal`, while call - * identity remains immutable. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - * @param exec - the allowed call about to dispatch (name, parsed arguments, caller agent, signal). - * @mode waterfall - */ -'tools/execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult> -``` - -Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a normalized result; wrappers may change only `exec.signal`, while call identity remains immutable. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - -- `exec` — the allowed call about to dispatch (name, parsed arguments, caller agent, signal). - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L89) - -### tools/post-execute - -**Mode:** `waterfall` - -```ts website-api -/** - * Accept, replace, enrich, or block a normalized dispatch result. `next()` - * accepts it unchanged; thrown tools still reach this seam as errors. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - * @param exec - the call that just ran (name, parsed arguments, caller agent). - * @param result - the dispatch outcome a listener may accept, replace, or block. - * @mode waterfall - */ -'tools/post-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision> -``` - -Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts it unchanged; thrown tools still reach this seam as errors. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - -- `exec` — the call that just ran (name, parsed arguments, caller agent). -- `result` — the dispatch outcome a listener may accept, replace, or block. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L98) - -### tools/pre-execute - -**Mode:** `waterfall` - -```ts website-api -/** - * Allow, deny, or ask before dispatch. `next()` delegates to allow; missing - * approval support turns `ask` into denial. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - * @param exec - the pending call (name, parsed arguments, caller agent). - * @mode waterfall - */ -'tools/pre-execute'(this: Scoped<ToolRegistry>, exec: ToolExecution, next: () => Promise<PreToolDecision>): Promise<PreToolDecision> -``` - -Allow, deny, or ask before dispatch. `next()` delegates to allow; missing approval support turns `ask` into denial. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. - -- `exec` — the pending call (name, parsed arguments, caller agent). - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L80) - -### tools/result - -**Mode:** `emit` - -```ts website-api -/** - * Observe the frozen, lossless-JSON final outcome. Listener failures are contained. - * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`. - * @param exec - the execution object that traversed the pipeline. - * @param result - a deep-frozen snapshot of the final returned result. - * @mode emit - */ -'tools/result'(this: Scoped<ToolRegistry>, exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined -``` - -Observe the frozen, lossless-JSON final outcome. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`. - -- `exec` — the execution object that traversed the pipeline. -- `result` — a deep-frozen snapshot of the final returned result. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L106) - -## workflow/* - -### workflow/agent-end - -**Mode:** `emit` - -```ts website-api -/** - * One `agent()` call settled (clean result, child failure, or run - * cancellation). Paired with {@link Events['workflow/agent-start']} by - * `agent.seq`, exactly once per started call on every stop path — on an - * engine termination path (a worker killed past its grace) the end is - * engine-synthesized with outcome `'cancelled'`. - * @param info - the run's identity snapshot. - * @param agent - the call identity plus its outcome. - * @mode emit - */ -'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void -``` - -One `agent()` call settled (clean result, child failure, or run cancellation). Paired with Events['workflow/agent-start'] by `agent.seq`, exactly once per started call on every stop path — on an engine termination path (a worker killed past its grace) the end is engine-synthesized with outcome `'cancelled'`. - -- `info` — the run's identity snapshot. -- `agent` — the call identity plus its outcome. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L81) - -### workflow/agent-start - -**Mode:** `emit` - -```ts website-api -/** - * One `agent()` call established a ready child run. Paired with - * {@link Events['workflow/agent-end']} by `agent.seq`. A call that never - * receives a ready run from the provider emits neither - * event in this pair. - * @param info - the run's identity snapshot. - * @param agent - the call's sequence number, label, phase, and child id. - * @mode emit - */ -'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void -``` - -One `agent()` call established a ready child run. Paired with Events['workflow/agent-end'] by `agent.seq`. A call that never receives a ready run from the provider emits neither event in this pair. - -- `info` — the run's identity snapshot. -- `agent` — the call's sequence number, label, phase, and child id. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L70) - -### workflow/end - -**Mode:** `emit` - -```ts website-api -/** - * A workflow run settled (any stop reason). Fired when - * {@link WorkflowRun.result} resolves. Paired with - * {@link Events['workflow/start']}. - * @param info - the run's identity snapshot. - * @param result - the outcome data (stop reason, error, agent count) — - * deliberately WITHOUT the result value (see {@link WorkflowResultInfo}). - * @mode emit - */ -'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void -``` - -A workflow run settled (any stop reason). Fired when WorkflowRun.result resolves. Paired with Events['workflow/start']. - -- `info` — the run's identity snapshot. -- `result` — the outcome data (stop reason, error, agent count) — deliberately WITHOUT the result value (see `WorkflowResultInfo`). - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L91) - -### workflow/log - -**Mode:** `emit` - -```ts website-api -/** - * The script emitted a narration line (a `log(message)` call). - * @param info - the run's identity snapshot. - * @param message - the logged message, verbatim. - * @mode emit - */ -'workflow/log'(info: WorkflowRunInfo, message: string): void -``` - -The script emitted a narration line (a `log(message)` call). - -- `info` — the run's identity snapshot. -- `message` — the logged message, verbatim. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L60) - -### workflow/phase - -**Mode:** `emit` - -```ts website-api -/** - * The script entered a phase (a `phase(title)` call) — progress grouping - * for observers; no execution semantics. - * @param info - the run's identity snapshot. - * @param title - the phase title, verbatim. - * @mode emit - */ -'workflow/phase'(info: WorkflowRunInfo, title: string): void -``` - -The script entered a phase (a `phase(title)` call) — progress grouping for observers; no execution semantics. - -- `info` — the run's identity snapshot. -- `title` — the phase title, verbatim. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L53) - -### workflow/start - -**Mode:** `emit` - -```ts website-api -/** - * A workflow run started — the script's meta block validated, the body - * about to execute. Paired with {@link Events['workflow/end']}. - * @param info - the run's identity snapshot (id + meta). - * @mode emit - */ -'workflow/start'(info: WorkflowRunInfo): void -``` - -A workflow run started — the script's meta block validated, the body about to execute. Paired with Events['workflow/end']. - -- `info` — the run's identity snapshot (id + meta). - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L45) diff --git a/website/zh-CN/api/harness/fs.md b/website/zh-CN/api/harness/fs.md deleted file mode 100644 index aed69681f6..0000000000 --- a/website/zh-CN/api/harness/fs.md +++ /dev/null @@ -1,205 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.fs - -`FileSystem` (abstract seam) — provided by `@deepseek-ai/dsh-fs`. - -Abstract filesystem provider. Targets must preserve identity across aliases; reads expose regular UTF-8 text or typed errors, listings are stable and content-free, and mutations are atomic. Optional guards add stale protection without changing the unguarded provider contract. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L80) - -### ctx.fs.resolve(path, opts?) - -```ts website-api -/** - * 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 - * async even though the local backend only normalizes + realpaths. - * - * @param path - the path to resolve; relative paths resolve against `opts.cwd`. - * @param opts - optional cwd override and cancellation signal. - * @returns the stable target; the same file yields the same `targetKey`. - */ -abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> -``` - -Resolve a model/plugin-supplied path into a stable FsTarget. May perform I/O (a remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence async even though the local backend only normalizes + realpaths. - -- `path` — the path to resolve; relative paths resolve against `opts.cwd`. -- `opts` — optional cwd override and cancellation signal. - -**Returns** the stable target; the same file yields the same `targetKey`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L94) - -### ctx.fs.stat(target, signal?) - -```ts website-api -/** - * Return target metadata, or `undefined` when the target does not exist. - * @param target - the resolved target to stat. - * @param signal - aborts the metadata round-trip. - * @returns metadata only, never content; undefined for an absent target. - */ -abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> -``` - -Return target metadata, or `undefined` when the target does not exist. - -- `target` — the resolved target to stat. -- `signal` — aborts the metadata round-trip. - -**Returns** metadata only, never content; undefined for an absent target. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L102) - -### ctx.fs.lstat(path, opts?, signal?) - -```ts website-api -/** - * Return path metadata without following the final path component when it is a - * symbolic link. This is intentionally path-shaped, not target-shaped: - * {@link resolve} follows symlinks to produce the stable identity used by - * normal reads/writes, while `lstat` lets a consumer reject the path itself - * before that follow happens. - * - * `opts.cwd` follows {@link resolve}'s cwd rules. `undefined` means the path is - * absent. - * @param path - the path to inspect; relative paths resolve against `opts.cwd`. - * @param opts - `cwd` overrides the backend's default base for relative paths. - * @param signal - aborts the metadata round-trip. - * @returns metadata only, never content; undefined for an absent path. - */ -abstract lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined> -``` - -Return path metadata without following the final path component when it is a symbolic link. This is intentionally path-shaped, not target-shaped: resolve follows symlinks to produce the stable identity used by normal reads/writes, while `lstat` lets a consumer reject the path itself before that follow happens. -`opts.cwd` follows resolve's cwd rules. `undefined` means the path is absent. - -- `path` — the path to inspect; relative paths resolve against `opts.cwd`. -- `opts` — `cwd` overrides the backend's default base for relative paths. -- `signal` — aborts the metadata round-trip. - -**Returns** metadata only, never content; undefined for an absent path. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L118) - -### ctx.fs.readText(target, signal?) - -```ts website-api -/** - * Read the whole regular text file as a single decoded string. - * @param target - the resolved target to read. - * @param signal - aborts the read. - * @returns the full decoded UTF-8 content. - */ -abstract readText(target: FsTarget, signal?: AbortSignal): Promise<string> -``` - -Read the whole regular text file as a single decoded string. - -- `target` — the resolved target to read. -- `signal` — aborts the read. - -**Returns** the full decoded UTF-8 content. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L126) - -### ctx.fs.streamText(target, signal?) - -```ts website-api -/** - * Stream the whole regular text file as decoded text chunks (same text - * semantics as {@link readText}, for large files). The backend owns - * cross-chunk UTF-8 decoding and binary rejection so the policy layer never - * touches raw bytes. - * @param target - the resolved target to read. - * @param signal - aborts the stream, including between chunks. - * @returns the chunk iterable, decoded and validated like {@link readText}. - */ -abstract streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>> -``` - -Stream the whole regular text file as decoded text chunks (same text semantics as readText, for large files). The backend owns cross-chunk UTF-8 decoding and binary rejection so the policy layer never touches raw bytes. - -- `target` — the resolved target to read. -- `signal` — aborts the stream, including between chunks. - -**Returns** the chunk iterable, decoded and validated like `readText`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L137) - -### ctx.fs.listDir(target, signal?) - -```ts website-api -/** - * List direct children of a directory in stable name order. Returns resolved - * child targets plus cheap metadata only; never reads file contents. - * @param target - the resolved directory target. - * @param signal - aborts the listing. - * @returns one entry per direct child, in stable name order. - */ -abstract listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]> -``` - -List direct children of a directory in stable name order. Returns resolved child targets plus cheap metadata only; never reads file contents. - -- `target` — the resolved directory target. -- `signal` — aborts the listing. - -**Returns** one entry per direct child, in stable name order. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L146) - -### ctx.fs.writeText(target, content, expected?, signal?) - -```ts website-api -/** - * Atomically create or replace UTF-8 text. `expected` guards intent and - * staleness; omission allows unconditional overwrite. - * @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. - * @returns the outcome, including the version the write produced. - */ -abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal): Promise<FsWriteOutcome> -``` - -Atomically create or replace UTF-8 text. `expected` guards intent and staleness; omission allows unconditional overwrite. - -- `target` — the resolved target to write. -- `content` — the full new file content. -- `expected` — the write intent guarding the write; omit for unconditional. -- `signal` — aborts before the atomic rename takes effect. - -**Returns** the outcome, including the version the write produced. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L157) - -### ctx.fs.editText(target, edit, expected?, signal?) - -```ts website-api -/** - * Atomically edit literal text. When supplied, the version guard is checked - * before matching so stale content reports `FS_STALE_VERSION`; omission edits - * the current content without a freshness precondition. - * @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. - * @returns the outcome, including the version the edit produced. - */ -abstract editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal): Promise<FsEditOutcome> -``` - -Atomically edit literal text. When supplied, the version guard is checked before matching so stale content reports `FS_STALE_VERSION`; omission edits the current content without a freshness precondition. - -- `target` — the resolved target to edit. -- `edit` — the literal search/replace request. -- `expected` — the version guard; omit for an unconditional edit. -- `signal` — aborts before the atomic rename takes effect. - -**Returns** the outcome, including the version the edit produced. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/fs/fs/src/index.ts#L169) diff --git a/website/zh-CN/api/harness/llm.md b/website/zh-CN/api/harness/llm.md deleted file mode 100644 index e8e4d53a16..0000000000 --- a/website/zh-CN/api/harness/llm.md +++ /dev/null @@ -1,94 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.llm - -`LlmService` — provided by `@deepseek-ai/dsh-llm`. - -The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L97) - -### ctx.llm.registerAdapter(providers, adapter) - -```ts website-api -/** - * Register an adapter for the given provider routes. Throws `LlmError` with code - * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing). - * Disposed with the fiber. - * @param providers - every provider route this adapter should serve. - * @param adapter - the adapter that streams calls for those providers. - * @returns the disposer that unregisters all of them. - */ -registerAdapter(providers: string[], adapter: LlmAdapter): () => void -``` - -Register an adapter for the given provider routes. Throws `LlmError` with code `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing). Disposed with the fiber. - -- `providers` — every provider route this adapter should serve. -- `adapter` — the adapter that streams calls for those providers. - -**Returns** the disposer that unregisters all of them. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L112) - -### ctx.llm.listProviders() - -```ts website-api -/** - * Describe provider routes with a registered adapter. - * @returns detached provider metadata in registration order. - */ -listProviders(): LlmProviderInfo[] -``` - -Describe provider routes with a registered adapter. - -**Returns** detached provider metadata in registration order. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L143) - -### ctx.llm.listModels(provider) - -```ts website-api -/** - * Discover models advertised by one registered provider. Catalog membership - * is advisory and never changes routing or request validation. - * @param provider - registered provider route to inspect. - * @returns detached model metadata in adapter-preferred order. - */ -async listModels(provider: string): Promise<LlmModelInfo[]> -``` - -Discover models advertised by one registered provider. Catalog membership is advisory and never changes routing or request validation. - -- `provider` — registered provider route to inspect. - -**Returns** detached model metadata in adapter-preferred order. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L153) - -### ctx.llm.stream(options) - -```ts website-api -/** - * Stream one model call as raw chunks (token-level deltas). Throws - * `LlmError` with code `NO_ADAPTER` if no adapter is registered for - * `options.provider`. Replay state is retained only when the same adapter - * instance owns its historical provider and the target provider. Final - * adapter selection, dispatch, and iteration failures retain their original - * Error identity and are tagged in a call-local scope for narrow agent-loop - * request recovery; middleware and nested-call failures remain untagged for - * the outer call. - * @param options - the full request; `options.provider` selects the adapter. - * @returns the chunk stream, possibly wrapped by `llm/stream` listeners. - */ -stream(options: GenerateOptions): AsyncIterable<StreamChunk> -``` - -Stream one model call as raw chunks (token-level deltas). Throws `LlmError` with code `NO_ADAPTER` if no adapter is registered for `options.provider`. Replay state is retained only when the same adapter instance owns its historical provider and the target provider. Final adapter selection, dispatch, and iteration failures retain their original Error identity and are tagged in a call-local scope for narrow agent-loop request recovery; middleware and nested-call failures remain untagged for the outer call. - -- `options` — the full request; `options.provider` selects the adapter. - -**Returns** the chunk stream, possibly wrapped by `llm/stream` listeners. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/llm/src/index.ts#L264) diff --git a/website/zh-CN/api/harness/permission.md b/website/zh-CN/api/harness/permission.md deleted file mode 100644 index c9f99adbf1..0000000000 --- a/website/zh-CN/api/harness/permission.md +++ /dev/null @@ -1,104 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.permission - -`PermissionService` — provided by `@deepseek-ai/dsh-permission`. - -Owns the deployment's permission presets and their write path. Requires a confining `ctx.bash` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L94) - -### ctx.permission.names - -```ts website-api -/** - * The advertised preset names, in the preset table's declaration order. - * @returns every switchable preset name. - */ -get names(): readonly string[] -``` - -The advertised preset names, in the preset table's declaration order. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L134) - -### ctx.permission.current(events) - -```ts website-api -/** - * Resolve the preset matching the effective knob values. A still-matching - * last selection wins shared-bundle ties; otherwise the first table match - * wins, or {@link CUSTOM_PRESET} when no entry matches. - * @param events - the session's events in log order. - * @returns the effective preset name, or `custom` when nothing matches. - */ -current(events: readonly SessionEvent[]): string -``` - -Resolve the preset matching the effective knob values. A still-matching last selection wins shared-bundle ties; otherwise the first table match wins, or CUSTOM_PRESET when no entry matches. - -- `events` — the session's events in log order. - -**Returns** the effective preset name, or `custom` when nothing matches. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L145) - -### ctx.permission.resolve(name) - -```ts website-api -/** - * Resolve a preset's knob bundle. - * @param name - the preset name to resolve. - * @returns the configured bundle. - * @throws when `name` is not in the table. - */ -resolve(name: string): PresetSpec -``` - -Resolve a preset's knob bundle. - -- `name` — the preset name to resolve. - -**Returns** the configured bundle. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L166) - -### ctx.permission.optionOf(name) - -```ts website-api -/** - * Build the client option for a table entry or {@link CUSTOM_PRESET}. A - * missing label falls back to the table key. - * @param name - a table key, or `custom`. - * @returns the option a client renders. - * @throws when `name` is neither a table key nor `custom`. - */ -optionOf(name: string): PresetOption -``` - -Build the client option for a table entry or CUSTOM_PRESET. A missing label falls back to the table key. - -- `name` — a table key, or `custom`. - -**Returns** the option a client renders. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L181) - -### ctx.permission.set(session, name) - -```ts website-api -/** - * Record a changed preset, then update each changed knob through its own - * setter. Selecting the effective preset again appends nothing. - * @param session - the session the switch belongs to. - * @param name - the preset to switch to; unknown names throw. - */ -set(session: Session, name: string): void -``` - -Record a changed preset, then update each changed knob through its own setter. Selecting the effective preset again appends nothing. - -- `session` — the session the switch belongs to. -- `name` — the preset to switch to; unknown names throw. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/permission/src/index.ts#L195) diff --git a/website/zh-CN/api/harness/sandbox.md b/website/zh-CN/api/harness/sandbox.md deleted file mode 100644 index 45187e9d31..0000000000 --- a/website/zh-CN/api/harness/sandbox.md +++ /dev/null @@ -1,35 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.sandbox - -`SandboxProvider` (abstract seam) — provided by `@deepseek-ai/dsh-sandbox`. - -Abstract process-sandbox service. confine must return enforcing argv or fail closed at wrap or runner-execution time; silent unconfined passthrough is forbidden. Functional probes arbitrate multi-runner chains and may be skipped for a sole candidate, whose own refusal remains the fail-closed end. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/sandbox/sandbox/src/index.ts#L111) - -### ctx.sandbox.confine(argv, policy) - -```ts website-api -/** - * Wrap `argv` so it executes confined under `policy` on this host; the - * caller spawns the returned argv in place of its own. - * @param argv - the exact argv the caller is about to spawn (program plus - * arguments), NOT a shell string — a shell-shaped consumer passes - * `['bash', '-c', command]`. - * @param policy - the file-effect policy this execution runs under, - * carried per call (see {@link SandboxPolicy}). - * @returns the argv to spawn instead, plus the enforcement completeness - * the selected backend achieves for it. - */ -abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv -``` - -Wrap `argv` so it executes confined under `policy` on this host; the caller spawns the returned argv in place of its own. - -- `argv` — the exact argv the caller is about to spawn (program plus arguments), NOT a shell string — a shell-shaped consumer passes `['bash', '-c', command]`. -- `policy` — the file-effect policy this execution runs under, carried per call (see `SandboxPolicy`). - -**Returns** the argv to spawn instead, plus the enforcement completeness the selected backend achieves for it. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/sandbox/sandbox/src/index.ts#L127) diff --git a/website/zh-CN/api/harness/session-persistence.md b/website/zh-CN/api/harness/session-persistence.md deleted file mode 100644 index f200cb1801..0000000000 --- a/website/zh-CN/api/harness/session-persistence.md +++ /dev/null @@ -1,109 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.sessionPersistence - -`SessionPersistence` (abstract seam) — provided by `@deepseek-ai/dsh-session-persistence`. - -Durable append-only session storage. Implementations preserve contiguous, losslessly JSON-serializable events; append resolves only after durability, and load balances a complete interrupted tail without rewriting committed events. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L42) - -### ctx.sessionPersistence.locate(meta) - -```ts website-api -/** - * Resolve this backend's independent local artifact for a session without - * reading, creating, flushing, or otherwise materializing it. Backends such - * as SQLite that do not own one artifact per session return `undefined`. - * @param meta - the immutable session header whose artifact is requested. - * @returns the backend-specific absolute location, when one exists. - */ -abstract locate(meta: SessionHeader): SessionLocation | undefined -``` - -Resolve this backend's independent local artifact for a session without reading, creating, flushing, or otherwise materializing it. Backends such as SQLite that do not own one artifact per session return `undefined`. - -- `meta` — the immutable session header whose artifact is requested. - -**Returns** the backend-specific absolute location, when one exists. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L54) - -### ctx.sessionPersistence.create(meta) - -```ts website-api -/** - * Register a new session's metadata. A backend MAY defer the physical write - * until the first {@link append} (lazy materialization), in which case a - * created-but-never-appended session is absent from {@link list} - * — abandoned sessions leave nothing behind. - * @param meta - the immutable header (id, version, cwd, lineage) to record. - */ -abstract create(meta: SessionHeader): Promise<void> -``` - -Register a new session's metadata. A backend MAY defer the physical write until the first append (lazy materialization), in which case a created-but-never-appended session is absent from list — abandoned sessions leave nothing behind. - -- `meta` — the immutable header (id, version, cwd, lineage) to record. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L63) - -### ctx.sessionPersistence.append(id, events) - -```ts website-api -/** - * Durably persist a batch of events (called from the write-behind drain at - * the `session/flush` checkpoint). Honors the append-only and contiguous-seq - * contracts: the first event's `seq` MUST equal the stored next-seq (after - * `load` has durably closed any interrupted turn). Rejects non-JSON- - * serializable `event.data` with an error naming the offending event type. - * @param id - the session the batch belongs to. - * @param events - the contiguous batch to persist, in seq order. - */ -abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void> -``` - -Durably persist a batch of events (called from the write-behind drain at the `session/flush` checkpoint). Honors the append-only and contiguous-seq contracts: the first event's `seq` MUST equal the stored next-seq (after `load` has durably closed any interrupted turn). Rejects non-JSON- serializable `event.data` with an error naming the offending event type. - -- `id` — the session the batch belongs to. -- `events` — the contiguous batch to persist, in seq order. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L74) - -### ctx.sessionPersistence.load(id) - -```ts website-api -/** - * Load a header and balanced contiguous log. A complete interrupted final - * turn is preserved and durably closed with missing tool errors plus any open - * step and turn boundaries; only a torn final record is discarded. Unknown - * versions and corruption in the committed prefix reject. - * @param id - the persisted session to reload. - * @returns the header and a log ending on a balanced `turn/end`. - */ -abstract load(id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }> -``` - -Load a header and balanced contiguous log. A complete interrupted final turn is preserved and durably closed with missing tool errors plus any open step and turn boundaries; only a torn final record is discarded. Unknown versions and corruption in the committed prefix reject. - -- `id` — the persisted session to reload. - -**Returns** the header and a log ending on a balanced `turn/end`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L84) - -### ctx.sessionPersistence.list() - -```ts website-api -/** - * Lightweight listing from metadata, without a full-log parse. - * @returns one header per materialized session. - */ -abstract list(): Promise<SessionHeader[]> -``` - -Lightweight listing from metadata, without a full-log parse. - -**Returns** one header per materialized session. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-persistence/session-persistence/src/index.ts#L90) diff --git a/website/zh-CN/api/harness/session-query.md b/website/zh-CN/api/harness/session-query.md deleted file mode 100644 index cc826c7af4..0000000000 --- a/website/zh-CN/api/harness/session-query.md +++ /dev/null @@ -1,103 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.sessionQuery - -`SessionQueryService` — provided by `@deepseek-ai/dsh-session-query`. - -Live-preferred logical-corpus exact-read and relationship-tracing service. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L38) - -### ctx.sessionQuery.listSessions() - -```ts website-api -/** - * List the complete logical corpus using live-preferred records. - * @returns deterministic newest-first cloned session records. - */ -listSessions(): Promise<SessionRecord[]> -``` - -List the complete logical corpus using live-preferred records. - -**Returns** deterministic newest-first cloned session records. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L63) - -### ctx.sessionQuery.listEvents(sessionId) - -```ts website-api -/** - * List lightweight raw-log event records for one logical session. - * @param sessionId - live-preferred session id to read. - * @returns event records in ascending seq order. - */ -async listEvents(sessionId: SessionId): Promise<SessionEventRecord[]> -``` - -List lightweight raw-log event records for one logical session. - -- `sessionId` — live-preferred session id to read. - -**Returns** event records in ascending seq order. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L72) - -### ctx.sessionQuery.traceSession(sessionId) - -```ts website-api -/** - * Trace known ancestry and descendants from one corpus observation. - * @param sessionId - logical session id to trace. - * @returns a complete lineage or an explicit unresolved parent boundary. - * @throws when corpus resolution fails, the target is absent, or its known ancestry cycles. - */ -async traceSession(sessionId: SessionId): Promise<SessionLineageTrace> -``` - -Trace known ancestry and descendants from one corpus observation. - -- `sessionId` — logical session id to trace. - -**Returns** a complete lineage or an explicit unresolved parent boundary. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L83) - -### ctx.sessionQuery.traceEvent(request) - -```ts website-api -/** - * Trace one event's direct positional and provenance relationships. - * @param request - target session id and event seq. - * @returns direct links plus the target's positional replacement chain. - * @throws when source resolution fails, the target is absent, or surface/provenance validation fails. - */ -async traceEvent(request: SessionEventTraceRequest): Promise<SessionEventTrace> -``` - -Trace one event's direct positional and provenance relationships. - -- `request` — target session id and event seq. - -**Returns** direct links plus the target's positional replacement chain. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L94) - -### ctx.sessionQuery.readEvent(request) - -```ts website-api -/** - * Read one full event plus a bounded raw-log context window. - * @param request - target session/seq and context sizes. - * @returns cloned target and neighboring events. - */ -async readEvent(request: SessionEventReadRequest): Promise<SessionEventWindow> -``` - -Read one full event plus a bounded raw-log context window. - -- `request` — target session/seq and context sizes. - -**Returns** cloned target and neighboring events. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/session-query/session-query/src/index.ts#L104) diff --git a/website/zh-CN/api/harness/sessions.md b/website/zh-CN/api/harness/sessions.md deleted file mode 100644 index f59001009d..0000000000 --- a/website/zh-CN/api/harness/sessions.md +++ /dev/null @@ -1,223 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.sessions - -`SessionStore` — provided by `@deepseek-ai/dsh-session`. - -In-memory session store (`ctx.sessions`). -Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L577) - -### ctx.sessions.create(id?, options?) - -```ts website-api -/** - * 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`). - * - * 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 - * — fold the session lifecycle into the agent's own effect via - * {@link prepare} + {@link enter} + {@link announce} (see - * `dsh-agent-loop`'s creation transaction). - * - * @param id - the session id; omitted, the store mints `session-<n>`. - * @param options - seed events and/or creation metadata for the header. - * @returns the live session, already entered and announced. - * @throws if a session with `id` already exists, metadata is not a plain - * lossless-JSON record with valid scalar fields, or `meta.cwd` is a - * non-absolute path (storage backends key directories off it). - */ -create(id?: SessionId, options?: CreateSessionOptions): Session -``` - -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 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 — fold the session lifecycle into the agent's own effect via prepare + enter + announce (see `dsh-agent-loop`'s creation transaction). - -- `id` — the session id; omitted, the store mints `session-<n>`. -- `options` — seed events and/or creation metadata for the header. - -**Returns** the live session, already entered and announced. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L606) - -### ctx.sessions.prepare(id?, options?) - -```ts website-api -/** - * Build a session WITHOUT entering it into the store — validate the id/cwd and - * construct the {@link Session} (with its immutable {@link SessionHeader}). - * Pairs with {@link enter} + {@link announce}: a caller that owns a composite - * `ctx.effect` (the agent factory) folds the session lifecycle into that ONE - * effect so a fiber unload tears the session + agent down as a single ORDERED - * chain rather than as racing sibling effects — which would remove the publication hooks - * before the loop's closing `session/flush`, dropping the closing events. - * - * @param id - the session id; omitted, the store mints `session-<n>`. - * @param options - seed events and/or creation metadata for the header. - * @returns the constructed session, NOT yet in the store. - * @throws if a session with `id` already exists, metadata is not a plain - * lossless-JSON record with valid scalar fields, or `meta.cwd` is a - * non-absolute path. - */ -prepare(id?: SessionId, options?: CreateSessionOptions): Session -``` - -Build a session WITHOUT entering it into the store — validate the id/cwd and construct the Session (with its immutable SessionHeader). Pairs with enter + announce: a caller that owns a composite `ctx.effect` (the agent factory) folds the session lifecycle into that ONE effect so a fiber unload tears the session + agent down as a single ORDERED chain rather than as racing sibling effects — which would remove the publication hooks before the loop's closing `session/flush`, dropping the closing events. - -- `id` — the session id; omitted, the store mints `session-<n>`. -- `options` — seed events and/or creation metadata for the header. - -**Returns** the constructed session, NOT yet in the store. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L635) - -### ctx.sessions.enter(session) - -```ts website-api -/** - * Enter a {@link prepare}d session into the store: install the module-private - * append publication hooks and add it to the store. Returns the DETACH - * disposer (hooks + store removal). Does NOT emit `session/created` — - * the caller yields this disposer inside its effect and THEN calls - * {@link announce}, so a throwing `session/created` listener rolls the attach - * back instead of leaking it. - * - * Re-checks the id for a duplicate: `prepare` and `enter` are public - * cross-package primitives and a caller may interleave arbitrary work (or - * another create) between them, so a stale prepared session must NOT overwrite - * a live store entry of the same id — its detach disposer would later delete - * the REAL session. The {@link create} convenience and the agent factory call - * the two back-to-back so they never trip this, but the public seam cannot - * assume that. - * - * @param session - a {@link prepare}d session not yet in the store. - * @returns the detach disposer (publication hooks + store removal). When called from - * a synchronous `session/created` listener, removal and disposal wait until - * that creation dispatch unwinds. - * @throws if a session with this id is already in the store. - */ -enter(session: Session): () => void -``` - -Enter a prepared session into the store: install the module-private append publication hooks and add it to the store. Returns the DETACH disposer (hooks + store removal). Does NOT emit `session/created` — the caller yields this disposer inside its effect and THEN calls announce, so a throwing `session/created` listener rolls the attach back instead of leaking it. -Re-checks the id for a duplicate: `prepare` and `enter` are public cross-package primitives and a caller may interleave arbitrary work (or another create) between them, so a stale prepared session must NOT overwrite a live store entry of the same id — its detach disposer would later delete the REAL session. The create convenience and the agent factory call the two back-to-back so they never trip this, but the public seam cannot assume that. - -- `session` — a `prepare`d session not yet in the store. - -**Returns** the detach disposer (publication hooks + store removal). When called from a synchronous `session/created` listener, removal and disposal wait until that creation dispatch unwinds. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L679) - -### ctx.sessions.announce(session) - -```ts website-api -/** Emit `session/created` exactly once for an {@link enter}ed session (with - * the carrier {@link enter} captured). Separate from {@link enter} so the - * caller can yield the detach disposer first (rollback safety — see - * {@link enter}). - * @param session - the entered session to announce to listeners. - * @throws if the session is not live or its announcement already began, - * including a reentrant call from a creation listener. */ -announce(session: Session): void -``` - -Emit `session/created` exactly once for an entered session (with the carrier enter captured). Separate from enter so the caller can yield the detach disposer first (rollback safety — see enter). - -- `session` — the entered session to announce to listeners. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L734) - -### ctx.sessions.flush(session) - -```ts website-api -/** - * Dispatch the awaited `session/flush` durability checkpoint for `session`, - * with the carrier captured at {@link enter}. THE flush entry point: the - * store owns the carrier, so callers (the loop's turn-end checkpoint, idle - * injection, teardown drains) must come through here rather than dispatch a - * raw `ctx.parallel('session/flush', …)` — one owner, one spelling, and the - * scoped-dispatch invariant can pin it. - * @param session - the session whose buffered events must reach durable storage. - * @returns resolves when every flush listener has settled; after all settle, - * rejects with the first registered listener failure if any listener failed. - */ -async flush(session: Session): Promise<void> -``` - -Dispatch the awaited `session/flush` durability checkpoint for `session`, with the carrier captured at enter. THE flush entry point: the store owns the carrier, so callers (the loop's turn-end checkpoint, idle injection, teardown drains) must come through here rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner, one spelling, and the scoped-dispatch invariant can pin it. - -- `session` — the session whose buffered events must reach durable storage. - -**Returns** resolves when every flush listener has settled; after all settle, rejects with the first registered listener failure if any listener failed. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L786) - -### ctx.sessions.get(id) - -```ts website-api -/** - * Look up a live session. - * @param id - the session id to look up. - * @returns the session, or undefined when no live session has that id. - */ -get(id: SessionId): Session | undefined -``` - -Look up a live session. - -- `id` — the session id to look up. - -**Returns** the session, or undefined when no live session has that id. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L818) - -### ctx.sessions.list() - -```ts website-api -/** - * All live sessions, in creation order. - * @returns a fresh array; mutating it does not affect the store. - */ -list(): Session[] -``` - -All live sessions, in creation order. - -**Returns** a fresh array; mutating it does not affect the store. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L826) - -### ctx.sessions.fork(source, boundary?, childSessionId?) - -```ts website-api -/** - * Create a live child session from a turn-enclosed prefix of a live source. - * `boundary` is an inclusive source event seq; omitted means the source's - * current last event. A non-empty selected slice must end at `turn/end`. - * - * @param source - Live source session object or id. - * @param boundary - Inclusive source event seq to fork through; omitted means - * the source's current last event, and omitted on an empty source forks an - * empty child. - * @param childSessionId - Optional child session id; omitted delegates to - * `SessionStore`'s id policy. - * @returns The created live child session. - */ -fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session -``` - -Create a live child session from a turn-enclosed prefix of a live source. `boundary` is an inclusive source event seq; omitted means the source's current last event. A non-empty selected slice must end at `turn/end`. - -- `source` — Live source session object or id. -- `boundary` — Inclusive source event seq to fork through; omitted means the source's current last event, and omitted on an empty source forks an empty child. -- `childSessionId` — Optional child session id; omitted delegates to `SessionStore`'s id policy. - -**Returns** The created live child session. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/session/src/index.ts#L843) diff --git a/website/zh-CN/api/harness/skills.md b/website/zh-CN/api/harness/skills.md deleted file mode 100644 index 31ab6cd1d9..0000000000 --- a/website/zh-CN/api/harness/skills.md +++ /dev/null @@ -1,96 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.skills - -`SkillService` — provided by `@deepseek-ai/dsh-skill`. - -Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted model-visible summaries, and loads full skill bodies on demand. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L141) - -### ctx.skills.registerProvider(provider) - -```ts website-api -/** - * Register a borrowed same-process provider synchronously during plugin apply. Duplicate and - * reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters - * the provider and invalidates catalog caches. - * @param provider - the provider to register by `provider.name`. - * @returns the exact Cordis effect disposer that unregisters this provider; - * composite effects may yield it directly to preserve teardown ordering. - */ -registerProvider(provider: SkillProvider): () => void -``` - -Register a borrowed same-process provider synchronously during plugin apply. Duplicate and reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters the provider and invalidates catalog caches. - -- `provider` — the provider to register by `provider.name`. - -**Returns** the exact Cordis effect disposer that unregisters this provider; composite effects may yield it directly to preserve teardown ordering. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L168) - -### ctx.skills.register(skill) - -```ts website-api -/** - * Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which - * outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and - * receives a no-op disposer so it cannot remove the winner. - * @param skill - the complete skill definition to expose for discovery. - * @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches. - */ -register(skill: SkillRegistration): () => void -``` - -Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and receives a no-op disposer so it cannot remove the winner. - -- `skill` — the complete skill definition to expose for discovery. - -**Returns** the exact Cordis effect disposer, preserving composite teardown order and invalidating caches. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L199) - -### ctx.skills.list(options?) - -```ts website-api -/** - * List model-invocable skill summaries for a workspace. Lookup options and - * provider candidates are readonly same-process values borrowed throughout - * discovery. - * @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery. - * @returns sorted summaries, excluding skills disabled for model invocation. - */ -async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]> -``` - -List model-invocable skill summaries for a workspace. Lookup options and provider candidates are readonly same-process values borrowed throughout discovery. - -- `options` — lookup options; `cwd` selects project roots and `signal` cancels discovery. - -**Returns** sorted summaries, excluding skills disabled for model invocation. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L230) - -### ctx.skills.get(name, options?) - -```ts website-api -/** - * Load and validate the winning candidate, passing its opaque discovery locator back to the - * provider. Cancellation is rechecked after selection, including cache hits, and raced against - * loading so an uncooperative provider cannot hang the caller. - * @param name - kebab-case skill name. - * @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work. - * @returns the full skill, including body content, or `undefined`. - */ -async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined> -``` - -Load and validate the winning candidate, passing its opaque discovery locator back to the provider. Cancellation is rechecked after selection, including cache hits, and raced against loading so an uncooperative provider cannot hang the caller. - -- `name` — kebab-case skill name. -- `options` — lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work. - -**Returns** the full skill, including body content, or `undefined`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/skill/skill/src/index.ts#L246) diff --git a/website/zh-CN/api/harness/spill-store.md b/website/zh-CN/api/harness/spill-store.md deleted file mode 100644 index 942e811c6b..0000000000 --- a/website/zh-CN/api/harness/spill-store.md +++ /dev/null @@ -1,32 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.spillStore - -`SpillStore` (abstract seam) — provided by `@deepseek-ai/dsh-spill`. - -Abstract spill storage service. Subclass, implement saveText, and load the subclass as a plugin — it registers as `ctx.spillStore` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior). -Semantics every implementation must honor: -- saveText persists the FULL `content` verbatim and returns an opaque locator, exact byte length, and model-facing retrieval guidance. -- Storage is scoped by the request's SaveTextSpill.owner session; the backend chooses a private (not world-readable) location and a collision-free name derived from — never equal to — the caller's `suggestedName`. -- `saveText` REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable); the caller decides how to degrade (the spill policy treats a rejection as best-effort and keeps the inline result). - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/spill/spill/src/index.ts#L45) - -### ctx.spillStore.saveText(input) - -```ts website-api -/** - * Persist `input.content` to a session-scoped spill artifact. - * @param input - the owner, provenance, suggested name, and full text to save. - * @returns the saved artifact's {@link SpillRef}; rejects on a storage failure. - */ -abstract saveText(input: SaveTextSpill): Promise<SpillRef> -``` - -Persist `input.content` to a session-scoped spill artifact. - -- `input` — the owner, provenance, suggested name, and full text to save. - -**Returns** the saved artifact's `SpillRef`; rejects on a storage failure. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/spill/spill/src/index.ts#L55) diff --git a/website/zh-CN/api/harness/subagents.md b/website/zh-CN/api/harness/subagents.md deleted file mode 100644 index de71a5effe..0000000000 --- a/website/zh-CN/api/harness/subagents.md +++ /dev/null @@ -1,89 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.subagents - -`SubagentService` — provided by `@deepseek-ai/dsh-subagent`. - -Named provider registry and capability-checked start surface. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L153) - -### ctx.subagents.registerProvider(provider) - -```ts website-api -/** - * Register a provider under its name. Registration is effect-scoped and HMR - * safe; removing a provider blocks new starts but does not revoke runs that - * were already returned to their holders. - * @param provider - the trusted provider implementation. - * @returns the exact Cordis effect disposer. - */ -registerProvider(provider: SubagentProvider): () => void -``` - -Register a provider under its name. Registration is effect-scoped and HMR safe; removing a provider blocks new starts but does not revoke runs that were already returned to their holders. - -- `provider` — the trusted provider implementation. - -**Returns** the exact Cordis effect disposer. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L167) - -### ctx.subagents.getProvider(name) - -```ts website-api -/** - * Look up a provider by name. - * @param name - the provider name. - * @returns the provider, or undefined when absent. - */ -getProvider(name: string): SubagentProvider | undefined -``` - -Look up a provider by name. - -- `name` — the provider name. - -**Returns** the provider, or undefined when absent. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L190) - -### ctx.subagents.list() - -```ts website-api -/** - * List registered provider names in insertion order. - * @returns the registered names. - */ -list(): string[] -``` - -List registered provider names in insertion order. - -**Returns** the registered names. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L198) - -### ctx.subagents.start(name, request) - -```ts website-api -/** - * Establish a ready child on the named provider. Capability and semantic - * checks run before delegation. Provider ownership lasts until its promise - * fulfills; a rejection therefore has no run for the caller to dispose and - * emits no run lifecycle events. - * @param name - the provider to use. - * @param request - child prompt, parent, signal, and optional capabilities. - * @returns the ready holder-owned run. - */ -async start(name: string, request: SubagentStartRequest): Promise<SubagentRun> -``` - -Establish a ready child on the named provider. Capability and semantic checks run before delegation. Provider ownership lasts until its promise fulfills; a rejection therefore has no run for the caller to dispose and emits no run lifecycle events. - -- `name` — the provider to use. -- `request` — child prompt, parent, signal, and optional capabilities. - -**Returns** the ready holder-owned run. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/subagent/subagent/src/index.ts#L211) diff --git a/website/zh-CN/api/harness/system-prompt.md b/website/zh-CN/api/harness/system-prompt.md deleted file mode 100644 index ac22619752..0000000000 --- a/website/zh-CN/api/harness/system-prompt.md +++ /dev/null @@ -1,96 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.systemPrompt - -`SystemPrompt` — provided by `@deepseek-ai/dsh-system-prompt`. - -Registry service for the prompt inputs assembled before each model step. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L209) - -### ctx.systemPrompt.section(section) - -```ts website-api -/** - * Register an ordered prompt section in the calling context's scope. A scoped - * section shadows a global section with the same name; duplicates within one - * layer and non-finite orders throw. Registration and disposal emit - * `system-prompt/change`. - * @param section - the section to register. - * @returns the exact Cordis effect disposer. - */ -section(section: PromptSection): () => void -``` - -Register an ordered prompt section in the calling context's scope. A scoped section shadows a global section with the same name; duplicates within one layer and non-finite orders throw. Registration and disposal emit `system-prompt/change`. - -- `section` — the section to register. - -**Returns** the exact Cordis effect disposer. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L250) - -### ctx.systemPrompt.tools(provider) - -```ts website-api -/** - * Register a tool-schema provider in the calling context's scope. Global and - * matching scoped providers both contribute; returning the reserved - * {@link TOOL_ORDER_REST} name makes assembly fail. - * @param provider - evaluated for each assembly with its context. - * @returns the exact Cordis effect disposer. - */ -tools(provider: (context: AssembleContext) => ToolProviderResult): () => void -``` - -Register a tool-schema provider in the calling context's scope. Global and matching scoped providers both contribute; returning the reserved TOOL_ORDER_REST name makes assembly fail. - -- `provider` — evaluated for each assembly with its context. - -**Returns** the exact Cordis effect disposer. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L291) - -### ctx.systemPrompt.variable(name, provider) - -```ts website-api -/** - * Register a prompt variable in the calling context's scope. Scoped values - * shadow globals; invalid or duplicate names throw. A provider may return - * `undefined`, but rendering a section that references that value then fails. - * @param name - the `[a-z][a-z0-9_]*` reference name. - * @param provider - evaluated for each assembly. - * @returns the exact Cordis effect disposer. - */ -variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void -``` - -Register a prompt variable in the calling context's scope. Scoped values shadow globals; invalid or duplicate names throw. A provider may return `undefined`, but rendering a section that references that value then fails. - -- `name` — the `[a-z][a-z0-9_]*` reference name. -- `provider` — evaluated for each assembly. - -**Returns** the exact Cordis effect disposer. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L325) - -### ctx.systemPrompt.assemble(context?) - -```ts website-api -/** - * Assemble global and scoped providers, detach tool parameters, apply - * canonical ordering, then run the assembly waterfall. Scoped sections and - * variables shadow globals; the returned waterfall value is authoritative. - * @param context - the optional scope and plugin-defined assembly fields. - * @returns the authoritative post-waterfall assembly. - */ -async assemble(context: AssembleContext = {}): Promise<PromptAssembly> -``` - -Assemble global and scoped providers, detach tool parameters, apply canonical ordering, then run the assembly waterfall. Scoped sections and variables shadow globals; the returned waterfall value is authoritative. - -- `context` — the optional scope and plugin-defined assembly fields. - -**Returns** the authoritative post-waterfall assembly. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/system-prompt/src/index.ts#L365) diff --git a/website/zh-CN/api/harness/tasks.md b/website/zh-CN/api/harness/tasks.md deleted file mode 100644 index 1a43639871..0000000000 --- a/website/zh-CN/api/harness/tasks.md +++ /dev/null @@ -1,191 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.tasks - -`TaskService` — provided by `@deepseek-ai/dsh-tasks`. - -The `tasks` service: the runtime-global background task registry. See the module doc for the ownership, isolation, and lifecycle contracts. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L76) - -### ctx.tasks.start(spec) - -```ts website-api -/** - * Preflight access, validation, and owner cleanup before starting and - * atomically registering work. A throwing starter leaves nothing registered; - * after it returns, registration cannot fail. Settlement records the outcome, - * notifies listeners, and releases waiters. - * @param spec - task identity, owner, and synchronous starter. - * @returns the registry-issued `<kind>-N` id. - */ -start(spec: TaskStart): TaskId -``` - -Preflight access, validation, and owner cleanup before starting and atomically registering work. A throwing starter leaves nothing registered; after it returns, registration cannot fail. Settlement records the outcome, notifies listeners, and releases waiters. - -- `spec` — task identity, owner, and synchronous starter. - -**Returns** the registry-issued `<kind>-N` id. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L101) - -### ctx.tasks.list(caller?) - -```ts website-api -/** - * List caller-owned and unowned tasks in registration order without exposing - * another session's labels. - * @param caller - reading agent; a non-agent caller sees only unowned tasks. - * @returns fresh snapshots. - */ -list(caller?: Agent): TaskSnapshot[] -``` - -List caller-owned and unowned tasks in registration order without exposing another session's labels. - -- `caller` — reading agent; a non-agent caller sees only unowned tasks. - -**Returns** fresh snapshots. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L153) - -### ctx.tasks.get(id, caller?) - -```ts website-api -/** - * Return a non-consuming snapshot without changing its read cursor or notice - * state. Throws for an unknown or foreign task. - * @param id - task to look up. - * @param caller - reading agent checked against the owner. - * @returns a fresh snapshot. - */ -get(id: TaskId, caller?: Agent): TaskSnapshot -``` - -Return a non-consuming snapshot without changing its read cursor or notice state. Throws for an unknown or foreign task. - -- `id` — task to look up. -- `caller` — reading agent checked against the owner. - -**Returns** a fresh snapshot. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L167) - -### ctx.tasks.read(id, caller?) - -```ts website-api -/** - * Read the next stream delta, or the idempotent final output after settlement. - * A terminal read marks the task reported. Throws for an unknown or foreign - * task. - * @param id - task to read. - * @param caller - reading agent checked against the owner. - * @returns output text and the post-read snapshot. - */ -read(id: TaskId, caller?: Agent): TaskRead -``` - -Read the next stream delta, or the idempotent final output after settlement. A terminal read marks the task reported. Throws for an unknown or foreign task. - -- `id` — task to read. -- `caller` — reading agent checked against the owner. - -**Returns** output text and the post-read snapshot. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L181) - -### ctx.tasks.kill(id, caller?, reason?) - -```ts website-api -/** - * Request cancellation, then mark the task stopping and reported. A producer - * throw propagates without changing task state. Throws for an unknown or - * foreign task. - * @param id - task to cancel. - * @param caller - killing agent checked against the owner. - * @param reason - logged reason forwarded to the producer. - * @returns `requested` for live work, otherwise `already-finished`. - */ -kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-finished' -``` - -Request cancellation, then mark the task stopping and reported. A producer throw propagates without changing task state. Throws for an unknown or foreign task. - -- `id` — task to cancel. -- `caller` — killing agent checked against the owner. -- `reason` — logged reason forwarded to the producer. - -**Returns** `requested` for live work, otherwise `already-finished`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L200) - -### ctx.tasks.wait(id, timeoutMs, caller?, signal?) - -```ts website-api -/** - * Wait for settlement or timeout without cancelling the task. Caller abort - * rejects only while the task is live; after settlement it returns the - * terminal snapshot so a notice suppressed for this waiter is still delivered. - * Timed-out and aborted waits detach their resolvers. Throws for invalid, - * unknown, or foreign input. - * @param id - task to wait for. - * @param timeoutMs - positive finite wait bound in milliseconds. - * @param caller - waiting agent checked against the owner. - * @param signal - optional cancellation of the wait itself. - * @returns snapshot at settlement or timeout. - */ -async wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise<TaskSnapshot> -``` - -Wait for settlement or timeout without cancelling the task. Caller abort rejects only while the task is live; after settlement it returns the terminal snapshot so a notice suppressed for this waiter is still delivered. Timed-out and aborted waits detach their resolvers. Throws for invalid, unknown, or foreign input. - -- `id` — task to wait for. -- `timeoutMs` — positive finite wait bound in milliseconds. -- `caller` — waiting agent checked against the owner. -- `signal` — optional cancellation of the wait itself. - -**Returns** snapshot at settlement or timeout. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L226) - -### ctx.tasks.onTaskDone(listener) - -```ts website-api -/** - * Register an effect-scoped completion listener. Each listener is contained; - * returned promises are observed but not awaited. No listener runs after - * service disposal. - * @param listener - receives each terminal snapshot and its exact owner. - * @returns disposer that unregisters the listener. - */ -onTaskDone(listener: TaskDoneListener): () => void -``` - -Register an effect-scoped completion listener. Each listener is contained; returned promises are observed but not awaited. No listener runs after service disposal. - -- `listener` — receives each terminal snapshot and its exact owner. - -**Returns** disposer that unregisters the listener. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L283) - -### ctx.tasks.attachSurface(name) - -```ts website-api -/** - * Attach an effect-scoped surface that can read and stop tasks. {@link start} - * refuses work while none is attached. - * @param name - diagnostic label; duplicate names remain independent. - * @returns disposer that detaches this surface. - */ -attachSurface(name: string): () => void -``` - -Attach an effect-scoped surface that can read and stop tasks. start refuses work while none is attached. - -- `name` — diagnostic label; duplicate names remain independent. - -**Returns** disposer that detaches this surface. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/tasks/tasks/src/index.ts#L297) diff --git a/website/zh-CN/api/harness/token-meter.md b/website/zh-CN/api/harness/token-meter.md deleted file mode 100644 index 30b83f79d4..0000000000 --- a/website/zh-CN/api/harness/token-meter.md +++ /dev/null @@ -1,72 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.tokenMeter - -`TokenMeterService` — provided by `@deepseek-ai/dsh-token-meter`. - -Replay owner for one service-wide estimator and isolated per-session folds. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/token-meter/src/index.ts#L106) - -### ctx.tokenMeter.contextWindow - -```ts website-api -/** Provider context-window capacity used by pressure consumers. */ -readonly contextWindow: number -``` - -Provider context-window capacity used by pressure consumers. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/token-meter/src/index.ts#L112) - -### ctx.tokenMeter.measure(session, requestHeader?) - -```ts website-api -/** - * Measure current request pressure and surface through the durable tail. - * - * Provider usage is reused only when the latest successful call's canonical - * request envelope matches `requestHeader` and its total is no lower than - * that call's full heuristic anchor; otherwise the complete envelope and - * surface are heuristically repriced. - * - * `requestHeader` affects request pressure only; surface fields always - * describe the current session surface. Every call clones those positional - * nodes, so measurement is O(surface). - * - * @param session - session to replay through its current durable tail. - * @param requestHeader - optional effective request envelope replacing the latest logged header. - * @returns a detached deeply immutable pressure and surface measurement. - */ -measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement -``` - -Measure current request pressure and surface through the durable tail. -Provider usage is reused only when the latest successful call's canonical request envelope matches `requestHeader` and its total is no lower than that call's full heuristic anchor; otherwise the complete envelope and surface are heuristically repriced. -`requestHeader` affects request pressure only; surface fields always describe the current session surface. Every call clones those positional nodes, so measurement is O(surface). - -- `session` — session to replay through its current durable tail. -- `requestHeader` — optional effective request envelope replacing the latest logged header. - -**Returns** a detached deeply immutable pressure and surface measurement. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/token-meter/src/index.ts#L143) - -### ctx.tokenMeter.estimateMessage(message) - -```ts website-api -/** - * Heuristically price one model-visible message. - * @param message - message to price without mutation. - * @returns content and role-framing tokens under the fixed service heuristic. - */ -estimateMessage(message: Message): number -``` - -Heuristically price one model-visible message. - -- `message` — message to price without mutation. - -**Returns** content and role-framing tokens under the fixed service heuristic. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/llm/token-meter/src/index.ts#L181) diff --git a/website/zh-CN/api/harness/tools.md b/website/zh-CN/api/harness/tools.md deleted file mode 100644 index eb416e8b75..0000000000 --- a/website/zh-CN/api/harness/tools.md +++ /dev/null @@ -1,162 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.tools - -`ToolRegistry` — provided by `@deepseek-ai/dsh-tools`. - -Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L438) - -### ctx.tools.register(definition) - -```ts website-api -/** - * Register globally or in the calling agent scope. Scoped tools shadow - * globals; duplicates within one layer and the reserved `run_code` name fail. - * @param definition - the tool schema, execution, and optional presentation functions. - * @returns the exact disposer that unregisters the tool. - */ -register(definition: ToolDefinition): () => void -``` - -Register globally or in the calling agent scope. Scoped tools shadow globals; duplicates within one layer and the reserved `run_code` name fail. - -- `definition` — the tool schema, execution, and optional presentation functions. - -**Returns** the exact disposer that unregisters the tool. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L538) - -### ctx.tools.restrict(filter) - -```ts website-api -/** - * Restrict global tools for the calling agent scope. Empty filters, unknown - * names, scope-local names, and reserved transport names fail. Restrictions - * intersect; scoped registrations remain visible. - * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove). - * @returns the exact disposer that lifts this restriction. - */ -restrict(filter: ToolRestriction): () => void -``` - -Restrict global tools for the calling agent scope. Empty filters, unknown names, scope-local names, and reserved transport names fail. Restrictions intersect; scoped registrations remain visible. - -- `filter` — global-surface mask: `allow` (keep only) and/or `deny` (remove). - -**Returns** the exact disposer that lifts this restriction. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L578) - -### ctx.tools.guard(guard) - -```ts website-api -/** - * Register a monotonic guard after the extensible `tools/pre-execute` - * waterfall. A plain-context guard applies globally; one registered through - * `agent.ctx` applies only to that agent. Any matching guard may deny by - * returning a reason, while no guard can force-allow a call another guard - * denied. The exact effect disposer is returned for ordered ownership and - * HMR cleanup. - * @param guard - synchronous check; a returned string denies the execution. - * @returns the exact disposer that unregisters the guard. - */ -guard(guard: ToolGuard): () => void -``` - -Register a monotonic guard after the extensible `tools/pre-execute` waterfall. A plain-context guard applies globally; one registered through `agent.ctx` applies only to that agent. Any matching guard may deny by returning a reason, while no guard can force-allow a call another guard denied. The exact effect disposer is returned for ordered ownership and HMR cleanup. - -- `guard` — synchronous check; a returned string denies the execution. - -**Returns** the exact disposer that unregisters the guard. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L629) - -### ctx.tools.get(name, scope?) - -```ts website-api -/** - * Look up a tool as one scope sees it (scoped - * shadows global; a restricted-away global reads as absent). Presenters pass - * the calling agent so the rendered card matches the definition that - * actually executed. - * @param name - the tool name as registered. - * @param scope - the viewing scope (the agent); omitted = the global view. - * @returns the definition the scope resolves, or undefined when none is visible. - */ -get(name: string, scope?: ScopeKey): ToolDefinition | undefined -``` - -Look up a tool as one scope sees it (scoped shadows global; a restricted-away global reads as absent). Presenters pass the calling agent so the rendered card matches the definition that actually executed. - -- `name` — the tool name as registered. -- `scope` — the viewing scope (the agent); omitted = the global view. - -**Returns** the definition the scope resolves, or undefined when none is visible. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L731) - -### ctx.tools.schemas(scope?) - -```ts website-api -/** - * Project visible definitions onto the allowlisted model-facing schema fields, - * excluding execution and presentation callbacks. - * @param scope - the viewing scope (the agent); omitted = the global view. - * @returns one deep-cloned schema per visible tool. - */ -schemas(scope?: ScopeKey): ToolSchema[] -``` - -Project visible definitions onto the allowlisted model-facing schema fields, excluding execution and presentation callbacks. - -- `scope` — the viewing scope (the agent); omitted = the global view. - -**Returns** one deep-cloned schema per visible tool. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L741) - -### ctx.tools.executionMode(exec) - -```ts website-api -/** - * Classify a pending call through the caller's visible tool definition. Only - * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or - * throwing classifiers are exclusive. - * @param exec - call name, parsed arguments, and optional agent scope. - * @returns the fail-closed scheduling mode. - */ -executionMode(exec: ToolExecutionInput): ToolExecutionMode -``` - -Classify a pending call through the caller's visible tool definition. Only an exact `true` is parallel; unknown, hidden, undeclared, invalid, or throwing classifiers are exclusive. - -- `exec` — call name, parsed arguments, and optional agent scope. - -**Returns** the fail-closed scheduling mode. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L762) - -### ctx.tools.execute(exec) - -```ts website-api -/** - * Execute through pre-policy, guards, around-dispatch, post-policy, and final - * notification. Tool and listener failures resolve as materialized error - * results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is - * the same lossless, frozen snapshot final observers receive. - * @param exec - the typed same-process call input. The registry assigns its - * correlation token before policy begins. - * @returns the materialized final result. - */ -async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult> -``` - -Execute through pre-policy, guards, around-dispatch, post-policy, and final notification. Tool and listener failures resolve as materialized error results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is the same lossless, frozen snapshot final observers receive. - -- `exec` — the typed same-process call input. The registry assigns its correlation token before policy begins. - -**Returns** the materialized final result. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/tools/src/index.ts#L782) diff --git a/website/zh-CN/api/harness/user-interaction.md b/website/zh-CN/api/harness/user-interaction.md deleted file mode 100644 index 072087f8ca..0000000000 --- a/website/zh-CN/api/harness/user-interaction.md +++ /dev/null @@ -1,49 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.userInteraction - -`UserInteractionService` — provided by `@deepseek-ai/dsh-user-interaction`. - -`ctx.userInteraction`: one active UI provider plus an `ask()` surface. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-interaction/src/index.ts#L82) - -### ctx.userInteraction.registerProvider(provider) - -```ts website-api -/** - * Register the UI provider. Only one provider may be active in a context. - * - * @param provider UI-side implementation that collects answers. - * @returns Disposer that unregisters this provider. - */ -registerProvider(provider: UserInteractionProvider): () => void -``` - -Register the UI provider. Only one provider may be active in a context. - -- `provider` — UI-side implementation that collects answers. - -**Returns** Disposer that unregisters this provider. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-interaction/src/index.ts#L95) - -### ctx.userInteraction.ask(request) - -```ts website-api -/** - * Ask the active UI provider and wait for the user's answer. - * - * @param request Questions, owner agent, and abort signal. - * @returns The answer chosen or typed by the human. - */ -async ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer> -``` - -Ask the active UI provider and wait for the user's answer. - -- `request` — Questions, owner agent, and abort signal. - -**Returns** The answer chosen or typed by the human. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/ui/user-interaction/src/index.ts#L114) diff --git a/website/zh-CN/api/harness/web.md b/website/zh-CN/api/harness/web.md deleted file mode 100644 index 5827430261..0000000000 --- a/website/zh-CN/api/harness/web.md +++ /dev/null @@ -1,105 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.web - -`WebService` — provided by `@deepseek-ai/dsh-web`. - -The web access service. Registered as `ctx.web` (one instance per context). -Selection semantics (resolved at execution time, never order-dependent): -- A configured id that is registered and `available()` → that provider. -- A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`. -- A configured id registered but unavailable → `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. -- No id configured, exactly one registered usable provider → that provider. -- No id configured, multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`. -- No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L74) - -### ctx.web.registerSearchProvider(provider) - -```ts website-api -/** - * Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER` - * if its id is already registered for search. Returns a disposer; disposed - * with the calling fiber. - * @param provider - the provider; its `id` is the registry key. - * @returns the disposer that unregisters the provider. - */ -registerSearchProvider(provider: WebSearchProvider): () => void -``` - -Register a search provider. Throws WebError `WEB_DUPLICATE_PROVIDER` if its id is already registered for search. Returns a disposer; disposed with the calling fiber. - -- `provider` — the provider; its `id` is the registry key. - -**Returns** the disposer that unregisters the provider. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L103) - -### ctx.web.registerFetchProvider(provider) - -```ts website-api -/** - * Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER` - * if its id is already registered for fetch. Returns a disposer; disposed - * with the calling fiber. - * @param provider - the provider; its `id` is the registry key. - * @returns the disposer that unregisters the provider. - */ -registerFetchProvider(provider: WebFetchProvider): () => void -``` - -Register a fetch provider. Throws WebError `WEB_DUPLICATE_PROVIDER` if its id is already registered for fetch. Returns a disposer; disposed with the calling fiber. - -- `provider` — the provider; its `id` is the registry key. - -**Returns** the disposer that unregisters the provider. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L114) - -### ctx.web.search(request, signal?) - -```ts website-api -/** - * Run one search through the selected provider. Resolves the provider at call - * time with the selection rules above; throws {@link WebError} when the - * capability cannot run. The seam enforces `request.maxResults` on the result: - * if the provider over-returns, `sources[]` is truncated and `truncated` set. - * @param request - the query plus result-shaping options. - * @param signal - optional cancellation signal forwarded to the provider. - * @returns the provider's results, capped to `request.maxResults`. - */ -async search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult> -``` - -Run one search through the selected provider. Resolves the provider at call time with the selection rules above; throws WebError when the capability cannot run. The seam enforces `request.maxResults` on the result: if the provider over-returns, `sources[]` is truncated and `truncated` set. - -- `request` — the query plus result-shaping options. -- `signal` — optional cancellation signal forwarded to the provider. - -**Returns** the provider's results, capped to `request.maxResults`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L140) - -### ctx.web.fetch(request, signal?) - -```ts website-api -/** - * Retrieve one URL through the selected provider. Resolves the provider at - * call time with the selection rules above; throws {@link WebError} when the - * capability cannot run. A non-2xx response is a result, not a throw. - * @param request - the URL plus retrieval options. - * @param signal - optional cancellation signal forwarded to the provider. - * @returns the retrieval outcome; non-2xx responses resolve descriptively. - */ -async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult> -``` - -Retrieve one URL through the selected provider. Resolves the provider at call time with the selection rules above; throws WebError when the capability cannot run. A non-2xx response is a result, not a throw. - -- `request` — the URL plus retrieval options. -- `signal` — optional cancellation signal forwarded to the provider. - -**Returns** the retrieval outcome; non-2xx responses resolve descriptively. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/web/web/src/index.ts#L157) diff --git a/website/zh-CN/api/harness/workflows.md b/website/zh-CN/api/harness/workflows.md deleted file mode 100644 index f34c3d8a6c..0000000000 --- a/website/zh-CN/api/harness/workflows.md +++ /dev/null @@ -1,29 +0,0 @@ -<!-- Generated by scripts/gen-website-api.ts — do not edit by hand. Run `pnpm run gen-website-api` to regenerate. --> - -# ctx.workflows - -`WorkflowService` (abstract seam) — provided by `@deepseek-ai/dsh-workflow`. - -Workflow execution seam. Invalid requests throw before publication; a live run is holder-owned, its result never rejects, cancellation and disposal are bounded, and disposal waits for child cleanup within that bound. Lifecycle listener failures are contained, and `workflow/end` fires exactly once as the result settles. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L159) - -### ctx.workflows.start(request) - -```ts website-api -/** - * Parse and execute a workflow script. - * @param request - the script, its `args`, the parent agent, and an - * optional cancel signal. - * @returns the live run; its `result` resolves when the script settles. - */ -abstract start(request: WorkflowStartRequest): WorkflowRun -``` - -Parse and execute a workflow script. - -- `request` — the script, its `args`, the parent agent, and an optional cancel signal. - -**Returns** the live run; its `result` resolves when the script settles. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/workflow/workflow/src/index.ts#L170) diff --git a/website/zh-CN/api/index.md b/website/zh-CN/api/index.md deleted file mode 100644 index 4357fcbfe3..0000000000 --- a/website/zh-CN/api/index.md +++ /dev/null @@ -1,43 +0,0 @@ -# API 参考 - -本节是 DeepSeek Harness 的 API 参考。除本页外,`cordis/` 与 `harness/` 下的所有页面**由脚本从源码生成**(`pnpm run gen-website-api`,CI 校验新鲜度);签名代码块保留源码的原始 JSDoc,签名与说明永远与代码一致。生成页目前为英文,中文版将随统一翻译流程提供。 - -## 框架 API - -Cordis 微内核提供的基础能力,所有插件开发都建立在这些 API 之上: - -- [Context](./cordis/context) — 上下文对象,所有服务和方法的入口 -- [Events](./cordis/events) — 事件系统 API(on / emit / bail / serial / waterfall) -- [Fiber](./cordis/fiber) — 插件生命周期(状态机、effect、dispose) -- [Registry](./cordis/registry) — 插件注册(plugin / inject) -- [Service](./cordis/service) — 服务基类 - -## Harness API - -每个 `ctx.*` 服务一页,按服务名索引: - -- [ctx.agentLoop](./harness/agent-loop) — ReAct 循环的创建与恢复 -- [ctx.agents](./harness/agents) — Agent 注册表与工厂 -- [ctx.approval](./harness/approval) — 用户审批 -- [ctx.bash](./harness/bash) — Bash 执行接口(抽象缝) -- [ctx.codeRuntime](./harness/code-runtime) — 代码执行接口(抽象缝) -- [ctx.compact](./harness/compact) — 上下文压缩接口(抽象缝) -- [ctx.fs](./harness/fs) — 文件系统接口(抽象缝) -- [ctx.llm](./harness/llm) — LLM 服务与适配器注册 -- [ctx.permission](./harness/permission) — 权限策略 -- [ctx.sandbox](./harness/sandbox) — 沙箱执行接口(抽象缝) -- [ctx.sessionPersistence](./harness/session-persistence) — 会话持久化接口(抽象缝) -- [ctx.sessionQuery](./harness/session-query) — 会话检索 -- [ctx.sessions](./harness/sessions) — 会话存储 -- [ctx.skills](./harness/skills) — 技能加载 -- [ctx.subagents](./harness/subagents) — 子代理委派 -- [ctx.systemPrompt](./harness/system-prompt) — 系统提示词组装 -- [ctx.tasks](./harness/tasks) — 后台任务 -- [ctx.tools](./harness/tools) — Tool 注册表 -- [ctx.userInteraction](./harness/user-interaction) — 用户交互接口 -- [ctx.web](./harness/web) — Web 搜索与抓取 -- [ctx.workflows](./harness/workflows) — 动态工作流引擎(抽象缝) - -事件总表:[Harness events](./harness/events) — 全部事件按作用域分组,含触发模式与载荷签名。 - -想学"怎么写一个 tool / 插件"?教程在[开发指南](../develop/basic/);本节只做精确的接口参考。 diff --git a/website/zh-CN/design/composability.md b/website/zh-CN/design/composability.md deleted file mode 100644 index a52a7bb8c5..0000000000 --- a/website/zh-CN/design/composability.md +++ /dev/null @@ -1,77 +0,0 @@ -# 可组合性与插件系统 - -## 组合 - -编程的本质就是组合。将小的构建块拼装为更大的系统,再将大系统作为块继续拼装——这是从函数到模块到微服务一脉相承的思想。 - -组合可以分为两种: - -- **静态组合**:编译期确定的组合,例如函数调用、模块导入。 -- **动态组合**:运行时确定的组合,例如热更新、插件加载/卸载。 - -静态组合是逻辑的组合;动态组合为可组合性引入了时间和空间两个新维度。 - -## 三种可组合性 - -| 维度 | 定义 | 对应问题 | -|------|------|----------| -| **逻辑可组合性** (Logical) | 功能能否被任意拆分和组装 | 接口设计是否正交 | -| **时间可组合性** (Temporal) | 能否灵活、安全地控制组合的运行时序 | 能否热加载/卸载而不泄漏 | -| **空间可组合性** (Spatial) | 能否灵活、安全地管理组合的依赖关系 | 依赖缺失时行为是否确定 | - -一门编程语言或应用框架越多地使用组合范式,就称它的可组合性越好。 - -## 传统插件系统的问题 - -插件系统是动态组合的典型形式。浏览器扩展、IDE 插件、操作系统驱动,都是其实例。然而大多数插件系统并不可靠。 - -### 不可逆的插件化 - -以 VSCode 为例: - -- 卸载或更新插件时需要重启整个系统。 -- 无法在运行时追踪和回收副作用,导致内存泄漏和非预期的资源占用。 -- 即便提供了 `deactivate` 钩子,也无法强制开发者正确实现清理逻辑。 - -**根本原因**:未做到时间可组合——系统不知道某个插件产生了哪些副作用、占用了哪些资源。 - -### 不完全的插件化 - -- 无法表达插件间的依赖关系,扩展能力受限。 -- 只有外围功能被下放给插件,核心功能依然通过修改主体代码来实现。 - -**根本原因**:未做到空间可组合——系统缺乏对依赖关系的建模和管理。 - -## Cordis 的解法 - -Cordis 同时解决了上述两个问题: - -1. **可逆作用** (Revertible Effects) 实现时间可组合性——所有注册自动追踪、自动回收。 -2. **响应式余作用** (Reactive Coeffects) 实现空间可组合性——依赖声明驱动加载顺序。 - -两者通过**上下文模型** (Context Model) 统一为单一的编程范式:开发者只需通过 `ctx` 调用框架 API,可逆性和依赖管理由框架保证。 - -## 在 Harness 中的体现 - -DeepSeek Harness 将 Cordis 的可组合性应用到 Agent 开发领域: - -```ts -import type { Context } from 'cordis' -import { defineTool } from '@deepseek-ai/dsh-tools' -import type {} from '@deepseek-ai/dsh-llm' - -// 一个 Harness 插件天然是可逆的 -export const inject = ['tools', 'llm'] // 空间可组合:声明依赖 - -export function apply(ctx: Context) { - // 时间可组合:注册会被自动追踪和回收 - ctx.tools.register(defineTool({ - name: 'my-tool', - description: '...', - parameters: { /* ... */ }, - async execute(args) { return [] }, - })) -} -``` - -插件卸载时,tool 自动注销、事件监听自动移除——无需手动清理。依赖的服务(如 `llm`)消失时,插件自动挂起;恢复时自动重新加载。 diff --git a/website/zh-CN/design/context-model.md b/website/zh-CN/design/context-model.md deleted file mode 100644 index 6323db27df..0000000000 --- a/website/zh-CN/design/context-model.md +++ /dev/null @@ -1,152 +0,0 @@ -# 上下文模型 - -上下文 (Context) 是 Cordis 将作用与余作用统一的运行时模型。它提供了一种编程范式,允许开发者无心智负担地编写时间、空间可组合的程序。 - -## 作用上下文 (Effect Context) - -当副作用被记录到全局环境时,$\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right)$ 也就变成了一个更大的 $\mathcal{C}$。 - -递归地定义: - -$$ -\begin{matrix} -\mathcal{C}_1=\mathcal{C}_0\times\left(\mathcal{C}_0\to\mathcal{C}_0\right)\\ -\mathcal{C}_2=\mathcal{C}_1\times\left(\mathcal{C}_1\to\mathcal{C}_1\right)\\ -\cdots\\ -\mathcal{C}_{n+1}=\mathcal{C}_n\times\left(\mathcal{C}_n\to\mathcal{C}_n\right)\\ -\end{matrix} -$$ - -每一层 $\mathcal{C}$ 包含上一层的状态,同时记录了上一层的副作用。 - -利用递归类型得到真正的作用上下文: - -$$ -\mathcal{C}=\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right) -$$ - -这就是 Cordis Context 的理论根基:**上下文既是状态容器,又是副作用追踪器。** - -## 上下文的派生 - -当一个插件被加载时,从当前上下文派生出新的上下文实例: - -``` -Root Context -├── Plugin A Context ← 管理 A 的副作用 -│ └── Sub-plugin Context -└── Plugin B Context ← 管理 B 的副作用 -``` - -- 子级上下文管理插件内部的全部副作用 -- 插件整体作为一个副作用被父级上下文收集 -- 父级 dispose 时,子级先被 dispose(保证依赖逆序) - -## 余作用上下文 (Coeffect Context) - -余作用由作用产生: - -- **提供服务**本身是一种作用——它占用了服务命名空间资源 -- 因此服务的提供被记录在作用上下文中 -- 上下文将作用与余作用关联起来,提供了统一的时间、空间可组合性 - -```ts -import { Service, type Context } from 'cordis' - -// 提供服务 = 一个 effect(占用 ctx.llm 这个 "资源") -class LlmService extends Service { - constructor(ctx: Context) { - super(ctx, 'llm') - } - // 当此插件卸载时,ctx.llm 被回收(effect 的逆操作) - // 所有依赖 llm 的插件因 coeffect 不满足而挂起 -} -``` - -## 基于上下文的开发范式 - -上下文模型提供了两个关键优势: - -### 无感性 (Transparent) - -框架将领域中的所有方法都封装为 effect 版本。开发者只需调用 `ctx` 上的方法,就能自动获得时间/空间可组合性: - -```ts -import type { Context } from 'cordis' -import type { ToolDefinition } from '@deepseek-ai/dsh-tools' -import type { LlmAdapter, Message } from '@deepseek-ai/dsh-llm' -import type { Agent } from '@deepseek-ai/dsh-agent' - -declare function validateResult(agent: Agent, turn: number, step: number, message: Message, next: () => Promise<Message>): Promise<Message> -declare const myTool: ToolDefinition -declare const adapter: LlmAdapter - -export function apply(ctx: Context) { - // 以下每一行都是 effect——卸载时自动逆序回收 - ctx.on('agent/step-result', validateResult) - ctx.tools.register(myTool) - ctx.llm.registerAdapter(['my-model'], adapter) - - // 开发者无需知道"可逆作用"的存在 - // 只需通过 ctx 调用,框架保证一切安全 -} -``` - -### 渐进性 (Incremental) - -可以逐步将现有框架中的 API 替换为可组合版本,无需一次性重写: - -```ts -import type { Context } from 'cordis' - -declare const ctx: Context -declare function handler(): void -declare const legacySystem: { - register(handler: () => void): object - unregister(token: object): void -} - -// 第一步:用 ctx.effect 包装遗留 API -ctx.effect(() => { - const legacy = legacySystem.register(handler) - return () => legacySystem.unregister(legacy) -}) - -// 第二步:在未来将遗留 API 原生改造为 effect -// 两种方式可以并存 -``` - -## 在 Harness 中的完整图景 - -DeepSeek Harness 的运行时是一个 Context 树: - -``` -Root Context (Cordis 应用) -├── dsh-session (提供 ctx.sessions) -├── dsh-tools (提供 ctx.tools) -├── dsh-llm (提供 ctx.llm) -│ └── deepseek-adapter (注册模型适配器) -├── dsh-agent-loop (提供 ctx.agentLoop) -├── dsh-bash (提供 ctx.bash) -│ └── bash-local (本地执行器实现) -├── dsh-fs (提供 ctx.fs) -│ └── fs-local (本地 FS 实现) -├── dsh-system-prompt (提供 ctx.systemPrompt) -└── Agent Context (由 agents.create() 派生) - ├── Agent 自己注册的 tools - ├── Agent 的 session - └── Subagent Context (进一步派生) -``` - -每个节点都是一个 Context 实例。插件加载/卸载、服务出现/消失、Agent 创建/销毁——这一切都在 Context 树上以统一的语义发生。 - -## 总结 - -| 概念 | 解决的问题 | Cordis 机制 | -|------|-----------|-------------| -| 作用上下文 | 副作用追踪与回收 | `ctx.effect()` / `fiber.dispose()` | -| 上下文派生 | 副作用的层级隔离 | `ctx.plugin()` 创建子 Context | -| 余作用上下文 | 依赖的动态管理 | `inject` 声明 + 服务生命周期 | -| 统一范式 | 开发者无需关心底层机制 | 只需通过 `ctx` 调用 API | - -这就是为什么 Harness 能在保持「一切皆插件」的同时,不给插件开发者增加心智负担——**上下文模型把复杂性封装在了框架内部**。 diff --git a/website/zh-CN/design/effects-coeffects.md b/website/zh-CN/design/effects-coeffects.md deleted file mode 100644 index 01c181315f..0000000000 --- a/website/zh-CN/design/effects-coeffects.md +++ /dev/null @@ -1,69 +0,0 @@ -# 作用与余作用 - -## 作用 (Effects) - -Effects 是程序中对系统状态或外部环境产生影响的操作:I/O、状态修改、资源占用等。 - -学术界对作用有两种主要建模方式: - -### 单子作用 (Monadic Effects) - -- 通过单子 (monad) 将副作用封装为类型安全的计算链。 -- 提供 `return`(纯值注入)和 `bind`(链式组合)两个基本操作。 -- 以纯函数式的方式处理带有副作用的计算。(Moggi 1991, Wadler 1992) -- 代表语言:Haskell (IO Monad)、Rust (Result/Option) - -### 代数作用 (Algebraic Effects) - -- 允许在函数中"抛出"一个 effect,在调用栈的更高层次"捕获"并处理。 -- 类似异常处理,但更通用——处理后可以恢复执行。 -- 代表语言:Koka、Eff、OCaml 5+ (Kiselyov 2018, Kawahara 2020) - -## 余作用 (Coeffects) - -Coeffects 是程序执行时依赖的上下文信息:环境变量、系统资源、外部服务等。 - -- Coeffects 是 effects 的对偶 (dual) 概念,通常通过余单子 (comonad) 建模。(Petricek 2013, 2014; Brünnler 2014) -- 更前沿的理论将带有资源的上下文建模为 **graded algebra**(有序半环加最大元): - - 加法 = 并行组合;0 元 = 无资源 - - 乘法 = 串行组合;1 元 = 单位资源 - - 序 = 资源约束;最大元 = 无限资源 - - (Breuvart 2015, Gaboardi 2016, Dal Lago 2022) - -## 现有理论的不足 - -这些理论主要面向**静态分析**和**短时程序**: - -1. **缺乏运行时追踪**:类型系统能标记副作用的存在,但无法在运行时追踪和回收。对长时运行程序(服务端、Agent),这意味着资源泄漏不可避免。 - -2. **缺乏动态性**:面向编译期分析,无法处理运行时的加载/卸载需求。 - -3. **崩溃而非降级**:类型不满足时直接拒绝编译或运行时崩溃,而长时运行程序更希望安全降级——挂起不满足依赖的部分,而非停止整个系统。 - -## Cordis 的突破 - -Cordis 选择了不同的路径——在运行时层面解决可组合性问题: - -| 现有理论 | Cordis 方案 | -|----------|-------------| -| 类型标记副作用 | 运行时追踪并自动回收副作用 | -| 编译期拒绝 | 运行时挂起/恢复 | -| 面向短时程序 | 面向长时运行程序设计 | - -这由两个互补机制实现: - -- **[可逆作用](./revertible-effects)** — 将副作用形式化为可逆的群操作 -- **[响应式余作用](./reactive-coeffects)** — 将依赖建模为具有生命周期的服务 - -## 在 Agent 开发中的意义 - -对 DeepSeek Harness 而言,作用/余作用模型直接支撑了以下能力: - -| 作用 (Effect) | 余作用 (Coeffect) | -|---------------|-------------------| -| 注册一个 tool | 依赖 tool registry 服务 | -| 注册一个 LLM adapter | 依赖 LLM 服务接口 | -| 监听 session 事件 | 依赖 session 服务存在 | -| 启动子进程 | 依赖 bash executor 实现 | - -每一个 effect 都可逆(tool 可注销、adapter 可移除);每一个 coeffect 都有生命周期(服务消失则依赖者挂起)。这就是 Agent 能被安全热替换的根本原因。 diff --git a/website/zh-CN/design/index.md b/website/zh-CN/design/index.md deleted file mode 100644 index de6ebcf7aa..0000000000 --- a/website/zh-CN/design/index.md +++ /dev/null @@ -1,39 +0,0 @@ -# 系统设计 - -DeepSeek Harness 建立在 Cordis 微内核之上,采用「一切皆插件」的架构。本节阐述这套设计背后的理论基础和设计哲学。 - -## 核心思想 - -Harness 追求三种可组合性的统一: - -| 维度 | 含义 | Cordis 对应机制 | -|------|------|----------------| -| 逻辑可组合性 | 功能能否自由拆分和拼装 | 插件系统、事件系统 | -| 时间可组合性 | 运行时能否安全地加载/卸载功能 | 可逆作用、自动清理 | -| 空间可组合性 | 依赖关系能否被安全地声明和管理 | 服务生命周期、依赖注入 | - -这三种可组合性在上下文模型中统一为单一的编程范式。 - -## 目录 - -- [可组合性与插件系统](./composability) — 组合的本质,以及传统插件系统为什么不可靠 -- [作用与余作用](./effects-coeffects) — Cordis 效果系统的理论模型 -- [可逆作用](./revertible-effects) — 时间可组合性的形式化定义与证明 -- [响应式余作用](./reactive-coeffects) — 空间可组合性的服务语义 -- [上下文模型](./context-model) — Context 如何将作用与余作用统一 - -## 设计如何映射到 Harness - -| 理论概念 | Harness 中的体现 | -|----------|-----------------| -| 可逆作用 | `ctx.tools.register()` 返回 disposer;插件卸载时工具自动注销 | -| 响应式余作用 | `inject: ['llm']` 声明依赖;LLM 适配器不可用时插件自动挂起 | -| 上下文派生 | 子 Agent 拥有独立 Context,继承父级服务但有独立生命周期 | -| Waterfall 事件 | `agent/request` 链式拦截,任一监听器可决定最终请求参数 | -| Capability seam | bash/fs/web 三层拆分:接口 → 实现 → 模型工具 | - -## 进一步阅读 - -- [插件与生命周期](/zh-CN/develop/framework/) — 实践中的 Fiber 状态机 -- [服务与依赖](/zh-CN/develop/framework/service) — 服务声明与注入 -- [能力的三层拆分](/zh-CN/develop/practice/) — Capability seam 模式 diff --git a/website/zh-CN/design/reactive-coeffects.md b/website/zh-CN/design/reactive-coeffects.md deleted file mode 100644 index 2ff6cb4199..0000000000 --- a/website/zh-CN/design/reactive-coeffects.md +++ /dev/null @@ -1,100 +0,0 @@ -# 响应式余作用 - -响应式余作用 (Reactive Coeffects) 是 Cordis 实现**空间可组合性**的核心机制。 - -- 将代码中的资源依赖抽象为服务 (service) 的概念 -- 通过运行时生命周期语义,实现自动、安全、高效的资源管理 - -## 依赖的本质是生命周期 - -传统的依赖注入(如 Angular DI、Spring IoC)解决的是"怎么拿到依赖"的问题,但忽略了一个关键问题:**依赖是有生命周期的**。 - -一个数据库连接池可能重启,一个 API 服务可能下线,一个 LLM adapter 可能被热替换。当依赖消失时,依赖者应当如何表现? - -- 崩溃?——对长时运行程序不可接受。 -- 继续运行?——可能产生不一致状态。 -- **自动挂起,等待恢复?**——Cordis 的选择。 - -## 服务与生命周期 - -Cordis 将程序中的资源依赖抽象为**服务** (service): - -- 任何插件都可以声明自己依赖的服务列表 -- 服务存在明确的生命周期(提供、撤销) -- 运行时对依赖不满足的插件**等待**,而非拒绝 -- 服务生命周期结束前,依赖该服务的插件**先一步被回收** - -```ts -import { Service, type Context } from 'cordis' - -// LLM 适配器插件:提供 llm 服务 -export class LlmService extends Service { - static inject = ['http'] // 自身依赖 http - // 当 http 不可用时,LlmService 自动挂起 - // 挂起导致 ctx.llm 不可用 - // 所有 inject: ['llm'] 的插件级联挂起 - - constructor(ctx: Context) { - super(ctx, 'llm') - } -} -``` - -## 与现有理论的对比 - -### 与 Comonad 余作用比较 - -基于 Comonad 的余作用(Petricek 2013)将上下文建模为静态结构,侧重于编译期分析。Cordis 的响应式余作用额外引入了**时序语义**: - -- 服务可在运行时出现/消失 -- 依赖关系随之动态建立/解除 -- 效果的生命周期由依赖关系决定 - -### 与 Grade Algebra 余作用比较 - -基于 Grade Algebra 的余作用(Gaboardi 2016)用有序半环描述资源的组合规则。Cordis 的服务依赖可以建模为**交换半群**: - -- 服务名构成依赖集合 -- 集合并(∪)对应并行依赖 -- 交换律:依赖 A + B ≡ 依赖 B + A(声明顺序无关) -- 结合律:依赖分组方式不影响语义 - -但 Cordis 还增加了代数不具备的运行时行为:当集合中的某个服务不可用时,整个依赖集不满足,触发挂起。 - -## 在 Cordis 中的实现 - -```ts -import type { Context } from 'cordis' -import type {} from '@deepseek-ai/dsh-tools' -import type {} from '@deepseek-ai/dsh-llm' - -// 声明依赖 -export const inject = ['tools', 'llm'] - -export function apply(ctx: Context) { - // 到这里时,ctx.tools 和 ctx.llm 一定可用 - // 如果任一服务消失,此插件自动卸载 - // 服务恢复后,自动重新执行 apply -} -``` - -服务生命周期变化时的行为: - -``` -llm service 可用 → 依赖 llm 的插件 PENDING → ACTIVE -llm service 消失 → 依赖 llm 的插件 ACTIVE → DISPOSED -llm service 恢复 → 依赖 llm 的插件重新 PENDING → ACTIVE -``` - -## 为什么 Agent 需要响应式余作用 - -在 Harness 场景下,响应式余作用直接支撑: - -| 场景 | 行为 | -|------|------| -| LLM adapter 热替换 | 依赖 `llm` 的插件自动挂起/恢复,中间不丢状态 | -| 按需加载 bash 执行器 | bash tool 只在 `bash` 服务就绪后注册 | -| 子 Agent 独立服务空间 | 通过 `ctx.isolate()` 隔离服务实例,互不干扰 | -| 可选能力降级 | 不声明 `inject`,用 `ctx.get('web')` 读取——服务不可用时返回 `undefined`,插件照常运行 | - -这意味着 Harness 插件开发者无需编写防御性的 "if service exists" 检查——框架保证:当你的 `apply` 被调用时,声明的依赖一定已就绪。 diff --git a/website/zh-CN/design/revertible-effects.md b/website/zh-CN/design/revertible-effects.md deleted file mode 100644 index 3cbcfdfc06..0000000000 --- a/website/zh-CN/design/revertible-effects.md +++ /dev/null @@ -1,141 +0,0 @@ -# 可逆作用 - -可逆作用 (Revertible Effects) 是 Cordis 实现**时间可组合性**的核心机制。 - -- 在单子作用的基础上增加可逆性约束 -- 提供面向长时运行程序的作用系统 -- 确保程序可以在插件粒度上回到任意状态 - -## 副作用的封装 - -现实中的程序需要与各种副作用打交道。假设一个不纯函数: - -$$ -f_\text{impure}: \text{X}\to\text{Y} -$$ - -我们将所有可能的副作用用类型 $\mathcal{C}$ 封装,函数变为: - -$$ -f: \mathcal{C}\times\text{X}\to\mathcal{C}\times\text{Y} -$$ - -对于长时运行程序,忽略函数本身的入参和出参,$f$ 属于函数空间 $\mathfrak{F}=\mathcal{C}\to\mathcal{C}$。 - -## 从幺半群到群 - -任何函数 $f: \mathcal{C}\to\mathcal{C}$ 都是状态空间到自身的变换。在组合 $\circ$ 下构成**幺半群**: - -1. 封闭性:$f\circ g$ 也是 $\mathcal{C}\to\mathcal{C}$ -2. 结合律:$(f\circ g)\circ h=f\circ (g\circ h)$ -3. 单位元:$\text{id}$,使得 $f\circ\text{id}=\text{id}\circ f=f$ - -如果额外要求每个 $f$ 存在逆元 $f^{-1}$(即副作用可回收),$\mathfrak{F}$ 升级为**群**。 - -## 副作用都可逆吗? - -观察计算机中的副作用模式: - -| 操作 | 占用资源 | 逆操作 | -|------|----------|--------| -| 打开文件 | 文件描述符 | 关闭文件 | -| 创建子进程 | 进程号 | 杀死进程 | -| 监听端口 | 端口 | 取消监听 | -| 添加回调函数 | 事件槽位 | 删除回调 | -| 分配内存 | 内存区块 | 回收内存 | - -**副作用就是对资源的占用。** 计算机的资源天然设计为可重复使用,因此这些副作用一定是可逆的。 - -## 追踪和回收副作用 - -Cordis 通过 $\text{effect}$ 和 $\text{restore}$ 函子追踪和回收逆函数。 - -### effect 函子 - -$$ -\begin{array}{} -\text{effect}&:& -\left(\mathcal{C}\to\mathcal{C}\right)&\to& -\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right)&\to& -\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right)\\ -\text{effect}&=&f&\mapsto&\left(c, h\right)&\mapsto&\left(f(c), h\circ f^{-1}\right) -\end{array} -$$ - -直觉:执行 $f$ 产生的副作用记入状态 $c$,同时将逆操作 $f^{-1}$ 追加到回收链 $h$ 中。 - -### 同态性证明 - -$\text{effect}$ 是从 $\mathcal{C}\to\mathcal{C}$ 到 $\mathcal{C}\times(\mathcal{C}\to\mathcal{C})\to\mathcal{C}\times(\mathcal{C}\to\mathcal{C})$ 的同态: - -$$ -\begin{aligned} -\text{effect}\ (f\circ g) \left(c, h\right) -&=\left((f\circ g)(c), h\circ (f\circ g)^{-1}\right)\\ -&=\left(f(g(c)), h\circ g^{-1}\circ f^{-1}\right)\\ -&=\left(\text{effect}\ f\right)\left(g(c), h\circ g^{-1}\right)\\ -&=\left(\text{effect}\ f\right)\circ\left(\text{effect}\ g\right) \left(c, h\right) -\end{aligned} -$$ - -这意味着:组合两个操作后再追踪 = 分别追踪后再组合。副作用追踪与执行顺序无关。 - -### restore 函子 - -$$ -\begin{array}{} -\text{restore}&:& -\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right)&\to& -\mathcal{C}\times\left(\mathcal{C}\to\mathcal{C}\right)\\ -\text{restore}&=&\left(c, h\right)&\mapsto&\left(h(c),\text{id}\right) -\end{array} -$$ - -直觉:将回收链 $h$ 应用到当前状态,一次性回收所有已追踪的副作用。 - -## 在 Cordis 中的实现 - -理论映射到 API: - -| 数学概念 | Cordis API | 说明 | -|----------|-----------|------| -| $\text{effect}(f)$ | `ctx.effect(() => { ...; return dispose })` | 注册副作用并返回清理函数 | -| $\text{restore}$ | `fiber.dispose()` | 执行 Fiber 的整个回收链 | -| $f^{-1}$ | dispose 返回值 / cleanup 函数 | 逆操作 | - -```ts -import type { Context } from 'cordis' -import type { ToolDefinition } from '@deepseek-ai/dsh-tools' - -declare module 'cordis' { - interface Events { - 'my-plugin/event'(): void - } -} - -declare function startServer(port: number): { close(): void } -declare function handler(): void -declare const myTool: ToolDefinition - -export function apply(ctx: Context) { - // effect: 创建资源,返回其逆操作 - ctx.effect(() => { - const server = startServer(8080) // f: 占用端口 - return () => server.close() // f⁻¹: 释放端口 - }) - - // 框架 API 内部已封装 effect - ctx.on('my-plugin/event', handler) // 内部: effect(addListener, removeListener) - ctx.tools.register(myTool) // 内部: effect(addTool, removeTool) -} -// 当此插件被卸载时,restore 自动按逆序执行所有 f⁻¹ -``` - -## 为什么 Agent 需要可逆作用 - -在 Harness 场景下,可逆作用直接支撑: - -- **热替换 LLM 适配器**:卸载旧适配器(回收注册)、加载新适配器,无需重启 -- **动态 tool 管理**:根据对话上下文动态添加/移除 tool,不泄漏 -- **子 Agent 生命周期**:子 Agent 完成后,其注册的所有临时 tool 和监听器自动清理 -- **优雅关闭**:进程退出时所有插件按依赖逆序 dispose,确保资源完全释放 diff --git a/website/zh-CN/develop/framework/events.md b/website/zh-CN/develop/framework/events.md deleted file mode 100644 index d043dc0cb8..0000000000 --- a/website/zh-CN/develop/framework/events.md +++ /dev/null @@ -1,228 +0,0 @@ -# 事件系统 - -事件是 Cordis 插件间通信的核心机制。Harness 大量使用事件来实现松耦合的扩展点。 - -## 基本用法 - -### 监听事件 - -```ts -import type { Context } from 'cordis' - -declare module 'cordis' { - interface Events { - 'event-name'(payload: string): void - } -} - -declare const ctx: Context - -ctx.on('event-name', (payload) => { - // 处理事件 -}) -``` - -### 触发事件 - -```ts -import type { Context } from 'cordis' - -declare module 'cordis' { - interface Events { - 'event-name'(payload: string): void - } -} - -declare const ctx: Context -declare const payload: string - -ctx.emit('event-name', payload) -``` - -## 事件模式 - -Cordis 提供多种事件触发模式,适用于不同场景: - -### emit — 广播 - -同步依次调用所有监听器,不等待、不关心返回值(监听器如果是 async,其 Promise 被忽略): - -```ts -import type { Context } from 'cordis' - -declare module 'cordis' { - interface Events { - 'my-plugin/turn-end'(agentId: string, turnIndex: number): void - } -} - -declare const ctx: Context -declare const agentId: string -declare const turnIndex: number - -// 触发 -ctx.emit('my-plugin/turn-end', agentId, turnIndex) - -// 监听 -ctx.on('my-plugin/turn-end', (agentId, turnIndex) => { - console.log(`Turn ${turnIndex} ended`) -}) -``` - -### bail — 短路 - -同步依次调用监听器,第一个返回**非 `undefined`/`null`/`false`** 值的监听器终止链并作为最终值(返回 `undefined`/`null`/`false` 则继续下一个): - -```ts -import type { Context } from 'cordis' - -declare module 'cordis' { - interface Events { - 'some-check'(input: string): string | undefined - } -} - -declare const ctx: Context -declare const input: string -declare function shouldBlock(input: string): boolean - -// 触发 -const result = ctx.bail('some-check', input) - -// 监听(返回值阻止后续监听器) -ctx.on('some-check', (input) => { - if (shouldBlock(input)) return 'blocked' - // 返回 undefined 继续传递给下一个监听器 - return undefined -}) -``` - -### serial — 顺序执行 - -按注册顺序逐个 `await` 监听器,遇到第一个 bail 值(非 `undefined`/`null`/`false`)即停止并返回它;全部返回空值则执行到底。相当于 `bail` 的异步版: - -```ts -import type { Context } from 'cordis' - -declare module 'cordis' { - interface Events { - 'setup-phase'(context: object): Promise<void> | void - } -} - -declare const ctx: Context -declare const context: object - -await ctx.serial('setup-phase', context) -``` - -### waterfall — 管道 - -监听器围绕默认实现层层包裹,形成数据管道。**必须调用 `next()` 委托给下游**,不调用即为否决: - -```ts -import type { Context } from 'cordis' -import type { Message } from '@deepseek-ai/dsh-llm' - -declare module 'cordis' { - interface Events { - 'my-plugin/messages'(messages: Message[], next: () => Promise<Message[]>): Promise<Message[]> - } -} - -declare const ctx: Context -declare const messages: Message[] -declare const extraMessage: Message - -// 触发:最后一个参数是默认实现(所有监听器都调用 next 时的最终值) -const finalMessages = await ctx.waterfall('my-plugin/messages', messages, async () => messages) - -// 监听(必须调用 next) -ctx.on('my-plugin/messages', async (messages, next) => { - // next() 委托给下游监听器(最终到达默认实现),返回值可以被加工 - const result = await next() - return [...result, extraMessage] -}) -``` - -::: warning -Waterfall 监听器**必须调用 `next()`**。不调用 `next` 等于否决整个管道,这是故意为之的设计——用于实现拦截/网关逻辑。 -::: - -## Typed Events - -Harness 使用 TypeScript 声明合并来为事件提供类型安全: - -```ts -import type {} from 'cordis' - -declare module 'cordis' { - interface Events { - 'my-plugin/ready'(payload: { id: string }): void - 'my-plugin/check'(input: string): boolean | undefined - } -} - -// 现在 ctx.on('my-plugin/ready', ...) 和 ctx.emit('my-plugin/ready', ...) -// 都有正确的类型推导 -``` - -## 命名约定 - -Harness 事件遵循 `namespace/action` 命名: - -``` -agent/pre-step — 每个 step 开始前的检查点(serial) -agent/step-result — step 的 assistant 消息组装完成(waterfall) -tools/pre-execute — tool 执行前的允许/拒绝门(waterfall) -tools/post-execute — tool 执行后的检查/改写缝(waterfall) -llm/stream — 每次流式模型调用的环绕点(waterfall) -session/event — 会话事件被记录(emit) -session/flush — 会话持久化检查点(parallel) -``` - -完整的事件列表(含每个事件的签名与派发模式)见仓库中的 `docs/cordis-catalog/events.md`。 - -## 事件也是效果 - -通过 `ctx.on()` 注册的监听器会在插件卸载时自动移除: - -```ts -import type { Context } from 'cordis' -import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' - -declare function handler(agent: Agent, status: AgentStatus): void - -export function apply(ctx: Context) { - // 这个监听器在插件 dispose 时自动清理 - ctx.on('agent/status', handler) -} -``` - -## 实战示例:日志插件 - -一个记录所有 tool 调用的简单插件: - -```ts -import type { Context } from 'cordis' -import type {} from '@deepseek-ai/dsh-tools' - -export const name = 'tool-logger' - -export function apply(ctx: Context) { - ctx.on('tools/execute', async (exec, next) => { - console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`) - const result = await next() - const text = result.content - .map(b => b.type === 'text' ? b.text : '') - .join('') - console.log(`[tool result] ${text.slice(0, 100)}`) - return result - }) -} -``` - -## 下一步 - -- [能力三件套](../practice/) — 事件在 capability seam 中的角色 -- [LLM 适配器](../practice/llm-adapter) — 实现一个完整的 LLM 后端 diff --git a/website/zh-CN/guide/config.md b/website/zh-CN/guide/config.md deleted file mode 100644 index 35fb42185b..0000000000 --- a/website/zh-CN/guide/config.md +++ /dev/null @@ -1,367 +0,0 @@ -# 配置文件 - -Harness 使用 `cordis.yml` 描述一个 Agent 加载哪些插件、以什么参数运行。 - -## 从例子开始 - -### echo-agent 的配置 - -这是一开始的第一个 Agent 的完整配置: - -```yaml -# 热替换:修改代码后自动重载,不用手动重启 -- id: hmr - name: '@cordisjs/plugin-hmr' - config: - root: ['.'] - -# Mock 模型:从本地 `.ts` 文件加载,注册一个名为 `mock-llm` 的工具 -# 本地模拟 LLM 响应,不联网 -- id: mock-llm - name: './src/mock-llm.ts' - -# Echo 工具:收到文本后转大写返回 -- id: echo-tool - name: './src/echo-tool.ts' - -# Bash 执行器:从 npm 包 `@deepseek-ai/dsh-bash-local`加载,提供 bash 命令执行能力 -- id: bash - name: '@deepseek-ai/dsh-bash-local' - -# 应用主体:把 session 管理、tool 调度、agent loop 等组装成一个可交互的终端 Agent -# 只需告诉它用哪个模型 (`model`)、什么人设 (`persona`) -- id: stdio-agent - name: '@deepseek-ai/dsh-stdio-demo' - config: - model: mock-echo - persona: 'You are echo-agent, a demo agent.' - welcome: 'echo-agent ready. Type a message ("echo <text>" triggers the tool).' - persistenceRoot: './.sessions' -``` - -### repl-agent 的配置 - -真实场景——接入 DeepSeek API,带完整工具链: - -```yaml -# 热替换:同上,开发时自动重载 -- id: hmr - name: '@cordisjs/plugin-hmr' - config: - root: ['.'] - -# LLM 后端:从 npm 包加载,具备接入 DeepSeek API 能力 -# `!!js` 从环境变量读取密钥,不会写进配置文件 -# `models` 声明该适配器能处理哪些模型名 -- id: llm-deepseek - name: '@deepseek-ai/dsh-llm-deepseek' - config: - apiKey: !!js process.env.DEEPSEEK_API_KEY - baseURL: !!js process.env.DEEPSEEK_BASE_URL - models: - - deepseek-v4-pro - - deepseek-v4-flash - -# Bash 执行器:让 Agent 能跑 shell 命令 -# timeoutMs 设置单条命令的超时时间 -- id: bash - name: '@deepseek-ai/dsh-bash-local' - config: - timeoutMs: 60000 - -# 应用主体:和 echo-agent 一样的框架,只是配置不同 -# `model` 指定默认使用哪个模型(要和上面 models 列表里的名字对应) -# `persona` 是系统提示词,{{model}} 会被替换为实际模型名 -# `resumeSessionId` 设了就恢复旧对话,没设就每次新建 -- id: stdio-agent - name: '@deepseek-ai/dsh-stdio-demo' - config: - model: deepseek-v4-flash - resumeSessionId: !!js process.env.RESUME_SESSION_ID - persistenceRoot: './.sessions' - welcome: 'agent REPL ready. Give it a coding task.' - persona: | - You are a coding agent powered by the {{model}} model. - Verify your work by running the code or tests. Keep answers brief and factual. - -# Token 计量:统一定义模型能看到的 token 上限 -- id: token-meter - name: '@deepseek-ai/dsh-token-meter' - config: - contextWindow: 128000 - -# 自动压缩:对话太长时自动总结旧内容,腾出上下文空间 -# thresholdRatio 超过这个比例就触发压缩 -# compactionRetries 是压缩后仍超标时的额外重试次数 -- id: compact-basic - name: '@deepseek-ai/dsh-compact-basic' - config: - thresholdRatio: 0.8 - retainTokens: 20480 - maxTokens: 8192 - compactionRetries: 1 - -# 子代理:把子任务分配给独立的 Agent 去做 -# subagent 是服务注册,spawn/fork 是两种委派方式: -# spawn — 全新子代理,不知道父级在聊什么 -# fork — 继承父级对话上下文的子代理 -# tool-subagent 把委派能力暴露给模型,toolName 是模型看到的工具名 -- id: subagent - name: '@deepseek-ai/dsh-subagent' - -- id: subagent-spawn - name: '@deepseek-ai/dsh-subagent-spawn' - config: - providerName: spawn - -- id: subagent-fork - name: '@deepseek-ai/dsh-subagent-fork' - config: - providerName: fork - -- id: tool-subagent - name: '@deepseek-ai/dsh-tool-subagent' - config: - provider: spawn - toolName: subagent - -- id: tool-subagent-fork - name: '@deepseek-ai/dsh-tool-subagent' - config: - provider: fork - toolName: subagent_fork - -# 动态工作流:模型编写一段编排脚本,引擎在独立 worker 线程里运行它, -# 并通过上面的 spawn 后端把 agent() 调用分发为子代理 -- id: workflow-workerthread - name: '@deepseek-ai/dsh-workflow-workerthread' - config: - provider: spawn - -- id: tool-workflow - name: '@deepseek-ai/dsh-tool-workflow' - -# 任务追踪:模型可以用 todo_write 记录和更新任务清单 -- id: tool-todo - name: '@deepseek-ai/dsh-tool-todo' - -# 文件系统:让 Agent 能读写编辑文件 -# fs-local 提供本地文件操作能力,cwd 是工作目录 -# fs-policy 是安全策略——必须先读才能写,防止模型盲写 -# tool-fs 把能力暴露给模型(read / write / edit 三个工具) -- 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' -``` - -和 echo-agent 对比:同一个 `dsh-stdio-demo` 应用主体,只是把 mock 换成了真实 API,加上了更多工具插件。 - -## 语法详解 - -### 插件声明字段 - -每个插件条目支持以下字段: - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `name` | string | 是 | 插件来源(npm 包名或相对路径) | -| `id` | string | 否 | 实例标识符,用于日志和调试。省略时由 loader 生成并写回 | -| `config` | object | 否 | 传递给插件的配置 | -| `disabled` | boolean | 否 | 设为 `true` 临时禁用该插件 | -| `group` | boolean | 否 | 标记该条目为嵌套分组(`config` 为子条目列表) | -| `inject` | array \| object | 否 | 声明该插件依赖的服务 | -| `intercept` | object | 否 | 按服务名拦截并覆盖下游配置 | -| `isolate` | object | 否 | 服务隔离:服务名 → `true` 或隔离标签 | - -### 插件来源 (`name`) - -**npm 包** — 已安装的 `@deepseek-ai/dsh-*` 包或第三方包: - -```yaml -- name: '@deepseek-ai/dsh-llm-deepseek' -``` - -**相对路径** — 本地 TypeScript 文件(相对于 `cordis.yml` 所在目录): - -```yaml -- name: './src/my-tool.ts' -``` - -### 环境变量 (`!!js`) - -用 `!!js` 标签在配置中引用运行时表达式: - -```yaml -config: - apiKey: !!js process.env.DEEPSEEK_API_KEY - cwd: !!js process.cwd() -``` - -::: warning -是 `!!js`(两个感叹号),不是 `!js`。写错了会静默失败。 -::: - -环境变量从仓库根目录的 `.env` 文件自动加载(已被 gitignore)。 - -### 禁用插件 - -不想删配置但暂时不加载?加一行 `disabled`: - -```yaml -- id: compact-basic - name: '@deepseek-ai/dsh-compact-basic' - disabled: true -``` - -## 各插件配置参考 - -### stdio-agent(标准应用主体) - -**包名:** `@deepseek-ai/dsh-stdio-demo` - -| 字段 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `model` | string | **必填** | 使用的模型名,需与 LLM 适配器注册的名字一致 | -| `persona` | string | `''` | 系统提示词。支持 `{{model}}` 等模板变量 | -| `toolOrder` | string[] | — | 模型看到的工具顺序。省略则按字母排序 | -| `persistenceRoot` | string | `'./.sessions'` | 会话日志存储目录 | -| `welcome` | string | `'ready.'` | 启动时显示的欢迎信息 | -| `resumeSessionId` | string | — | 恢复指定会话 ID。留空则每次新建 | - -### llm-deepseek(DeepSeek 适配器) - -**包名:** `@deepseek-ai/dsh-llm-deepseek` - -| 字段 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `apiKey` | string | `$DEEPSEEK_API_KEY` | API 密钥。省略则从环境变量读取 | -| `baseURL` | string | `$DEEPSEEK_BASE_URL` 或官方地址 | API 端点 | -| `models` | string[] | `['deepseek-v4-flash', 'deepseek-v4-pro']` | 注册的模型名列表 | -| `thinking` | `'enabled'` \| `'disabled'` | `'enabled'` | 是否开启思维链 | -| `reasoningEffort` | `'high'` \| `'max'` | — | 思维链深度(仅 thinking 开启时有效) | - -### bash-local(Bash 执行器) - -**包名:** `@deepseek-ai/dsh-bash-local` - -| 字段 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `cwd` | string | `process.cwd()` | 命令执行的工作目录 | -| `timeoutMs` | number | `120000` | 单条命令的超时时间(毫秒) | -| `maxTimeoutMs` | number | `600000` | 单条命令超时的上限(模型不能请求更久) | -| `maxOutputBytes` | number | `64000` | 单次输出的内存上限(超出后溢出到临时文件) | -| `graceMs` | number | `3000` | kill 时从 SIGTERM 到 SIGKILL 的等待时间 | - -### compact-basic(自动压缩) - -**包名:** `@deepseek-ai/dsh-compact-basic` - -| 字段 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `contextWindow` | number | **必填** | 模型的上下文窗口大小(token) | -| `thresholdRatio` | number | **必填** | token 占用超过此比例时触发压缩(0-1) | -| `retainTokens` | number | **必填** | 压缩后至少保留多少 token 的近期内容 | -| `maxTokens` | number | **必填** | 总结时的最大输出 token | -| `summarizationModel` | string | `''`(用当前模型) | 专门用于总结的模型名 | -| `compactionRetries` | number | **必填** | 首次压缩后仍超标时的额外重试次数 | -| `auto` | boolean | `true` | 是否自动在每步前检查并触发压缩 | -| `charsPerToken` | number | `4` | 每 token 估算字符数。中文应设 1-2 | - -### fs-local(文件系统) - -**包名:** `@deepseek-ai/dsh-fs-local` - -| 字段 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `cwd` | string | `process.cwd()` | 工作目录,相对路径以此为基准 | - -### fs-policy(文件系统策略) - -**包名:** `@deepseek-ai/dsh-fs-policy` - -无配置项。加载即启用"必须先读才能写"的安全策略。 - -### tool-fs(文件系统工具) - -**包名:** `@deepseek-ai/dsh-tool-fs` - -无配置项。加载后向模型暴露 `read`、`write`、`edit` 三个工具。 - -### tool-web(Web 工具) - -**包名:** `@deepseek-ai/dsh-tool-web` - -| 字段 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `search` | boolean | `true` | 是否注册 `web_search` 工具 | -| `fetch` | boolean | `true` | 是否注册 `web_fetch` 工具 | -| `searchMaxResults` | number | `8` | 单次搜索返回的最大结果数 | - -### subagent-spawn / subagent-fork(子代理后端) - -**包名:** `@deepseek-ai/dsh-subagent-spawn` / `@deepseek-ai/dsh-subagent-fork` - -| 字段 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `providerName` | string | `'spawn'` / `'fork'` | 注册到子代理服务的 provider 名称 | - -### tool-subagent(子代理工具) - -**包名:** `@deepseek-ai/dsh-tool-subagent` - -| 字段 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `provider` | string | **必填** | 使用哪个 provider(如 `spawn`、`fork`) | -| `toolName` | string | `'subagent'` | 暴露给模型的工具名。多次加载时必须不同 | -| `agentOptions.model` | string | — | 子代理使用的模型名(省略则继承父代理) | - -### tool-todo(任务清单) - -**包名:** `@deepseek-ai/dsh-tool-todo` - -无配置项。加载后向模型暴露 `todo_write` 工具。 - -### hmr(热替换) - -**包名:** `@cordisjs/plugin-hmr` - -| 字段 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `root` | string[] | `['.']` | 监听文件变更的目录列表 | -| `base` | string | — | 解析 `root` 的基准目录(默认取配置文件所在目录) | -| `ignored` | string[] | `['**/node_modules', '**/.*', 'cache', 'data']` | 忽略的 glob 列表 | -| `debounce` | number | `100` | 变更合并窗口(毫秒) | - -其余字段透传给 chokidar(`Config` 继承 `ChokidarOptions`)。 - -::: tip -hmr 仅用于开发环境。它需要 `node --expose-internals` 启动参数,`demo:*` 脚本已自动添加。 -::: - ---- - -## 加载顺序 - -`cordis.yml` 的条目是**并发启动**的(loader 对全部条目 `Promise.all`),文件顺序不决定加载顺序。真正的先后关系由依赖协调:插件声明的 `inject` 服务就绪之前,插件不会启动;服务出现后自动继续。所以**不要依赖书写顺序传递时序**——需要"先有 A 再有 B"就让 B `inject` A 提供的服务。 - -文件顺序只是给人读的。推荐按角色分组书写: - -1. **hmr** — 热替换(仅开发时需要) -2. **LLM 适配器** — 模型后端 -3. **执行器** — bash、fs 等能力提供者 -4. **应用主体** — `dsh-stdio-demo` 或 `dsh-acp-demo` -5. **附加插件** — compact、subagent、todo 等 - -应用主体内部已经捆绑了核心能力(session、tools、agent-loop),不需要手动加载。 - -## 下一步 - -- [开发插件](../develop/basic/) — 编写自己的插件 -- [API 参考](../api/) — 查看各插件完整接口