mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge origin/master into task/command-feedback-master
This commit is contained in:
@@ -1,10 +1,20 @@
|
||||
# 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 Agent Note](../.agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md) owns rationale.
|
||||
This file defines document structure, 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.
|
||||
|
||||
## Document structure
|
||||
|
||||
These rules apply to human-facing documentation; [Agent Notes](../.agents/notes/README.md) remain outside their scope. A [postmortem](postmortem/README.md) is an incident-scoped reference; chronology records evidence, not a teaching sequence. A document's subject and tree position fix its scope: describe its own subject at appropriate detail, describe direct children only by purpose, responsibility, and high-level behavior, and link to the owning descendant for lower-level detail. Document type does not widen that scope. A reference may be exhaustive only about its own subject. Testing mechanisms, fixtures, and harnesses belong at the lowest owning level; higher documents link there.
|
||||
|
||||
Classify every in-scope document as a tutorial or reference. A tutorial follows an ordered path to an outcome and introduces only what each step needs. A reference defines a lookup scope and describes current behavior without depending on a teaching sequence. Separate substantial tutorial and reference content; use a clear structural boundary when either part is small.
|
||||
|
||||
Before writing a tutorial, privately classify the reader's starting knowledge and each concept as beginner, intermediate, or advanced. Establish prerequisites before dependent concepts, increase difficulty gradually, and move unnecessary advanced material to a later tutorial or reference.
|
||||
|
||||
Author in this order: locate the document in the tree; set its permitted detail; choose tutorial or reference; for a tutorial, order concepts by prerequisite and difficulty; relocate descendant-owned detail; replace lower-level explanations with links to their owners.
|
||||
|
||||
## The tier taxonomy: one home per fact
|
||||
|
||||
Each fact has one home: the tier whose job it is. Elsewhere, link to that home; `verify-md-links` keeps links resolving while duplicated prose drifts.
|
||||
Each fact has one home: the tier whose job it is. Elsewhere, link to that home.
|
||||
|
||||
| Tier | Job | Does NOT belong there |
|
||||
|---|---|---|
|
||||
@@ -12,17 +22,15 @@ Each fact has one home: the tier whose job it is. Elsewhere, link to that home;
|
||||
| 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) |
|
||||
| [Agent Notes](../.agents/notes/README.md) | Active 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; archived notes are frozen history, never current authority |
|
||||
| [Agent Notes](../.agents/notes/README.md) | Decision records under their own lifecycle contract | Migration plans, checklists, and spec-speak once implemented; archived notes are frozen history |
|
||||
| [postmortem/](postmortem/README.md) | Incident stories — the only tier where war-story narrative belongs | — |
|
||||
| [cookbook/](cookbook/adding-a-package.md) | Step-by-step how-tos with numbered verify steps | Design rationale (→ the Agent Note each guide links) |
|
||||
| [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
|
||||
| Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns |
|
||||
| [development.md](development.md) | First-stop contributor onboarding: local setup, daily workflow, and CI shape at summary level; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), gate-by-gate enumerations that drift from `package.json` scripts |
|
||||
| Package README | Per-package config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc or catalog restatement, other packages' concerns |
|
||||
| [development.md](development.md) | Contributor onboarding: setup, daily workflow, and CI shape at summary level | Runtime rationale (→ Agent Notes), drifting gate inventories |
|
||||
| Generated catalogs: [cordis events](cordis-catalog/events.md), [cordis services](cordis-catalog/services.md), [Cordis core API](cordis-catalog/core/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive enumerations regenerated from source, freshness-gated | Hand edits of any kind |
|
||||
| Skills (`.agents/skills/`) | Reusable workflows and specialized decision standards | Product and runtime contracts (→ docs or source) |
|
||||
|
||||
Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type shapes → core data; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link.
|
||||
|
||||
## 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, Agent Notes, or postmortems.
|
||||
@@ -31,7 +39,7 @@ Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookb
|
||||
- **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)).
|
||||
- **Bilingual pairs update together**: editing either side obligates the counterpart and a re-record in the same change ([i18n contract](i18n/README.md)).
|
||||
- **Comments and JSDoc state complete contracts, not reasoning transcripts.** Preserve behavior, conditions, timing, modality, exceptions, consequences, and non-obvious orientation; delete implementation narration, test walkthroughs, review analysis, and code restatement. Keep the local contract and link to its owning rationale. Use [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for required coverage, decision rules, and examples.
|
||||
- **Comments and JSDoc state complete contracts, not reasoning transcripts.** Preserve behavior, timing, modality, exceptions, consequences, and non-obvious orientation; delete narration, test walkthroughs, review analysis, and code restatement. Keep the local contract and link its rationale. Use [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for details.
|
||||
- Your audience is professional programmers. Prefer concise and straight-forward English over metaphor. Do not overuse words like "gate", "vocabulary", "surface", "seams".
|
||||
|
||||
## Wordcount Budgets
|
||||
@@ -44,20 +52,18 @@ When the gate goes red:
|
||||
2. **Condense** content that belongs here but can be shorter.
|
||||
3. **Raise** the ceiling only when the words truly need the space; justify the manifest diff in the PR. A too-low ceiling is a budget bug.
|
||||
|
||||
Ceilings are guardrails, not reduction targets. Retain at least 5% headroom; lower a ceiling only when the document's durable contract still has room, and raise it when necessary content would otherwise be deleted. Targets: root `AGENTS.md` ≤ 1,600 words; `architecture.md` ≤ 1,800; each subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤ 650 and this file ≤ 1,250; `packages/README.md` ≤ 600. Review and the slop checklist govern unbudgeted tiers.
|
||||
Ceilings are guardrails, not reduction targets. At or below target, retain at least 5% headroom; above target, freeze the ceiling until relocation or condensation brings the document under target. Lower a ceiling only when the contract still has room, and raise it when content would otherwise be deleted. Targets: root `AGENTS.md` ≤ 1,600 words; `architecture.md` ≤ 1,800; subtree `AGENTS.md` ≤ 600, except `packages/AGENTS.md` ≤ 650 and this file ≤ 1,250; `packages/README.md` ≤ 600. Review governs unbudgeted tiers.
|
||||
|
||||
## The slop checklist
|
||||
|
||||
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 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.
|
||||
- Narrated history or war stories: "previously", "now", "no longer", "used to", "renamed", "was moved", PRs, or commits. State the current fact; link an Agent Note or postmortem when needed.
|
||||
- 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.
|
||||
- Hand-restated catalogs, JSDoc, or inventories of tests, packages, and status when source or a generator is authoritative.
|
||||
- Reasoning transcripts: step-by-step implementation narration, proof of obvious branches, test walkthroughs, or rejected local alternatives. Keep the resulting contract or durable rationale; delete the path used to derive it.
|
||||
- The same rationale repeated beside sibling methods. State it once at the owning seam or shared helper.
|
||||
- Rationale repeated beside sibling methods instead of once at the owning seam or 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/` Agent Notes: "should", migration plans, acceptance checklists. An implemented Agent Note describes what is, per the [implemented-note instructions](../.agents/notes/implemented/AGENTS.md).
|
||||
|
||||
@@ -17,20 +17,22 @@ sequenceDiagram
|
||||
participant Session
|
||||
participant SDK as UI or SDK listener
|
||||
User->>Agent: followup(content)
|
||||
Agent-->>SDK: <code>agent/inbox/enqueue</code>
|
||||
Agent-->>SDK: <code>agent/inbox/spliced</code>
|
||||
Agent-->>SDK: <code>agent/inbox/inserted</code> { message }
|
||||
Agent->>Driver: queued work wakes driver
|
||||
Driver-->>SDK: <code>agent/status</code> running
|
||||
Note over Agent,Driver: next-step acceptance window opens
|
||||
Driver->>Hooks: <code>agent/prompt-submit</code> waterfall
|
||||
Hooks-->>Driver: authoritative allow, block, or add context
|
||||
alt prompt blocked or admission failed
|
||||
Driver-->>Driver: append context-only batch or keep steering boundary pending
|
||||
else prompt allowed
|
||||
Note over Agent,Driver: claim pending next-step input plus one queued prompt
|
||||
Driver-->>SDK: <code>agent/inbox/spliced</code> pure deletion
|
||||
Driver-->>SDK: <code>agent/inbox/claimed</code> { message, turn } per message
|
||||
Driver->>Hooks: <code>agent/pre-step</code> waterfall
|
||||
Hooks-->>Driver: authoritative reject or enter(messages)
|
||||
alt proposed step rejected or pre-step failed
|
||||
Driver-->>Driver: claimed batch stays removed, no turn opens
|
||||
else enter proposed step
|
||||
Driver->>Session: <code>turn/start</code>
|
||||
Driver->>Session: <code>user/message</code>
|
||||
Driver->>Prompt: <code>system-prompt/assemble</code> waterfall
|
||||
Driver-->>Driver: <code>agent/step</code> serial checkpoint
|
||||
Driver->>Session: <code>step/start</code>
|
||||
Driver->>Session: <code>user/message</code> per entered message
|
||||
Driver->>Prompt: <code>system-prompt/assemble</code> waterfall
|
||||
Driver->>LLM: <code>agent/request</code> waterfall, then <code>llm/stream</code> waterfall
|
||||
LLM-->>Driver: StreamChunk*
|
||||
Driver->>Session: <code>assistant/chunk</code>*
|
||||
@@ -53,11 +55,17 @@ sequenceDiagram
|
||||
Driver->>Session: <code>tool/result</code>
|
||||
end
|
||||
end
|
||||
Driver->>Session: post-tool context and steering (no prompt-submit)
|
||||
Driver->>Session: <code>step/end</code>
|
||||
Driver->>Hooks: <code>agent/turn-stopping</code> serial terminal checkpoint
|
||||
opt natural stop and next-step inbox empty
|
||||
Driver->>Hooks: <code>agent/turn-stopping</code> serial terminal checkpoint
|
||||
end
|
||||
opt next-step input is pending
|
||||
Driver-->>Driver: claim pending next-step input
|
||||
Driver-->>SDK: <code>agent/inbox/claimed</code> { message, turn } per message
|
||||
Driver->>Hooks: <code>agent/pre-step</code> waterfall
|
||||
Hooks-->>Driver: authoritative reject or enter(messages)
|
||||
end
|
||||
end
|
||||
Note over Agent,Driver: next-step acceptance window closes
|
||||
Driver->>Session: <code>turn/end</code>
|
||||
end
|
||||
Driver-->>SDK: <code>agent/status</code> idle
|
||||
@@ -65,9 +73,9 @@ 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/step` for pressure before request derivation 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 failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.
|
||||
`dsh-compact-basic` uses `agent/pre-step` for pressure before request derivation 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 failed turn close, and opens a fresh retry turn only when pruning or summarization advances the surface replacement generation; otherwise the original request error remains authoritative.
|
||||
|
||||
The returned `agent/prompt-submit` allow is authoritative; listeners wrapping `next()` preserve downstream content and additional contexts unless replacement is intentional. Steering bypasses that waterfall and joins at its durable checkpoint.
|
||||
The returned `agent/pre-step` decision is authoritative; listeners wrapping `next()` preserve downstream messages unless replacement is intentional. Steering and injected context pass through the same waterfall after a later boundary claims their next-step batch.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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 docs/architecture.md
|
||||
architecture.md: 2ae982eba49b6dbd2365496915f9917071167813
|
||||
architecture.zh.md: abaef961504ff64dbcd1e8e8ba9bd002406fa7f4
|
||||
architecture.md: 9b84c1482cb379fd796e21db45f128ba49750c54
|
||||
architecture.zh.md: 84708fcae24623e50b0157782cf459c35a55844b
|
||||
|
||||
@@ -6,9 +6,7 @@ English | [中文](architecture.zh.md)
|
||||
|
||||
## Overview
|
||||
|
||||
Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services, typed events, and disposable registrations.
|
||||
|
||||
`packages/core/` groups the default agent flow; capabilities remain plugins.
|
||||
Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services, typed events, and disposable registrations. `packages/core/` groups the default flow; capabilities remain plugins.
|
||||
|
||||
### Default Services
|
||||
|
||||
@@ -46,7 +44,10 @@ Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services,
|
||||
| `ctx.sessionPersistence` | [`session-persistence/`](../packages/session-persistence/README.md) | durable session-log storage |
|
||||
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | live-preferred exact/filter/trace queries over SQLite FTS, workspace-authorized model tools |
|
||||
| `ctx.sessionTitle` | [`session-title/`](../packages/session-title/README.md) | log-backed fallbacks, one optional asynchronous provider |
|
||||
| `ctx.settings` | [`settings/`](../packages/settings/README.md) | per-plugin user-settings namespaces layered over composition entries |
|
||||
| `ctx.credentials` | [`credentials/`](../packages/credentials/README.md) | named secret references resolved per operation, never inlined in configuration |
|
||||
| `ctx.directoryPicker` | [`host/directory-picker`](../packages/host/directory-picker/README.md) | GUI-host directory picking (`native`/`browse` interactions) |
|
||||
| `ctx.typert` | [`typert/registry`](../packages/typert/registry/README.md) | runtime registry for generated package reflection and live Zod schemas |
|
||||
| `ctx.invariants` | [`support/invariants`](../packages/support/invariants/README.md) | package-name-selected registry of package-owned runtime checks |
|
||||
|
||||
## Event
|
||||
@@ -56,8 +57,8 @@ Events are the service extension API ([catalog](cordis-catalog/events.md), [prod
|
||||
### Event Domains
|
||||
|
||||
- **Session events** are durable log facts emitted through `session/event`.
|
||||
- **Agent events** carry live `Agent` for status, prompt admission, request shaping, validation, and continuation.
|
||||
- **Capability events** let owning seams attach policy and adapters without a loop import.
|
||||
- **Agent events** carry live `Agent` for inbox, step, status, request, validation, and continuation.
|
||||
- **Capability events** attach policy and adapters without a loop import.
|
||||
|
||||
### Interception Semantics
|
||||
|
||||
@@ -65,72 +66,67 @@ Waterfalls are around-middleware: listeners delegate with `next()`; returning wi
|
||||
|
||||
## Default Loop Lifecycle
|
||||
|
||||
A **session** is append-only. An ordinary **turn** claims one queued `send()` item; injection claims none. A successor awaits its predecessor'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 or plugins stop it; a **step** is one model request plus tools. Quotes in the [sequence below](agent-lifecycle.md) mark durable events.
|
||||
A **session** is append-only. A **turn** claims one queued follow-up, waits for its predecessor's checkpoint, and may share its `running` interval ([decision](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md)); injection claims none. A **step** is one model request plus tools. Fresh creation and persisted resume first acquire an exact unpublished `SessionPreparation`; Agent and session publication happen only after private setup against that Session is ready ([decision](../.agents/notes/implemented/architecture/2026-08-05-session-preparation.md)). Quotes in the [sequence](agent-lifecycle.md) mark durable events.
|
||||
|
||||
Creation without an id mints `<config-id>-session-<uuid>`; `sessionId` resumes or creates, while `resumeSessionId` requires history. Resume restores lineage and delegation depth before publication. Setup failures emit `agent-loop/config-start-failed`; teardown is silent.
|
||||
Creation without an id mints `<config-id>-session-<uuid>`; `sessionId` resumes or creates, while `resumeSessionId` requires history. Resume restores lineage and delegation depth before publication; setup failure emits `agent-loop/config-start-failed`.
|
||||
|
||||
### Turn Flow
|
||||
|
||||
```text
|
||||
choose declarative identity and fresh/resume path
|
||||
-> prepare private session + agent.ctx -> await unpublished setup
|
||||
choose declarative identity and acquire fresh/restored SessionPreparation
|
||||
-> prepare private agent.ctx around exact Session -> await unpublished setup -> invoke optional synchronous setup commit
|
||||
-> enter session + agent -> session/created -> agent/created
|
||||
-> enable driving -> agent/session-start(source) -> start driver
|
||||
forever:
|
||||
wait for a queued message
|
||||
claim message -> emit agent/status(running) if starting an interval
|
||||
open the next-step acceptance window
|
||||
-> agent/prompt-submit
|
||||
blocked or failed prompt -> close the window without opening a turn
|
||||
append a context-only caller batch immediately
|
||||
keep steering and context staged beside it pending for a later admitted turn
|
||||
allowed prompt:
|
||||
'turn/start'
|
||||
append prompt + additional contexts as separate 'user/message' events
|
||||
STEP loop:
|
||||
agent/step
|
||||
drain injected context and steering (steering bypasses prompt-submit)
|
||||
assemble system prompt and tool schemas
|
||||
snapshot the derived messages (the reconstruction boundary)
|
||||
waking inbox insertion starts the driver before send returns
|
||||
-> emit agent/status(running) if starting an interval
|
||||
-> 'turn/start'
|
||||
claim next-step input plus one next-turn message
|
||||
-> emit agent/inbox/claimed({ message, turn }) for each claimed message
|
||||
-> agent/pre-step(messages, { turn, step, signal })
|
||||
reject, empty input, cancellation, or listener failure
|
||||
-> the claimed batch stays removed; close the no-step turn; stop the driver
|
||||
enter -> step loop:
|
||||
'step/start'
|
||||
agent/request (config only) -> prepare reasoning/default under turn signal -> log request/header -> llm/stream (frozen, registration-bound)
|
||||
append the returned batch as separate 'user/message' events
|
||||
assemble ordered prompt and tool schemas -> snapshot derived messages
|
||||
agent/request (config only) -> prepare adapter defaults/provenance + context capacity under turn signal -> log request/header (+ request/context on route change) -> llm/stream (frozen, registration-bound)
|
||||
'assistant/chunk'
|
||||
'assistant/message'
|
||||
schedule tool calls by ctx.tools.executionMode:
|
||||
exclusive -> one-call barrier
|
||||
parallel -> rolling pool, <= maxParallelToolCalls in flight; reclassify before start
|
||||
each start -> 'tool/call' -> ordered tools/pre-execute -> concurrent tools/execute
|
||||
each model-order result -> ordered tools/post-execute -> 'tool/result'
|
||||
drain accepted tool context and steering
|
||||
exclusive -> barrier
|
||||
parallel -> rolling pool, <= maxParallelToolCalls; reclassify at start
|
||||
start -> 'tool/call' -> tools/pre-execute -> concurrent tools/execute
|
||||
model-order result -> ordered tools/post-execute -> 'tool/result'
|
||||
'step/end'
|
||||
continue for tools or steering unless a result concluded the turn
|
||||
otherwise agent/turn-stopping -> drain -> continue only for steering
|
||||
close the next-step acceptance window
|
||||
'turn/end' -> agent/settled
|
||||
tools owe another request or next-step inbox is nonempty
|
||||
-> claim -> agent/pre-step -> append entered batch -> continue
|
||||
otherwise agent/turn-stopping -> re-check the next-step inbox
|
||||
'turn/end'
|
||||
start the next waking queued message, or emit agent/status(idle)
|
||||
|
||||
idle inject:
|
||||
append 'user/message'
|
||||
do not open a turn or run the model
|
||||
queue non-waking next-step context
|
||||
leave it pending until followup or steer wakes the driver
|
||||
```
|
||||
|
||||
Each step assembles ordered prompt sections, tool schemas, and variables; unknown references fail the turn. `dsh-system-prompt` owns identity and persona; the loop supplies `provider`, `model`, and `cwd` ([prompt ownership](../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)).
|
||||
|
||||
Admission-time and active-turn `inject()` stage for the next step; post-tool `additionalContexts` settles after results. Steering shares that staging boundary and requests another step. Idle `inject()` appends immediately without changing turn numbers; persistence drains eagerly.
|
||||
`inject()` queues non-waking `next-step` context; an idle driver leaves it pending until `followup()` or `steer()` wakes the driver. Post-tool `additionalContexts` use the same inbox. `agent/pre-step` receives the exclusive claimed batch and upcoming turn, step, and signal. Reject opens no step; enter supplies the complete batch appended after `step/start`. Empty tool continuations still traverse the waterfall, whose final value settles all rewrites.
|
||||
|
||||
Pruning precedes summaries; overflow retries require durable progress. `agent/request-error` may authorize one retry turn between failed-step and turn close; cancellation wins. Adapter-owned `retryPolicy` makes normal mode bounded; always mode delegates specialized recovery before retrying until success or cancellation ([compaction](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md), [retry foundation](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md), [provider policy](../.agents/notes/implemented/feature/2026-07-24-provider-retry-policies.md)).
|
||||
Pruning precedes summaries; overflow retries require durable progress. `agent/request-error` may authorize a same-step retry of the frozen prompt; cancellation wins. Adapter `retryPolicy` bounds normal mode, while always mode retries after specialized recovery ([compaction](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md), [retry foundation](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md), [provider policy](../.agents/notes/implemented/feature/2026-07-24-provider-retry-policies.md)). The generated [agent lifecycle](agent-lifecycle.md) owns exact event order, and the [agent-loop README](../packages/core/agent-loop/README.md) owns queue, steering, retry, and cancellation mechanics.
|
||||
|
||||
### Failure Boundaries
|
||||
|
||||
Adapter failures close their step before `agent/request-error` receives the exact `Error`, normalized `LlmFailure`, and signal. A handled failure closes its turn and opens a retry turn from durable history without an idle notification; exhaustion leaves terminal `turn/end`. Failed chunks commit neither messages nor tool calls.
|
||||
Adapter selection, dispatch, and iteration failures become terminal error or aborted `finish` chunks. `agent/request-error` receives request coordinates, normalized `LlmFailure`, available retry policy, and signal; middleware and consumer errors remain outside recovery. Failed chunks commit neither messages nor tool calls.
|
||||
|
||||
Other failures use `agent/error`. Cancellation and disposal beat recovery. Before request-header commit, the turn signal cancels asynchronous model-capability preparation; undispatched tools get synthetic `tool/call`/`ABORTED_BEFORE_DISPATCH` pairs. Effective `cancel(cause)` emits its cause before queue clearing and abort; observers cannot veto; idle calls emit nothing. Durability records user or parent cancellation as `aborted`, teardown as `disposed`; teardown awaits quiescence. The cause affects reporting, not late result-context handling ([decision](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)).
|
||||
Other failures use `agent/error`; cancellation and disposal beat recovery. Before request-header commit, the turn signal cancels capability preparation; undispatched tools get synthetic `tool/call`/`ABORTED_BEFORE_DISPATCH` pairs. Effective `cancel(cause)` reports its cause before clearing and aborting; idle calls emit nothing. Durability distinguishes `aborted` cancellation from `disposed` teardown, which awaits quiescence ([decision](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)).
|
||||
|
||||
Turn and step events are turn-enclosed; idle injected `user/message` events may sit between turns. Reload closes an interrupted tail with a synthetic turn end. After close, only `agent/error` reports failures. Each turn has one [TurnEndReason](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap).
|
||||
Turn and step events are turn-enclosed; the loop appends `user/message` events only from entered batches inside a turn. A turn opens before the initial claim and pre-step, so rejection, empty input, cancellation, or failure closes a durable turn without any step events. Standalone `compact/* { turn: null }` events consume no turn, and their lock-time markers may interleave with inbox splices. Reload synthesizes interrupted turn ends; `session/end-seed` distinguishes stale compaction orphans from live locks. After close, only `agent/error` reports failures. Each turn has one [TurnEndReason](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap).
|
||||
|
||||
### Agent Handles
|
||||
|
||||
`ctx.agents` owns live agents and returns `AgentHandle { agent, dispose() }`. Plugins use full `send()` options or `followup()`, `steer()`, and `inject()` presets; `cancel()` and `whenIdle()` control lifecycle. One awaited disposer coordinates teardown ownership.
|
||||
`ctx.agents` owns agents and returns `AgentHandle { agent, dispose() }`. Plugins use `send()` or its `followup()`, `steer()`, and `inject()` presets. `cancel()` and `whenIdle()` control lifecycle, while awaited disposal owns teardown. A follow-up `MessageId` follows durable inbox insertion, claiming, and discard notifications, not prompt output or turn ending; only an owner of a whole activity interval may summarize it as a run result ([decision](../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md)).
|
||||
|
||||
### Agent Scope
|
||||
|
||||
@@ -142,11 +138,11 @@ Each agent owns scoped `agent.ctx`; shared storage overlays its tool, prompt, an
|
||||
|
||||
The session log is authoritative. `deriveMessages()` projects model history; raw `assistant/chunk` events preserve replay and UI fidelity. Fork, resume, transcript rendering, telemetry, and persistence derive from this stream.
|
||||
|
||||
**Model-visible ⟺ logged**: messages at `step/start` plus the folded `request/header` reconstruct every request; package-owned `dsh-agent-loop/invariant` can assert this through `ctx.invariants` ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
|
||||
**Model-visible ⟺ logged**: messages entering at `step/start` plus the folded `request/header` reconstruct every request. The header marks adapter defaults so later proposals discard them and re-resolve the route without losing explicit settings. `request/context` separately records registration-bound provider, model, and capacity metadata when the route changes; it does not participate in request reconstruction or header equality. `dsh-agent-loop/invariant` asserts reconstructability through `ctx.invariants` ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
|
||||
|
||||
Durability is a plugin concern. Backends eagerly drain synchronous `session/event` notifications. `session/flush` barriers precede each request and top-level tool dispatch, then follow `turn/end` before another queued turn or idle observation. `SessionPersistence` stores `SessionEvent` directly and metadata in `SessionHeader`; JSONL defaults to checksummed Zstandard, while SQLite shares the contract ([decision](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md)).
|
||||
Durability is a plugin concern. Backends eagerly drain synchronous `session/event` notifications. `session/flush` precedes requests and top-level tool dispatch, and follows `turn/end` before another turn or idle. `SessionPersistence` stores events and header metadata; JSONL defaults to checksummed Zstandard and SQLite shares the contract ([decision](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md)).
|
||||
|
||||
Log-only events may sit between turns. Owners append through `Session`, flushing only for durability. `session/title` relies on eager persistence and lifecycle drains. Latest title wins with provenance; fallback and provider work never delays responses. Such records are fork boundaries, so forks inherit titles ([decision](../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md)).
|
||||
Between turns, owners append log-only events through `Session`, flushing only for durability. `session/title` needs eager persistence and lifecycle drains; manual compaction flushes its bracket before the operation completes. Title work never delays responses; latest wins with provenance. Title records are inherited fork boundaries ([decision](../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md)).
|
||||
|
||||
### Model Content
|
||||
|
||||
@@ -162,11 +158,11 @@ A swappable capability usually has **interface / implementation / consumer** lay
|
||||
|
||||
Exceptions combine LLM interface/consumer, filesystem policy, web registries, and named skill/subagent providers. Subagents spawn fresh, fork a completed-turn prefix, or use ACP children ([subagent.md](core-data-structures/subagent.md)).
|
||||
|
||||
`dsh-workspace-context` injects baseline at the first `agent/step` and appends `ctx.fs`-discovered changes through `tools/post-execute`; its [decision](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md) records isolation. `dsh-paths` owns shared paths.
|
||||
`dsh-workspace-context` composes its baseline on the first `agent/pre-step` and folds it into the final entering batch right after the claimed prompt, so it reaches the first request with the direct prompt; rejection keeps it in the next-step inbox. Filesystem changes projected after tools are likewise folded into the next entering pre-step instead of creating a later context-only step ([decision](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md)). `dsh-paths` owns shared paths.
|
||||
|
||||
### Bundles And Apps
|
||||
|
||||
`dsh-agent-spine-demo` bundles a spine and optional goals. App packages own TUI, CLI, ACP automation, and JSON-RPC front doors ([README](../packages/examples/agent-spine-demo/README.md), [acp/](../packages/acp/README.md), [ui/](../packages/ui/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK defaults when config is absent ([Python SDK](../python/README.md)). Thin deployments use swappable backends and optional tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
|
||||
`dsh-agent-spine-demo` bundles a spine and optional goals. App packages own CLI, ACP automation, and JSON-RPC front doors ([README](../packages/examples/agent-spine-demo/README.md), [acp/](../packages/acp/README.md), [ui/](../packages/ui/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK defaults when config is absent ([Python SDK](../python/README.md)). Thin deployments use swappable backends and optional tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
|
||||
|
||||
### Where New Behavior Goes
|
||||
|
||||
@@ -183,8 +179,8 @@ New behavior attaches to a documented extension point; a loop change updates thi
|
||||
| Add filesystem access or policy | implement a `ctx.fs` provider or listen to `fs/*` policy events |
|
||||
| Confine spawned processes | use a `ctx.sandbox` backend; consumers wrap argv before spawning |
|
||||
| Intercept a request, tool, or turn | use its `agent/*` or `tools/*` event; `agent/turn-stopping` is the stop boundary |
|
||||
| Add model-facing context | call `agent.inject()` to append a sourced `user/message` without a turn |
|
||||
| Add UI or editor integration | drive `ctx.agents`, render from `session/event`; terminal-only overlays use `ctx.tui` |
|
||||
| Add model-facing context | call `agent.inject()` to queue sourced context for the next admitted request |
|
||||
| Add UI or editor integration | drive `ctx.agents` and render from `session/event` |
|
||||
| Add durable session state | extend `SessionEventMap`; render and replay from the log |
|
||||
| Add asynchronous session-title generation | register the sole `ctx.sessionTitle` provider |
|
||||
| Manage a same-session objective | use `ctx.goals`; continue through `Agent` and `agent/*` |
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
|
||||
## 概览
|
||||
|
||||
每个 harness 都是 [Cordis](cordis-primer.md) 上下文;各包(package)贡献服务、类型化事件和可释放的注册项。
|
||||
|
||||
`packages/core/` 汇集默认的 agent(智能体)流程;各项功能仍以插件形式存在。
|
||||
每个 harness 都是 [Cordis](cordis-primer.md) 上下文;各包(package)贡献服务、类型化事件和可释放的注册项。`packages/core/` 汇集默认流程;各项功能仍以插件形式存在。
|
||||
|
||||
### 默认服务
|
||||
|
||||
@@ -16,9 +14,9 @@
|
||||
|---|---|---|
|
||||
| — | [`dsh-scope`](../packages/core/scope/README.md) | 作用域上下文注册项与共享层存储(库) |
|
||||
| `ctx.sessions` | `dsh-session` | 内存中的事件溯源会话 |
|
||||
| `ctx.systemPrompt` | `dsh-system-prompt` | 有序提示词片段、工具 schema 和变量 |
|
||||
| `ctx.systemPrompt` | `dsh-system-prompt` | 有序的提示词片段、工具 schema 和变量 |
|
||||
| `ctx.tools` | `dsh-tools` | 工具注册表和[执行流水线](tool-execution-pipeline.md) |
|
||||
| `ctx.agents` | `dsh-agent` | 活跃 agent、委托创建、`agent/*` 事件、进程内发起方作用域 |
|
||||
| `ctx.agents` | `dsh-agent` | 活跃 agent(智能体)、委托创建、`agent/*` 事件、进程内发起方作用域 |
|
||||
| `ctx.agentLoop` | `dsh-agent-loop` | 实体 `Agent` 驱动器 |
|
||||
|
||||
### 功能服务
|
||||
@@ -46,7 +44,10 @@
|
||||
| `ctx.sessionPersistence` | [`session-persistence/`](../packages/session-persistence/README.md) | 会话日志的持久化存储 |
|
||||
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | 基于 SQLite 全文搜索的实时优先精确检索/过滤/追踪、经工作区授权的模型工具 |
|
||||
| `ctx.sessionTitle` | [`session-title/`](../packages/session-title/README.md) | 基于日志的回退标题和单个可选异步提供方 |
|
||||
| `ctx.settings` | [`settings/`](../packages/settings/README.md) | 按插件划分的用户设置命名空间,分层叠加在装配条目之上 |
|
||||
| `ctx.credentials` | [`credentials/`](../packages/credentials/README.md) | 具名密钥引用,按操作解析,绝不内联进配置 |
|
||||
| `ctx.directoryPicker` | [`host/directory-picker`](../packages/host/directory-picker/README.md) | GUI 宿主目录选取(`native`/`browse` 交互) |
|
||||
| `ctx.typert` | [`typert/registry`](../packages/typert/registry/README.md) | 生成的包反射和实时 Zod schema 的运行时注册表 |
|
||||
| `ctx.invariants` | [`support/invariants`](../packages/support/invariants/README.md) | 按包名筛选包自有运行时检查的注册表 |
|
||||
|
||||
## 事件
|
||||
@@ -56,8 +57,8 @@
|
||||
### 事件域
|
||||
|
||||
- **会话事件**是通过 `session/event` 发出的持久日志事实。
|
||||
- **Agent 事件**携带活跃 `Agent`,用于状态、提示词准入、请求塑形、验证和续跑。
|
||||
- **功能事件**让所属服务边界无需导入循环即可附加策略和适配器。
|
||||
- **Agent 事件**携带活跃 `Agent`,用于 inbox、步骤、状态、请求、验证和续跑。
|
||||
- **功能事件**无需导入循环即可附加策略和适配器。
|
||||
|
||||
### 拦截语义
|
||||
|
||||
@@ -65,72 +66,67 @@ waterfall(瀑布式事件)是环绕中间件:监听器通过 `next()` 委
|
||||
|
||||
## 默认循环生命周期
|
||||
|
||||
**会话**采用仅追加方式。普通**轮次**领取一项已排队的 `send()` 输入;注入不领取输入。后续轮次会等待前一轮次的检查点,但可以与其共用同一个 `running` 区间([决策](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md))。模型或插件停止轮次时,该轮次结束;一个**步骤**包含一次模型请求及其工具。[下文时序](agent-lifecycle.md)中的引号标记持久事件。
|
||||
**会话**采用仅追加方式。一个**轮次**领取一条已排队的后续消息,等待前一轮次的检查点,并可与其共用 `running` 区间([决策](../.agents/notes/implemented/simplification/2026-07-17-one-send-one-turn.md));注入不领取输入。一个**步骤**包含一次模型请求及其工具。新建与持久化恢复会先取得精确的未发布 `SessionPreparation`;只有基于该 Session 的私有设置准备完毕后,系统才会发布 agent 与会话([决策](../.agents/notes/implemented/architecture/2026-08-05-session-preparation.md))。[时序](agent-lifecycle.md)中的引号标记持久事件。
|
||||
|
||||
创建时若未提供 id,流程会生成 `<config-id>-session-<uuid>`;`sessionId` 用于恢复或创建会话,而 `resumeSessionId` 要求已有历史。恢复流程在发布前还原沿袭关系和委托深度。初始化失败会发出 `agent-loop/config-start-failed`;拆卸过程保持静默。
|
||||
创建时若未提供 id,流程会生成 `<config-id>-session-<uuid>`;`sessionId` 用于恢复或创建会话,而 `resumeSessionId` 要求已有历史。恢复流程在发布前还原沿袭关系和委托深度;初始化失败会发出 `agent-loop/config-start-failed`。
|
||||
|
||||
### 轮次流程
|
||||
|
||||
```text
|
||||
choose declarative identity and fresh/resume path
|
||||
-> prepare private session + agent.ctx -> await unpublished setup
|
||||
choose declarative identity and acquire fresh/restored SessionPreparation
|
||||
-> prepare private agent.ctx around exact Session -> await unpublished setup -> invoke optional synchronous setup commit
|
||||
-> enter session + agent -> session/created -> agent/created
|
||||
-> enable driving -> agent/session-start(source) -> start driver
|
||||
forever:
|
||||
wait for a queued message
|
||||
claim message -> emit agent/status(running) if starting an interval
|
||||
open the next-step acceptance window
|
||||
-> agent/prompt-submit
|
||||
blocked or failed prompt -> close the window without opening a turn
|
||||
append a context-only caller batch immediately
|
||||
keep steering and context staged beside it pending for a later admitted turn
|
||||
allowed prompt:
|
||||
'turn/start'
|
||||
append prompt + additional contexts as separate 'user/message' events
|
||||
STEP loop:
|
||||
agent/step
|
||||
drain injected context and steering (steering bypasses prompt-submit)
|
||||
assemble system prompt and tool schemas
|
||||
snapshot the derived messages (the reconstruction boundary)
|
||||
waking inbox insertion starts the driver before send returns
|
||||
-> emit agent/status(running) if starting an interval
|
||||
-> 'turn/start'
|
||||
claim next-step input plus one next-turn message
|
||||
-> emit agent/inbox/claimed({ message, turn }) for each claimed message
|
||||
-> agent/pre-step(messages, { turn, step, signal })
|
||||
reject, empty input, cancellation, or listener failure
|
||||
-> the claimed batch stays removed; close the no-step turn; stop the driver
|
||||
enter -> step loop:
|
||||
'step/start'
|
||||
agent/request (config only) -> prepare reasoning/default under turn signal -> log request/header -> llm/stream (frozen, registration-bound)
|
||||
append the returned batch as separate 'user/message' events
|
||||
assemble ordered prompt and tool schemas -> snapshot derived messages
|
||||
agent/request (config only) -> prepare adapter defaults/provenance + context capacity under turn signal -> log request/header (+ request/context on route change) -> llm/stream (frozen, registration-bound)
|
||||
'assistant/chunk'
|
||||
'assistant/message'
|
||||
schedule tool calls by ctx.tools.executionMode:
|
||||
exclusive -> one-call barrier
|
||||
parallel -> rolling pool, <= maxParallelToolCalls in flight; reclassify before start
|
||||
each start -> 'tool/call' -> ordered tools/pre-execute -> concurrent tools/execute
|
||||
each model-order result -> ordered tools/post-execute -> 'tool/result'
|
||||
drain accepted tool context and steering
|
||||
exclusive -> barrier
|
||||
parallel -> rolling pool, <= maxParallelToolCalls; reclassify at start
|
||||
start -> 'tool/call' -> tools/pre-execute -> concurrent tools/execute
|
||||
model-order result -> ordered tools/post-execute -> 'tool/result'
|
||||
'step/end'
|
||||
continue for tools or steering unless a result concluded the turn
|
||||
otherwise agent/turn-stopping -> drain -> continue only for steering
|
||||
close the next-step acceptance window
|
||||
'turn/end' -> agent/settled
|
||||
tools owe another request or next-step inbox is nonempty
|
||||
-> claim -> agent/pre-step -> append entered batch -> continue
|
||||
otherwise agent/turn-stopping -> re-check the next-step inbox
|
||||
'turn/end'
|
||||
start the next waking queued message, or emit agent/status(idle)
|
||||
|
||||
idle inject:
|
||||
append 'user/message'
|
||||
do not open a turn or run the model
|
||||
queue non-waking next-step context
|
||||
leave it pending until followup or steer wakes the driver
|
||||
```
|
||||
|
||||
每个步骤都会组装有序提示词片段、工具 schema 和变量;未知引用会使该轮次失败。`dsh-system-prompt` 负责身份和角色设定;循环提供 `provider`、`model` 和 `cwd`([提示词归属](../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md))。
|
||||
每个步骤都会组装有序的提示词片段、工具 schema 和变量;未知引用会使该轮次失败。`dsh-system-prompt` 负责身份和角色设定;循环提供 `provider`、`model` 和 `cwd`([提示词归属](../.agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md))。
|
||||
|
||||
接纳期间和活跃轮次内的 `inject()` 会为下一步骤暂存;工具执行后的 `additionalContexts` 会在结果记录完毕后落定。steering 与其共用这一暂存边界,并请求再执行一个步骤。空闲状态下的 `inject()` 会立即追加,且不改变轮次编号;持久化层会尽快排空。
|
||||
`inject()` 将不会唤醒驱动器的上下文排入 `next-step`;空闲驱动器会让它保持待处理,直至 `followup()` 或 `steer()` 唤醒。工具执行后的 `additionalContexts` 使用同一个 inbox。`agent/pre-step` 接收独占的已领取批次,以及即将使用的轮次、步骤和信号。拒绝则不进入步骤;进入则提供在 `step/start` 后追加的完整批次。空的工具续跑仍会经过 waterfall,其最终值一次性结算所有改写。
|
||||
|
||||
裁剪先于摘要;溢出重试必须取得持久进展。`agent/request-error` 可以在失败步骤与轮次关闭之间授权一个重试轮次;取消优先。适配器拥有的 `retryPolicy` 使 normal mode 保持有界;always mode 先委托专门恢复,再持续重试直至成功或取消([压缩](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)、[重试基础](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md)、[提供方策略](../.agents/notes/implemented/feature/2026-07-24-provider-retry-policies.md))。
|
||||
裁剪先于摘要;溢出重试必须取得持久进展。`agent/request-error` 可以授权使用冻结提示词进行同步骤重试;取消优先。适配器的 `retryPolicy` 使 normal mode 保持有界,always mode 则在专门恢复后重试([压缩](../.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)、[重试基础](../.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md)、[提供方策略](../.agents/notes/implemented/feature/2026-07-24-provider-retry-policies.md))。精确事件顺序由生成的 [agent 生命周期](agent-lifecycle.md)定义;队列、steering、重试与取消机制由 [agent-loop README](../packages/core/agent-loop/README.md)定义。
|
||||
|
||||
### 失败边界
|
||||
|
||||
适配器故障会先关闭自身步骤,再由 `agent/request-error` 接收准确的 `Error`、标准化的 `LlmFailure` 和信号。已处理的失败会关闭所在轮次,并从持久历史开启重试轮次,不发出空闲通知;重试耗尽则留下终态 `turn/end`。失败分片既不提交消息,也不提交工具调用。
|
||||
适配器选择、分发与迭代失败会成为 error 或 aborted 类型的终止 `finish` 分片。`agent/request-error` 接收请求坐标、标准化 `LlmFailure`、可用的重试策略和信号;middleware 与消费方错误仍在恢复之外。失败分片既不提交消息,也不提交工具调用。
|
||||
|
||||
其他故障使用 `agent/error`。取消和资源释放优先于恢复。在提交请求头之前,轮次信号会取消异步模型能力准备;尚未分派的工具会得到合成的 `tool/call`/`ABORTED_BEFORE_DISPATCH` 对。实际生效的 `cancel(cause)` 在清空队列和中止前发出原因;观察方不能否决;空闲调用不发事件。持久化层将用户或父级取消记录为 `aborted`,拆卸记录为 `disposed`;拆卸会等待完全停稳。原因只影响报告方式,不影响延迟完成的结果上下文处理([决策](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md))。
|
||||
其他故障使用 `agent/error`;取消和资源释放优先于恢复。在提交请求头之前,轮次信号会取消功能准备;尚未分派的工具会得到合成的 `tool/call`/`ABORTED_BEFORE_DISPATCH` 对。实际生效的 `cancel(cause)` 会在清空队列和中止前报告原因;空闲调用不发事件。持久化层以 `aborted` 区分取消,以 `disposed` 区分会等待完全停稳的拆卸([决策](../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md))。
|
||||
|
||||
轮次和步骤事件均位于轮次边界内;空闲时注入的 `user/message` 可以位于两个轮次之间。重新加载会用合成的轮次结束事件闭合中断尾部。关闭后仅由 `agent/error` 报告故障。每个轮次有一个 [TurnEndReason](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap)。
|
||||
轮次和步骤事件均位于轮次边界内;loop 只会在轮次内从进入步骤的批次追加 `user/message`。轮次会在首次领取与 pre-step 之前打开,因此拒绝、空输入、取消或失败会关闭一个不包含任何步骤事件的持久轮次。独立的 `compact/* { turn: null }` 事件不占用轮次,其锁定时刻标记可以与 inbox splice 交错。重新加载会为中断的轮次合成结束事件;`session/end-seed` 区分陈旧的压缩遗留项与活跃锁。关闭后仅由 `agent/error` 报告故障。每个轮次有一个 [TurnEndReason](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap)。
|
||||
|
||||
### Agent 句柄
|
||||
|
||||
`ctx.agents` 拥有活跃 agent,并返回 `AgentHandle { agent, dispose() }`。插件使用全部 `send()` 选项,或 `followup()`、`steer()` 和 `inject()` 预设;`cancel()` 与 `whenIdle()` 控制生命周期。一个需等待完成的 disposer 协调拆卸归属。
|
||||
`ctx.agents` 拥有 agent 并返回 `AgentHandle { agent, dispose() }`。插件使用 `send()`,或使用其 `followup()`、`steer()` 和 `inject()` 预设。`cancel()` 与 `whenIdle()` 控制生命周期,需等待完成的资源释放则负责拆卸。后续消息的 `MessageId` 跟踪持久 inbox 的插入、领取与丢弃通知,而不标识提示词输出或轮次结束;只有完整活动区间的所有方才能将其概括为一次运行结果([决策](../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md))。
|
||||
|
||||
### Agent 作用域
|
||||
|
||||
@@ -142,11 +138,11 @@ idle inject:
|
||||
|
||||
会话日志是权威依据。`deriveMessages()` 投影出模型历史;原始 `assistant/chunk` 事件保证回放和 UI 保真。fork、恢复、transcript(文本记录)渲染、遥测和持久化均派生自该事件流。
|
||||
|
||||
**模型可见 ⟺ 已记录**:`step/start` 时的消息与折叠后的 `request/header` 可以重建每个请求;该包的 `dsh-agent-loop/invariant` 可通过 `ctx.invariants` 断言这一点([可重建性](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md))。
|
||||
**模型可见 ⟺ 已记录**:在 `step/start` 进入的消息加上折叠后的 `request/header` 可以重建每个请求。该 header 会标记适配器默认值,使后续提议丢弃这些值并重新解析路由,同时不丢失显式设置。`request/context` 会在路由变化时另行记录与注册项绑定的提供方、模型及容量元数据;它不参与请求重建或 header 相等性判断。`dsh-agent-loop/invariant` 通过 `ctx.invariants` 断言可重建性([可重建性](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md))。
|
||||
|
||||
持久性由插件负责。后端会尽快排空同步的 `session/event` 通知。`session/flush` 屏障位于每次请求与顶层工具分发之前,并在 `turn/end` 之后、处理另一个已排队轮次或观察到空闲状态之前执行。`SessionPersistence` 直接存储 `SessionEvent`,并将元数据存入 `SessionHeader`;JSONL 默认采用带校验和的 Zstandard,SQLite 遵循同一契约([决策](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md))。
|
||||
持久性由插件负责。后端会尽快排空同步的 `session/event` 通知。`session/flush` 位于请求与顶层工具分发之前,并在 `turn/end` 之后、另一个轮次或空闲状态之前执行。`SessionPersistence` 存储事件和 header 元数据;JSONL 默认采用带校验和的 Zstandard,SQLite 遵循同一契约([决策](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md))。
|
||||
|
||||
纯日志事件可以位于轮次之间。事件所有方通过 `Session` 追加,仅为持久性而刷写。`session/title` 依赖尽快持久化与生命周期排空。最新标题按后写覆盖并携带来源信息;回退与提供方工作绝不会延迟响应。这类记录可作为 fork 边界,因此 fork 会继承标题([决策](../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md))。
|
||||
在轮次之间,事件所有方通过 `Session` 追加纯日志事件,仅为持久性而刷写。`session/title` 需要尽快持久化与生命周期排空;手动压缩会在操作完成前 flush 其标记对。标题工作绝不延迟响应;最新标题按后写覆盖并携带来源信息。标题记录是可继承的 fork 边界([决策](../.agents/notes/implemented/feature/2026-07-21-log-backed-session-titles.md))。
|
||||
|
||||
### 模型内容
|
||||
|
||||
@@ -162,11 +158,11 @@ idle inject:
|
||||
|
||||
例外情况包括 LLM(大语言模型)合并接口和消费方、文件系统整合策略、web 使用注册表、skill 和 subagent 使用具名提供方。subagent 可以通过 spawn 创建全新实例、fork 一个已完成轮次的前缀,或使用 ACP(Agent Client Protocol)子 agent([subagent.md](core-data-structures/subagent.md))。
|
||||
|
||||
`dsh-workspace-context` 在第一次 `agent/step` 注入基线,并通过 `tools/post-execute` 追加 `ctx.fs` 发现的变更;其[决策](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md)记录隔离方式。`dsh-paths` 负责共享路径。
|
||||
`dsh-workspace-context` 在第一次 `agent/pre-step` 组合基线并将它折入最终进入的批次、紧随已领取的直接提示词之后,使其与直接提示词一同抵达第一次请求;reject 则将它留在 next-step inbox。工具执行后投影的文件系统变更也会折入下一次进入步骤的 pre-step,而不会另外创建稍后的纯上下文步骤([决策](../.agents/notes/implemented/feature/2026-06-24-workspace-context.md))。`dsh-paths` 负责共享路径。
|
||||
|
||||
### 组合包与应用
|
||||
|
||||
`dsh-agent-spine-demo` 组合一套主干和可选目标。应用包负责 TUI、CLI(命令行界面)、ACP 自动化入口和 JSON-RPC 入口([README](../packages/examples/agent-spine-demo/README.md)、[acp/](../packages/acp/README.md)、[ui/](../packages/ui/README.md))。`dsh-jsonrpc-agent` 启动外部 `cordis.yml`;Python SDK 在配置缺失时提供默认项([Python SDK](../python/README.md))。轻量部署使用可替换后端和可选工具([examples/](../examples/AGENTS.md)、[可运行接线](cookbook/extension-cookbook.md#runnable-wirings)、[图谱](graph-atlas.md))。
|
||||
`dsh-agent-spine-demo` 组合一套主干和可选目标。应用包负责 CLI(命令行界面)、ACP 自动化入口和 JSON-RPC 入口([README](../packages/examples/agent-spine-demo/README.md)、[acp/](../packages/acp/README.md)、[ui/](../packages/ui/README.md))。`dsh-jsonrpc-agent` 启动外部 `cordis.yml`;Python SDK 在配置缺失时提供默认项([Python SDK](../python/README.md))。轻量部署使用可替换后端和可选工具([examples/](../examples/AGENTS.md)、[可运行接线](cookbook/extension-cookbook.md#runnable-wirings)、[图谱](graph-atlas.md))。
|
||||
|
||||
### 新行为的归属位置
|
||||
|
||||
@@ -183,8 +179,8 @@ idle inject:
|
||||
| 添加文件系统访问或策略 | 实现 `ctx.fs` 提供方,或监听 `fs/*` 策略事件 |
|
||||
| 限制生成的进程 | 使用 `ctx.sandbox` 后端;消费方在生成前包装 argv |
|
||||
| 拦截请求、工具或轮次 | 使用相应的 `agent/*` 或 `tools/*` 事件;`agent/turn-stopping` 是停止边界 |
|
||||
| 添加模型可见上下文 | 调用 `agent.inject()`,追加带来源的 `user/message`,但不创建轮次 |
|
||||
| 添加 UI 或编辑器集成 | 驱动 `ctx.agents`,从 `session/event` 渲染;仅终端浮层使用 `ctx.tui` |
|
||||
| 添加模型可见上下文 | 调用 `agent.inject()`,将带来源的上下文排入下一次获准请求 |
|
||||
| 添加 UI 或编辑器集成 | 驱动 `ctx.agents` 并从 `session/event` 渲染 |
|
||||
| 添加持久会话状态 | 扩展 `SessionEventMap`;从日志渲染和回放 |
|
||||
| 添加异步会话标题生成 | 注册唯一的 `ctx.sessionTitle` 提供方 |
|
||||
| 管理同会话目标 | 使用 `ctx.goals`;通过 `Agent` 和 `agent/*` 续跑 |
|
||||
|
||||
@@ -29,12 +29,22 @@ flowchart LR
|
||||
pkg_invariants["invariants"]
|
||||
svc_invariants["ctx.invariants<br/>Package-owned invariant registry"]
|
||||
pkg_scope["scope"]
|
||||
pkg_typert_registry["typert-registry"]
|
||||
svc_typert["ctx.typert<br/>Runtime type registry"]
|
||||
pkg_typert_loader["typert-loader"]
|
||||
svc_sessionPersistence["ctx.sessionPersistence<br/>Durable session persistence seam"]
|
||||
pkg_session_persistence_jsonl["session-persistence-jsonl"]
|
||||
pkg_session_persistence_sqlite["session-persistence-sqlite"]
|
||||
pkg_tool_bash["tool-bash"]
|
||||
pkg_hooks_claude["hooks-claude"]
|
||||
pkg_hooks_codex["hooks-codex"]
|
||||
pkg_settings["settings"]
|
||||
svc_settings["ctx.settings<br/>User-settings seam"]
|
||||
pkg_settings_local["settings-local"]
|
||||
pkg_apiproxy["apiproxy"]
|
||||
pkg_credentials["credentials"]
|
||||
svc_credentials["ctx.credentials<br/>Credential seam"]
|
||||
pkg_credentials_local["credentials-local"]
|
||||
pkg_session_telemetry["session-telemetry"]
|
||||
svc_telemetry["ctx.telemetry<br/>Session telemetry seam"]
|
||||
pkg_session_telemetry_otel["session-telemetry-otel"]
|
||||
@@ -46,12 +56,10 @@ flowchart LR
|
||||
svc_storageDomain["ctx.storageDomain<br/>Domain data facility"]
|
||||
pkg_workspace["workspace"]
|
||||
svc_workspace["ctx.workspace<br/>Workspace entity registry"]
|
||||
pkg_apiproxy["apiproxy"]
|
||||
svc_sessionQuery["ctx.sessionQuery<br/>Session reads, traces, filters, and search"]
|
||||
pkg_session_reference["session-reference"]
|
||||
pkg_tool_session_query["tool-session-query"]
|
||||
svc_sessionReferences["ctx.sessionReferences<br/>Cross-session snapshot preparation"]
|
||||
pkg_tui["tui"]
|
||||
pkg_session_title["session-title"]
|
||||
svc_sessionTitle["ctx.sessionTitle<br/>Log-backed session titles"]
|
||||
pkg_session_title_first_message_llm["session-title-first-message-llm"]
|
||||
@@ -79,13 +87,11 @@ flowchart LR
|
||||
pkg_host_apiproxy["host-apiproxy"]
|
||||
pkg_session_projection_cache["session-projection-cache"]
|
||||
svc_sessionProjectionCache["ctx.sessionProjectionCache<br/>Persisted projection cache"]
|
||||
svc_tui["ctx.tui<br/>Mounted-terminal interaction service"]
|
||||
pkg_skill["skill"]
|
||||
svc_skills["ctx.skills<br/>Skill provider registry"]
|
||||
pkg_skill_local["skill-local"]
|
||||
svc_agents["ctx.agents<br/>Agent service"]
|
||||
pkg_acp["acp"]
|
||||
pkg_tui_demo["tui-demo"]
|
||||
svc_agentLoop["ctx.agentLoop<br/>Concrete loop driver"]
|
||||
pkg_agent_spine_demo["agent-spine-demo"]
|
||||
pkg_goal["goal"]
|
||||
@@ -99,6 +105,9 @@ flowchart LR
|
||||
pkg_subagent_acp["subagent-acp"]
|
||||
pkg_bash["bash"]
|
||||
svc_bash["ctx.bash<br/>Bash executor seam"]
|
||||
pkg_pwsh_local["pwsh-local"]
|
||||
pkg_tool_pwsh["tool-pwsh"]
|
||||
pkg_bash_env["bash-env"]
|
||||
svc_bashEnv["ctx.bashEnv<br/>Managed bash environment registry"]
|
||||
pkg_pty["pty"]
|
||||
svc_pty["ctx.pty<br/>Persistent PTY session registry"]
|
||||
@@ -123,9 +132,10 @@ flowchart LR
|
||||
pkg_compact["compact"]
|
||||
svc_compact["ctx.compact<br/>Compaction seam"]
|
||||
pkg_subagent["subagent"]
|
||||
svc_subagents["ctx.subagents<br/>Subagent provider registry"]
|
||||
svc_subagents["ctx.subagents<br/>Subagent provider and continuation service"]
|
||||
pkg_subagent_spawn["subagent-spawn"]
|
||||
pkg_subagent_fork["subagent-fork"]
|
||||
pkg_tool_subagent_control["tool-subagent-control"]
|
||||
pkg_tool_ralph["tool-ralph"]
|
||||
pkg_tasks["tasks"]
|
||||
svc_tasks["ctx.tasks<br/>Background task registry"]
|
||||
@@ -160,6 +170,7 @@ flowchart LR
|
||||
pkg_agent_loop --> svc_agentLoop
|
||||
pkg_approval --> svc_approval
|
||||
pkg_bash --> svc_bash
|
||||
pkg_bash_env --> svc_bashEnv
|
||||
pkg_bash_local --> svc_bash
|
||||
pkg_bash_sandbox --> svc_bash
|
||||
pkg_code_runtime --> svc_codeRuntime
|
||||
@@ -168,6 +179,8 @@ flowchart LR
|
||||
pkg_compact --> svc_compact
|
||||
pkg_compact_basic --> svc_compact
|
||||
pkg_compact_tool_result_prune --> svc_toolResultPrune
|
||||
pkg_credentials --> svc_credentials
|
||||
pkg_credentials_local --> svc_credentials
|
||||
pkg_directory_picker --> svc_directoryPicker
|
||||
pkg_directory_picker_browse --> svc_directoryPicker
|
||||
pkg_directory_picker_native --> svc_directoryPicker
|
||||
@@ -185,6 +198,7 @@ flowchart LR
|
||||
pkg_plan_mode --> svc_planMode
|
||||
pkg_pty --> svc_pty
|
||||
pkg_pty_local --> svc_pty
|
||||
pkg_pwsh_local --> svc_bash
|
||||
pkg_sandbox --> svc_sandbox
|
||||
pkg_sandbox_local --> svc_sandbox
|
||||
pkg_sandbox_policy --> svc_sandboxPolicy
|
||||
@@ -202,6 +216,8 @@ flowchart LR
|
||||
pkg_session_title --> svc_sessionTitle
|
||||
pkg_session_title_all_messages_llm --> svc_sessionTitle
|
||||
pkg_session_title_first_message_llm --> svc_sessionTitle
|
||||
pkg_settings --> svc_settings
|
||||
pkg_settings_local --> svc_settings
|
||||
pkg_skill --> svc_skills
|
||||
pkg_skill_local --> svc_skills
|
||||
pkg_spill --> svc_spillStore
|
||||
@@ -220,10 +236,8 @@ flowchart LR
|
||||
pkg_tasks --> svc_tasks
|
||||
pkg_tasks_local --> svc_tasks
|
||||
pkg_token_meter --> svc_tokenMeter
|
||||
pkg_tool_bash --> svc_bashEnv
|
||||
pkg_tools --> svc_tools
|
||||
pkg_tui --> svc_tui
|
||||
pkg_tui --> svc_userInteraction
|
||||
pkg_typert_registry --> svc_typert
|
||||
pkg_user_interaction --> svc_userInteraction
|
||||
pkg_web --> svc_web
|
||||
pkg_web_fetch_local --> svc_web
|
||||
@@ -239,16 +253,20 @@ flowchart LR
|
||||
svc_agents --> pkg_agent_loop
|
||||
svc_agents --> pkg_cli_demo
|
||||
svc_agents --> pkg_subagent_inprocess
|
||||
svc_agents --> pkg_tui_demo
|
||||
svc_approval --> pkg_tool_bash
|
||||
svc_approval --> pkg_tools
|
||||
svc_bash --> pkg_hooks_claude
|
||||
svc_bash --> pkg_hooks_codex
|
||||
svc_bash --> pkg_tool_bash
|
||||
svc_bash --> pkg_tool_pwsh
|
||||
svc_bashEnv --> pkg_tool_bash
|
||||
svc_bashEnv --> pkg_tool_pwsh
|
||||
svc_clientModuleHost --> pkg_hmr
|
||||
svc_codeRuntime --> pkg_tools
|
||||
svc_commands --> pkg_tui
|
||||
svc_compact --> pkg_compact_basic
|
||||
svc_credentials --> pkg_apiproxy
|
||||
svc_credentials --> pkg_llm_deepseek
|
||||
svc_credentials --> pkg_llm_pi_ai
|
||||
svc_directoryPicker --> pkg_apiproxy
|
||||
svc_fs --> pkg_tool_fs
|
||||
svc_httpServer --> pkg_connection
|
||||
@@ -278,7 +296,6 @@ flowchart LR
|
||||
svc_sessionProjections --> pkg_tool_todo
|
||||
svc_sessionQuery --> pkg_session_reference
|
||||
svc_sessionQuery --> pkg_tool_session_query
|
||||
svc_sessionReferences --> pkg_tui
|
||||
svc_sessions --> pkg_agent
|
||||
svc_sessions --> pkg_agent_loop
|
||||
svc_sessions --> pkg_cli_demo
|
||||
@@ -287,12 +304,16 @@ flowchart LR
|
||||
svc_sessions --> pkg_session_query
|
||||
svc_sessions --> pkg_session_query_sqlite
|
||||
svc_sessions --> pkg_subagent_inprocess
|
||||
svc_settings --> pkg_apiproxy
|
||||
svc_settings --> pkg_llm_deepseek
|
||||
svc_settings --> pkg_llm_pi_ai
|
||||
svc_skills --> pkg_tool_skill
|
||||
svc_spillStore --> pkg_spill_policy
|
||||
svc_storage --> pkg_storage_domain
|
||||
svc_storageDomain --> pkg_workspace
|
||||
svc_subagents --> pkg_tool_ralph
|
||||
svc_subagents --> pkg_tool_subagent
|
||||
svc_subagents --> pkg_tool_subagent_control
|
||||
svc_subprocess --> pkg_bash_local
|
||||
svc_subprocess --> pkg_bash_sandbox
|
||||
svc_subprocess --> pkg_lsp_local
|
||||
@@ -318,8 +339,8 @@ flowchart LR
|
||||
svc_tools --> pkg_tool_subagent
|
||||
svc_tools --> pkg_tool_todo
|
||||
svc_tools --> pkg_tool_web
|
||||
svc_typert --> pkg_typert_loader
|
||||
svc_userInteraction --> pkg_tool_ask_user
|
||||
svc_userInteraction --> pkg_tui
|
||||
svc_web --> pkg_tool_web
|
||||
svc_workflows --> pkg_tool_ralph
|
||||
svc_workflows --> pkg_tool_workflow
|
||||
@@ -334,29 +355,31 @@ flowchart LR
|
||||
| `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), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`invariants`](../packages/support/invariants) | - | Owns append-only Session instances and emits the durable session event feed. |
|
||||
| `ctx.invariants` | `core` | [`invariants`](../packages/support/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
|
||||
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader) | - | Plugins register live zod contributions directly or through dsh-typert-loader; runtime consumers query schemas and reflection metadata at their own edges. |
|
||||
| `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), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | - | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
|
||||
| `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-local`](../packages/settings/settings-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the web gateway serves redacted layered descriptors and writes the user layer. |
|
||||
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `apiproxy` | - | Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the web gateway exposes value-free views and write-only storage. |
|
||||
| `ctx.telemetry` | `seam` | [`session-telemetry`](../packages/telemetry/session-telemetry) | [`session-telemetry-otel`](../packages/telemetry/session-telemetry-otel) | - | - | The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process. |
|
||||
| `ctx.storage` | `seam` | [`storage`](../packages/storage/storage) | [`storage-json`](../packages/storage/storage-json), [`storage-sqlite`](../packages/storage/storage-sqlite) | [`storage-domain`](../packages/storage/storage-domain) | - | Backends register side by side under names; data forms (domain first) mount on the hub and translate typed operations into opaque KV-unit primitives. |
|
||||
| `ctx.storageDomain` | `core` | [`storage-domain`](../packages/storage/storage-domain) | - | [`workspace`](../packages/workspace/workspace) | - | Waits for every configured backend, then publishes the domain form as one lifecycle-bound service for typed durable state. |
|
||||
| `ctx.workspace` | `core` | [`workspace`](../packages/workspace/workspace) | - | `apiproxy` | - | Owns WorkspaceId-branded records over the domain facility; stable sessionIds accounts drive Host RPC and GUI projections. |
|
||||
| `ctx.sessionQuery` | `seam` | [`session-query`](../packages/session-query/session-query) | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | [`session-reference`](../packages/context/session-reference), [`tool-session-query`](../packages/session-query/tool-session-query) | - | The interface supplies exact reads, filters, and traces; its concrete backend adds full-text reconciliation, ranking, snippets, and cursor generations, while the model consumer owns workspace authority and cursor-free rendering. |
|
||||
| `ctx.sessionReferences` | `core` | [`session-reference`](../packages/context/session-reference) | - | [`tui`](../packages/ui/tui) | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. |
|
||||
| `ctx.sessionReferences` | `core` | [`session-reference`](../packages/context/session-reference) | - | - | - | Projects bounded current-surface conversation snapshots into durable untrusted message context; host adapters own mention syntax. |
|
||||
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session-title/session-title) | [`session-title-first-message-llm`](../packages/session-title/session-title-first-message-llm), [`session-title-all-messages-llm`](../packages/session-title/session-title-all-messages-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. |
|
||||
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-pty`](../packages/pty/tool-pty), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. |
|
||||
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/cordis/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-pty`](../packages/pty/tool-pty), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. |
|
||||
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | [`tui`](../packages/ui/tui) | [`tool-ask-user`](../packages/ui/tool-ask-user), [`tui`](../packages/ui/tui) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
|
||||
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | - | [`tool-ask-user`](../packages/ui/tool-ask-user) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
|
||||
| `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions. |
|
||||
| `ctx.commands` | `core` | [`commands`](../packages/ui/commands) | - | [`tui`](../packages/ui/tui) | - | Plugins register direct human commands; TUI consumes the effective per-agent catalog without sending invocations to the model. |
|
||||
| `ctx.commands` | `core` | [`commands`](../packages/ui/commands) | - | - | - | Plugins register direct human commands without sending invocations to the model. |
|
||||
| `ctx.sessionProjections` | `core` | [`session-projection`](../packages/session-projection/session-projection) | - | [`tool-todo`](../packages/todo/tool-todo), [`session-title`](../packages/session-title/session-title), [`host-apiproxy`](../packages/host/apiproxy) | - | Domains register state-driven fold units; the eager drive keeps per-session watermark states and api-proxy serves baselines and pushes changed values. |
|
||||
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session-projection/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. |
|
||||
| `ctx.tui` | `bundle` | [`tui`](../packages/ui/tui) | - | - | - | One TUI front door provides a FIFO overlay host; injected plugins receive caller-fiber ownership without access to pi-tui or terminal lifecycle state. |
|
||||
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
|
||||
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`tui-demo`](../packages/examples/tui-demo) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
|
||||
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`cli-demo`](../packages/examples/cli-demo), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
|
||||
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
|
||||
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
|
||||
| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`lsp-local`](../packages/lsp/lsp-local), [`subagent-acp`](../packages/subagent/subagent-acp) | - | The bash executors, the LSP host, and the ACP subagent backend spawn their children through ctx.subprocess; the service owns tree lifetime, stdio dispositions (pipes, inherit, bounded spill-backed collection), and kill escalation. |
|
||||
| `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.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox), [`pwsh-local`](../packages/bash/pwsh-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pwsh`](../packages/bash/tool-pwsh), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them. |
|
||||
| `ctx.bashEnv` | `core` | [`bash-env`](../packages/bash/bash-env) | - | [`tool-bash`](../packages/bash/tool-bash), [`tool-pwsh`](../packages/bash/tool-pwsh) | - | Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace. |
|
||||
| `ctx.pty` | `seam` | [`pty`](../packages/pty/pty) | [`pty-local`](../packages/pty/pty-local) | [`tool-pty`](../packages/pty/tool-pty) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-pty exposes the owner-scoped model surface. |
|
||||
| `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox), [`pty-local`](../packages/pty/pty-local) | - | 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), [`pty-local`](../packages/pty/pty-local) | - | 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. |
|
||||
@@ -365,7 +388,7 @@ flowchart LR
|
||||
| `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), [`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), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; tool-subagent exposes configured delegation while tool-ralph requires one fresh structured-output route. |
|
||||
| `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), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
|
||||
| `ctx.tasks` | `seam` | [`tasks`](../packages/tasks/tasks) | [`tasks-local`](../packages/tasks/tasks-local) | [`tool-bash`](../packages/bash/tool-bash), [`tool-pty`](../packages/pty/tool-pty), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-tasks`](../packages/tasks/tool-tasks) | - | Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it; tasks-local is the process-local registry. |
|
||||
| `ctx.web` | `seam` | [`web`](../packages/web/web) | [`web-search-exa`](../packages/web/web-search-exa), [`web-search-perplexity`](../packages/web/web-search-perplexity), [`web-search-deepseek`](../packages/web/web-search-deepseek), [`web-fetch-local`](../packages/web/web-fetch-local) | [`tool-web`](../packages/web/tool-web) | - | Search and fetch providers register into one ctx.web seam; tool-web owns the stable model-facing names. |
|
||||
| `ctx.spillStore` | `seam` | [`spill`](../packages/spill/spill) | [`spill-local`](../packages/spill/spill-local) | [`spill-policy`](../packages/spill/spill-policy) | - | The backend saves oversized tool text and returns a model-facing locator plus retrieval hint; spill-policy is the tools/post-execute consumer that decides when to spill. |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +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
|
||||
adding-a-package.md: 1859310965538b35a353ee05c94b01d1093a3e43
|
||||
adding-a-package.zh.md: 22f574a0469609e44f5c55957560ff0f04b9a053
|
||||
# pnpm run verify-translation-pairing --write docs/cookbook/adding-a-package.md
|
||||
adding-a-package.md: a45b222f6aed905a18ef9b480c989e6045029afe
|
||||
adding-a-package.zh.md: af0e4d0779fa99ce43ebccba00c33eab16c4d362
|
||||
|
||||
@@ -14,7 +14,6 @@ packages/<group>/<pkg>/
|
||||
# ../../../vendor/cordis (+ ../../../vendor/schemastery if
|
||||
# you use Config, + ../../<group>/<dep> for each dsh dep)
|
||||
src/index.ts # service default export or plugin (name/inject/apply/Config)
|
||||
tests/<x>.spec.ts
|
||||
README.md # service API, events, extension points, design notes,
|
||||
# + gated Model Experience context blocks or short form
|
||||
# + the gated "Known Limitations and Deferred Work" section
|
||||
@@ -23,7 +22,7 @@ packages/<group>/<pkg>/
|
||||
|
||||
Choose an existing group when one matches the package's role (`core`, `llm`, `bash`, `compact`, `subagent`, `todo`, `session-persistence`, `ui`, `util`, or `support`). A new group is allowed, but it is a pure container: no `package.json`, no source files, and packages still sit exactly one level below it.
|
||||
|
||||
package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `package.json`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list is precise: `lib/index.js`, `lib/types/**/*.d.ts`, `lib/types/**/*.d.ts.map`, and `src`; do not publish `lib/types` JS or JS-map intermediates or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
|
||||
package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, a `version` matching the root `package.json`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list contains exactly `lib/index.js`, `lib/invariant.js`, `lib/types/**/*.d.ts`, and package-specific runtime artifacts recognized by the gate; a package whose runtime export points into the emitted tree also includes `lib/types/**/*.js`. Do not publish `src`, declaration maps, JS maps, or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
|
||||
|
||||
In-package relative imports use explicit `.ts` specifiers in source (for example, `export * from './types.ts'`). The compiler rewrites those to `.js` in emitted JS and leaves explicit `.ts` specifiers in declarations, which standard NodeNext/Node16 TypeScript consumers resolve to the sibling `.d.ts` files.
|
||||
|
||||
@@ -33,11 +32,11 @@ In-package relative imports use explicit `.ts` specifiers in source (for example
|
||||
|---|---|
|
||||
| `tsconfig.base.json` | no edit for an existing group; for a new group, add a `./packages/<group>/*/src` candidate to the `@deepseek-ai/dsh-*` wildcard |
|
||||
| `tsconfig.host.json` (host-side package) or `tsconfig.client.json` (client-side package) | add `{ "path": "./packages/<group>/<pkg>" }` to `references` — exactly one aggregate, never both ([layout](../development.md#typescript-project-layout)) |
|
||||
| `knip.json` | only if the package has non-`*.spec.ts` entries (e.g. `*.e2e.ts` → add a per-workspace override like `packages/llm/llm-deepseek`) |
|
||||
| `knip.json` | only if the package has entrypoints that repository discovery does not already cover |
|
||||
|
||||
A `packages/client/*` package additionally extends `tsconfig.base.client.json` instead of `tsconfig.base.json`, and a client plugin package declares `dshClient` in package.json, exports `./client`, and calls the shared tsdown preset (`packages/client/tsdown.client.ts`) — see [packages/client/AGENTS.md](../../packages/client/AGENTS.md) for the client-side contract.
|
||||
|
||||
Covered automatically by globs or package-manifest discovery — no edits needed: root `package.json` workspaces, `scripts/publint-all.ts`, `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`, `scripts/check-workspace-constraints.ts`.
|
||||
Covered automatically by globs or package-manifest discovery — no edits needed: root `package.json` workspaces, `scripts/publint-all.ts`, `tsdown.config.ts`, `.oxlintrc.json`, `scripts/check-workspace-constraints.ts`.
|
||||
|
||||
## 3. Decide the package topology
|
||||
|
||||
@@ -85,8 +84,7 @@ A package with no context effect or one consumer-owned path uses the audited `No
|
||||
pnpm install # registers the workspace
|
||||
pnpm run doc-sync
|
||||
pnpm run constraints && pnpm run typecheck && pnpm run lint
|
||||
pnpm run test:coverage # 100% per-file over src (types.ts exempt)
|
||||
pnpm run build && pnpm run hygiene
|
||||
```
|
||||
|
||||
Test expectations: every registry/registration needs an HMR-safety test (register from a child fiber, dispose it, assert cleanup). Excessive tests are welcome — see [docs/testing.md](../testing.md).
|
||||
Follow the [repository testing policy](../testing.md) for the behavior-specific checks and coverage required by the new package.
|
||||
|
||||
@@ -14,7 +14,6 @@ packages/<group>/<pkg>/
|
||||
# ../../../vendor/cordis (+ ../../../vendor/schemastery if
|
||||
# you use Config, + ../../<group>/<dep> for each dsh dep)
|
||||
src/index.ts # service default export or plugin (name/inject/apply/Config)
|
||||
tests/<x>.spec.ts
|
||||
README.md # service API, events, extension points, design notes,
|
||||
# + gated Model Experience context blocks or short form
|
||||
# + the gated "Known Limitations and Deferred Work" section
|
||||
@@ -23,7 +22,7 @@ packages/<group>/<pkg>/
|
||||
|
||||
当已有分组与包的角色匹配时,选择该分组(`core`、`llm`、`bash`、`compact`、`subagent`、`todo`、`session-persistence`、`ui`、`util` 或 `support`)。允许新建分组,但分组只是纯容器:没有 `package.json`,没有源文件,包仍然恰好位于其下一层。
|
||||
|
||||
package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-constraints.ts` 强制执行):`private: true`,`version` 与根 `package.json` 一致,`type: module`,`main: "lib/index.js"`,`types: "lib/types/index.d.ts"`,`exports["."].types: "./lib/types/index.d.ts"`,`exports["."].default: "./lib/index.js"`,`cordis` 同时出现在 peerDependencies 和 devDependencies 中(相同范围)。每个 dsh 对等依赖(peer dependency)都要在 devDependencies 中镜像。`schemastery` 放在 `dependencies` 中(它是运行时校验器),与 agent-loop 保持一致。`files` 列表要精确:`lib/index.js`、`lib/types/**/*.d.ts`、`lib/types/**/*.d.ts.map` 和 `src`;不要发布 `lib/types` 下的 JS 或 JS-map 中间产物,也不要发布陈旧的根声明文件。带有 `bin` 的 CLI 应用包在 `files` 中将 `lib/bin.js` 紧跟在 `lib/index.js` 之后。
|
||||
package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-constraints.ts` 强制执行):`private: true`,`version` 与根 `package.json` 一致,`type: module`,`main: "lib/index.js"`,`types: "lib/types/index.d.ts"`,`exports["."].types: "./lib/types/index.d.ts"`,`exports["."].default: "./lib/index.js"`,`cordis` 同时出现在 peerDependencies 和 devDependencies 中(相同范围)。每个 dsh 对等依赖(peer dependency)都要在 devDependencies 中镜像。`schemastery` 放在 `dependencies` 中(它是运行时校验器),与 agent-loop 保持一致。`files` 列表精确包含 `lib/index.js`、`lib/invariant.js`、`lib/types/**/*.d.ts` 以及门禁认可的包专用运行时产物;如果包的运行时 export 指向输出树,还要包含 `lib/types/**/*.js`。不要发布 `src`、声明映射、JS map 或陈旧的根声明文件。带有 `bin` 的 CLI 应用包在 `files` 中将 `lib/bin.js` 紧跟在 `lib/index.js` 之后。
|
||||
|
||||
包内的相对导入在源码中使用显式 `.ts` 后缀(例如 `export * from './types.ts'`)。编译器在输出的 JS 中将其重写为 `.js`,在声明文件中保留显式 `.ts` 后缀;标准的 NodeNext/Node16 TypeScript 消费方会将其解析到同目录的 `.d.ts` 文件。
|
||||
|
||||
@@ -33,11 +32,11 @@ package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-c
|
||||
|---|---|
|
||||
| `tsconfig.base.json` | 已有分组无需编辑;新分组需为 `@deepseek-ai/dsh-*` 通配符添加 `./packages/<group>/*/src` 候选路径 |
|
||||
| `tsconfig.host.json`(host 侧包)或 `tsconfig.client.json`(client 侧包) | 在 `references` 中添加 `{ "path": "./packages/<group>/<pkg>" }`——恰好一个聚合,绝不两个都加([布局](../development.md#typescript-project-layout)) |
|
||||
| `knip.json` | 仅当包有非 `*.spec.ts` 入口时需要(如 `*.e2e.ts` → 添加 per-workspace override,参照 `packages/llm/llm-deepseek`) |
|
||||
| `knip.json` | 仅当包有仓库发现机制尚未覆盖的入口时需要 |
|
||||
|
||||
`packages/client/*` 包改为 extends `tsconfig.base.client.json`(而非 `tsconfig.base.json`);client 插件包还需在 package.json 声明 `dshClient`、导出 `./client`、调用共享 tsdown preset(`packages/client/tsdown.client.ts`)——client 侧见 [packages/client/AGENTS.md](../../packages/client/AGENTS.md)。
|
||||
|
||||
以下内容由 glob 或包 manifest 发现机制自动覆盖,无需手动编辑:根 `package.json` workspaces、`scripts/publint-all.ts`、`tsdown.config.ts`、`vitest.config.ts`、`eslint.config.mjs`、`scripts/check-workspace-constraints.ts`。
|
||||
以下内容由 glob 或包 manifest 发现机制自动覆盖,无需手动编辑:根 `package.json` workspaces、`scripts/publint-all.ts`、`tsdown.config.ts`、`.oxlintrc.json`、`scripts/check-workspace-constraints.ts`。
|
||||
|
||||
## 3. 确定包拓扑
|
||||
|
||||
@@ -85,8 +84,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex
|
||||
pnpm install # registers the workspace
|
||||
pnpm run doc-sync
|
||||
pnpm run constraints && pnpm run typecheck && pnpm run lint
|
||||
pnpm run test:coverage # 100% per-file over src (types.ts exempt)
|
||||
pnpm run build && pnpm run hygiene
|
||||
```
|
||||
|
||||
测试要求:每个注册表/注册操作都需要一个 HMR(热模块替换)安全测试(从子 fiber 注册,dispose(资源释放)它,断言清理完成)。鼓励编写充分的测试——见 [docs/testing.md](../testing.md)。
|
||||
请遵循[仓库测试政策](../testing.md),为新包运行行为所需的专项检查并达到相应覆盖率。
|
||||
|
||||
@@ -1,6 +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
|
||||
adding-a-tool.md: d06e3d8e3c7da1f71a55bf9c4f56cd4b2cc03697
|
||||
adding-a-tool.zh.md: 53f608eba3b26b124f873990fa13ce1572c0baf2
|
||||
# pnpm run verify-translation-pairing --write docs/cookbook/adding-a-tool.md
|
||||
adding-a-tool.md: cb418a9118901cc6572fb17125351bdda922434b
|
||||
adding-a-tool.zh.md: d8b73a51d1dde8647e7a032648f8a6344fcc83eb
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Cookbook: adding a tool
|
||||
# Tool authoring reference
|
||||
|
||||
English | [中文](adding-a-tool.zh.md)
|
||||
|
||||
How to give the model a new capability. The minimal shape below shows the contract; `packages/bash/tool-bash` is the production-grade three-package seam.
|
||||
Reference for the contracts a model-facing tool must satisfy. For an ordered first tool, follow [Build a tool](../user/develop/basic/tool.md). `packages/bash/tool-bash` is the production-grade three-package example.
|
||||
|
||||
## The minimal shape
|
||||
|
||||
@@ -35,7 +35,7 @@ export function apply(ctx: Context) {
|
||||
}
|
||||
```
|
||||
|
||||
Registration is effect-based: disposing the plugin fiber unregisters the tool (write the HMR test). Schemas flow into the system-prompt assembly automatically.
|
||||
Registration is effect-based: disposing the plugin fiber unregisters the tool. Schemas flow into the system-prompt assembly automatically.
|
||||
|
||||
## Rules of the execute() contract
|
||||
|
||||
@@ -78,6 +78,8 @@ Both methods return a **`card`-tagged render intent** — pick the card kind tha
|
||||
- `generic` supplies an optional title and content.
|
||||
- `terminal` supplies raw output and optional exit metadata; each UI renders its capable or fallback view.
|
||||
- `diff` supplies applied hunks, often derived by `output.presentationMeta` and carried in persisted `result.meta` so replay reproduces them. Mutation tools keep a diff result because the completed view replaces the pending card.
|
||||
- `search` supplies a discovery result reconstructed from persisted `result.meta`: grouped-by-file matches (`shape: 'matches'`, grep) or a flat path list (`shape: 'paths'`, glob), plus `truncated`/`total` so a UI never presents a capped result as complete. The view carries no result text (a UI without a search card falls back to the raw result content), and there is no `search` call view — a discovery call's pending state stays a generic card, since matches exist only after `execute`. (tool-fs-search `grep`/`glob`.)
|
||||
- `web` supplies a completed web retrieval, discriminated by `kind: 'search' | 'fetch'` (the structured search sources or the fetch summary), derived from `result.meta`; it carries no body copy, so a UI without the `web` capability falls back to the raw result content. (tool-web `web_search`/`web_fetch`.)
|
||||
|
||||
Hard rules (they bite if broken):
|
||||
|
||||
@@ -85,8 +87,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 belongs in the canonical value or Native content merely to serve a UI. `output.render` owns model-facing prose; `presentationMeta` plus the card presenters own replayable UI state. A `terminal` result view carries raw output and the adapter adds any fallback framing.
|
||||
- **`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`; tools never import a UI or transport type. The TUI and host/client runtime map each `card` into their own view. 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.
|
||||
The neutral vocabulary lives in `dsh-tools`; tools never import a UI or transport type. Host/client runtimes map each `card` into their own view. 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
|
||||
## Verification
|
||||
|
||||
Cover argument rejection, every canonical value and Native rendering shape, output-schema rejection, 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 projected `tool/result` session events; prove the canonical value itself is not persisted. For a UI card, assert the exact `presentCall` and `presentResult` views and exercise the owning TUI or host/client projection. Add an assembled snapshot for the shipped model or UI behavior the tool changes.
|
||||
Follow the [repository testing policy](../testing.md) and the owning package's test documentation. A shipped model- or UI-visible change requires the assembled coverage specified there.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 实操手册:添加工具
|
||||
# 工具编写参考
|
||||
|
||||
[English](adding-a-tool.md) | 中文
|
||||
|
||||
如何为模型赋予一项新能力。下文的最小形态展示这项契约;`packages/bash/tool-bash` 是生产级、由三个包(package)构成的 seam。
|
||||
面向模型的工具必须满足哪些契约,均以本文为准。如需按步骤构建第一个工具,请阅读[构建工具](../user/develop/basic/tool.md)。`packages/bash/tool-bash` 是生产级的三包示例。
|
||||
|
||||
## 最小形态
|
||||
|
||||
@@ -35,7 +35,7 @@ export function apply(ctx: Context) {
|
||||
}
|
||||
```
|
||||
|
||||
注册基于副作用:dispose(资源释放)插件 fiber 即注销该工具(请编写 HMR(热模块替换)测试)。schema 会自动流入系统提示词的组装过程。
|
||||
注册基于副作用:dispose(资源释放)插件 fiber 即注销该工具。schema 会自动流入系统提示词的组装过程。
|
||||
|
||||
## execute() 契约的规则
|
||||
|
||||
@@ -78,6 +78,8 @@ producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的
|
||||
- `generic` 提供可选的标题和内容。
|
||||
- `terminal` 提供原始输出和可选的退出元数据;各 UI 根据自身能力渲染对应视图或回退视图。
|
||||
- `diff` 提供已应用的 hunk,通常由 `output.presentationMeta` 派生并通过持久化的 `result.meta` 携带,使回放能重现它们。变更类工具保留 diff 结果,因为完成后的视图会替换 pending 卡片。
|
||||
- `search` 提供从持久化 `result.meta` 重建的发现型结果:按文件分组的匹配(`shape: 'matches'`,grep)或扁平路径列表(`shape: 'paths'`,glob),外加 `truncated`/`total` 使 UI 永不把被截断的结果当作完整结果呈现。该视图不携带结果文本(无 search 卡片的 UI 回退到原始结果内容),也没有 `search` 调用视图——发现型调用的 pending 状态保持为 generic 卡片,因为匹配只在 `execute` 之后才存在。(tool-fs-search 的 `grep`/`glob`。)
|
||||
- `web` 提供已完成的 web 检索,以 `kind: 'search' | 'fetch'` 区分(结构化的搜索来源或抓取摘要),由 `result.meta` 派生;它不携带正文副本,因此不具备 `web` 能力的 UI 回退到原始结果内容。(tool-web `web_search`/`web_fetch`。)
|
||||
|
||||
硬性规则(违反会出问题):
|
||||
|
||||
@@ -85,8 +87,8 @@ producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的
|
||||
- **UI 格式不进入模型结果。** 围栏 ` ```console ` 块、diff、相对化路径均不应仅为服务 UI 而进入规范值或 Native 内容。`output.render` 负责模型可见的自然语言;`presentationMeta` 和卡片展示器负责可回放的 UI 状态。`terminal` 结果视图携带原始输出,由适配器按需添加回退格式。
|
||||
- **`defineTool` 对展示路径做软校验。** 格式错误或旧版日志中的 arg 形态会使包装器返回 `undefined`(通用回退)而非抛异常——展示绝不能导致回放崩溃。
|
||||
|
||||
中性词汇定义在 `dsh-tools` 中;工具绝不导入 UI 或传输类型。TUI 和 host/client 运行时将每个 `card` 映射到各自的视图。设计与原因见[渲染意图联合体 Agent Note](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md);`dsh-tool-fs`(generic/diff)和 `dsh-tool-bash`(terminal)是参考实现。
|
||||
中性词汇定义在 `dsh-tools` 中;工具绝不导入 UI 或传输类型。host/client 运行时将每个 `card` 映射到各自的视图。设计与原因见[渲染意图联合体 Agent Note](../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md);`dsh-tool-fs`(generic/diff)和 `dsh-tool-bash`(terminal)是参考实现。
|
||||
|
||||
## 每个工具必须的测试
|
||||
## 验证
|
||||
|
||||
覆盖参数拒绝、每种规范值和 Native 渲染形态、输出 schema 拒绝以及 HMR dispose。对于有副作用的工具,使用脚本化的 `MockAdapter` 驱动真实工具通过 agent loop(智能体循环),并断言其 `tool/call` 和投影后的 `tool/result` 会话事件;同时证明规范值本身未被持久化。对于 UI 卡片,断言 `presentCall` 和 `presentResult` 的精确视图,并实际运行所属 TUI 或 host/client 投影。如果工具改变了已交付的模型或 UI 行为,请添加组装应用快照。
|
||||
遵循[仓库测试策略](../testing.md)和所属包的测试文档。已交付且面向模型或 UI 的变更必须提供其中规定的组装覆盖。
|
||||
|
||||
@@ -1,6 +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
|
||||
adding-a-vendored-package.md: 71ca9fccc9418348784dbb6668127242e4fb45d2
|
||||
adding-a-vendored-package.zh.md: c340630aebeda0ec293a835cdfc8d15d71cd7801
|
||||
# pnpm run verify-translation-pairing --write docs/cookbook/adding-a-vendored-package.md
|
||||
adding-a-vendored-package.md: b85d74a3a09b27254883b88cb8e6587e32ed811c
|
||||
adding-a-vendored-package.zh.md: 2927837a28d1e7b593090d581522f69f84504806
|
||||
|
||||
@@ -42,7 +42,7 @@ Local relative imports/exports in vendored TypeScript source use explicit `.ts`
|
||||
| `vendor/README.md` | add a manifest table row (dir, npm name, version, upstream repo, commit SHA) and log any local modifications |
|
||||
| `scripts/publint-all.ts` | only if the vendored package is itself published from here (vendored deps normally are not — skip) |
|
||||
|
||||
Covered automatically by globs — no edits needed: root `package.json` workspaces (`vendor/*`), `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`. A per-package `vendor/<dir>/tsdown.config.ts` is needed ONLY if the build shape diverges from the root default (dual ESM/CJS or multiple entries — see `vendor/schemastery` and `vendor/logger-console`); its entry should read the JS emitted under `lib/types`.
|
||||
Covered automatically by globs — no edits needed: root `package.json` workspaces (`vendor/*`), `tsdown.config.ts`, `vitest.config.ts`, `.oxlintrc.json`. A per-package `vendor/<dir>/tsdown.config.ts` is needed ONLY if the build shape diverges from the root default (dual ESM/CJS or multiple entries — see `vendor/schemastery` and `vendor/logger-console`); its entry should read the JS emitted under `lib/types`.
|
||||
|
||||
## 3. Mind the manifest guard
|
||||
|
||||
@@ -53,7 +53,7 @@ Covered automatically by globs — no edits needed: root `package.json` workspac
|
||||
```sh
|
||||
pnpm install # registers the workspace
|
||||
pnpm run typecheck
|
||||
pnpm run build && pnpm run test && pnpm run constraints
|
||||
pnpm run build && pnpm run constraints
|
||||
```
|
||||
|
||||
The source `paths` map lives once in `tsconfig.base.json` and serves every graph. The important isolation boundary is the project-reference graph: vendored source must be referenced through its own `vendor/<dir>/tsconfig.json`, not pulled into an aggregate's strict program ([layout](../development.md#typescript-project-layout)).
|
||||
Run the behavior checks selected by the [testing policy](../testing.md). The source `paths` map lives once in `tsconfig.base.json` and serves every graph. The important isolation boundary is the project-reference graph: vendored source must be referenced through its own `vendor/<dir>/tsconfig.json`, not pulled into an aggregate's strict program ([layout](../development.md#typescript-project-layout)).
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 实操手册:添加一个 vendored 包(package)
|
||||
# 实操手册:添加一个 vendored 包
|
||||
|
||||
[English](adding-a-vendored-package.md) | 中文
|
||||
|
||||
当 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 集合验证;如有偏差,请在此修正。)
|
||||
当 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. 复制源码
|
||||
|
||||
@@ -42,7 +42,7 @@ vendored TypeScript 源码中的本地相对导入/导出在复制后使用显
|
||||
| `vendor/README.md` | 添加一行 manifest 表格行(dir、npm name、version、upstream repo、commit SHA)并记录所有本地修改 |
|
||||
| `scripts/publint-all.ts` | 仅当该 vendored 包本身从此仓库发布时才需要(vendored 依赖通常不发布——跳过) |
|
||||
|
||||
以下由 glob 自动覆盖,无需手动编辑:根 `package.json` 的 workspaces(`vendor/*`)、`tsdown.config.ts`、`vitest.config.ts`、`eslint.config.mjs`。只有当构建形态偏离根默认值时(双 ESM/CJS 或多入口——参见 `vendor/schemastery` 和 `vendor/logger-console`),才需要单独的 `vendor/<dir>/tsdown.config.ts`;其入口应读取 `lib/types` 下输出的 JS。
|
||||
以下由 glob 自动覆盖,无需手动编辑:根 `package.json` 的 workspaces(`vendor/*`)、`tsdown.config.ts`、`vitest.config.ts`、`.oxlintrc.json`。只有当构建形态偏离根默认值时(双 ESM/CJS 或多入口——参见 `vendor/schemastery` 和 `vendor/logger-console`),才需要单独的 `vendor/<dir>/tsdown.config.ts`;其入口应读取 `lib/types` 下输出的 JS。
|
||||
|
||||
## 3. 注意 manifest 守卫
|
||||
|
||||
@@ -53,7 +53,7 @@ vendored TypeScript 源码中的本地相对导入/导出在复制后使用显
|
||||
```sh
|
||||
pnpm install # registers the workspace
|
||||
pnpm run typecheck
|
||||
pnpm run build && pnpm run test && pnpm run constraints
|
||||
pnpm run build && pnpm run constraints
|
||||
```
|
||||
|
||||
源码 `paths` 映射只在 `tsconfig.base.json` 存在一份,服务所有图。重要的隔离边界是 project-reference 图:vendored 源码必须通过其自身的 `vendor/<dir>/tsconfig.json` 被引用,而非被拉入某个聚合的严格程序中([布局](../development.md#typescript-project-layout))。
|
||||
请运行[测试政策](../testing.md)所选择的行为检查。源码 `paths` 映射只在 `tsconfig.base.json` 存在一份,服务所有图。重要的隔离边界是 project-reference 图:vendored 源码必须通过其自身的 `vendor/<dir>/tsconfig.json` 被引用,而非被拉入某个聚合的严格程序中([布局](../development.md#typescript-project-layout))。
|
||||
|
||||
@@ -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 docs/cookbook/adding-an-llm-adapter.md
|
||||
adding-an-llm-adapter.md: a7f9dced70041653a0cb815147a07b6386d79e3e
|
||||
adding-an-llm-adapter.zh.md: 3515927585201326b713bb03cd863886ce7846bd
|
||||
adding-an-llm-adapter.md: 4fcc646ed2eea8a6170027d01761887aa28b0045
|
||||
adding-an-llm-adapter.zh.md: 35a671416f8160a6187a06f3dbd614dfe4faa778
|
||||
|
||||
@@ -34,13 +34,10 @@ Registration is effect-based (HMR-safe); one adapter per provider route — dupl
|
||||
|
||||
Provider-specific thinking-mode toggles remain in the adapter's Config. Exact model metadata uses one provider-neutral capability seam: implement `resolveModel()` with provider/model identity and optional `context` and `reasoning` fields, declare a configured `defaultEffort` only when one exists, and honor the resolver's optional `AbortSignal`. Reasoning efforts are ordered opaque ids mapped to provider requests by the adapter. Preserve the adapter's authoritative selectable list, including an adapter-defined `off` when supported, without exposing final wire spellings or clamping unsupported values; an id need not equal its wire representation.
|
||||
|
||||
## Structure that worked
|
||||
## Implementation structure
|
||||
|
||||
Split the adapter into testable stages (llm-deepseek's layout): wire types (`types.ts`, coverage-exempt) → request serializer → SSE/transport parser → chunk-translation state machine → a thin adapter class wiring them. Each stage gets its own unit suite.
|
||||
Keep wire types, request serialization, transport parsing, chunk translation, and the adapter class as separate responsibilities; [`llm-deepseek`](../../packages/llm/llm-deepseek/README.md) is the reference layout.
|
||||
|
||||
## Testing
|
||||
## Verification
|
||||
|
||||
- **Unit: mock the provider, not the harness.** A scripted `node:http` server speaking the provider's wire format covers happy paths, every error status, malformed payloads, premature closes, and aborts — no network, and it drives the 100% per-file coverage gate. Works for SDK-backed adapters too (point the SDK's baseURL at the mock).
|
||||
- **Hostile framing tests.** Split stream payloads at arbitrary byte positions (including mid-UTF-8) — real networks do.
|
||||
- **E2E: `tests/*.e2e.ts`** under `pnpm run test:e2e`, gated with `describe.skipIf(!process.env.MY_KEY)` so CI (no secrets) stays green. Cover representative model/provider/API families and every provider mode you map, a tool-call round trip INCLUDING the follow-up turn with results in history, and loose assertions only (substring/structure, bounded maxTokens — real models are nondeterministic).
|
||||
- Register the e2e file pattern in `knip.json` (per-workspace `entry` override) or knip flags it unused.
|
||||
Follow the [repository testing policy](../testing.md), which owns adapter coverage, real-provider checks, and published-entry requirements.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 实操手册:添加 LLM 适配器
|
||||
# 实操手册:添加 LLM(大语言模型)适配器
|
||||
|
||||
[English](adding-an-llm-adapter.md) | 中文
|
||||
|
||||
如何接入一个新的模型提供方。参考实现:`packages/llm/llm-deepseek`(直接 HTTP,SSE 由 `eventsource-parser` 分帧)与 `packages/llm/llm-pi-ai`(封装 LLM 库)。请先阅读 `packages/llm/llm/src/types.ts` 中的 `StreamChunk` 文档——它记录了两个适配器都经过验证的协议约定。
|
||||
如何接入一个新的模型提供方。参考实现:`packages/llm/llm-deepseek`(直接 HTTP,SSE(Server-Sent Events)由 `eventsource-parser` 分帧)与 `packages/llm/llm-pi-ai`(封装 LLM 库)。请先阅读 `packages/llm/llm/src/types.ts` 中的 `StreamChunk` 文档——它记录了两个适配器都经过验证的协议约定。
|
||||
|
||||
## 基本形态
|
||||
|
||||
@@ -20,7 +20,7 @@ export function apply(ctx: Context, config: Config) {
|
||||
}
|
||||
```
|
||||
|
||||
注册基于副作用(HMR 安全);每个提供方路由仅对应一个适配器,重复注册会抛出异常,多路由注册要么全部成功,要么全部失败。`options.provider` 用于选择适配器,`options.model` 是提供方模型 ID,因此动态模型目录适配器无需重新配置生命周期即可提供新模型。密钥采用 Cordis 原生方式管理:schemastery Config 带环境变量回退,通过 cordis.yml 的 `!!js process.env.MY_KEY` 注入。代码中禁止临时读取密钥文件。
|
||||
注册基于副作用,可安全支持 HMR(热模块替换);每个提供方路由仅对应一个适配器,重复注册会抛出异常,多路由注册要么全部成功,要么全部失败。`options.provider` 用于选择适配器,`options.model` 是提供方模型 ID,因此动态模型目录适配器无需重新配置生命周期即可提供新模型。密钥采用 Cordis 原生方式管理:schemastery Config 带环境变量回退,通过 cordis.yml 的 `!!js process.env.MY_KEY` 注入。切勿在代码中读取自行约定的密钥文件。
|
||||
|
||||
## 协议义务(两个实现共同验证的契约)
|
||||
|
||||
@@ -32,15 +32,12 @@ export function apply(ctx: Context, config: Config) {
|
||||
- 如果 `GenerateOptions` 中某个字段你的提供方无法支持(例如提供方不支持 stop sequences 时收到 `stop` 列表):抛出 `LlmError(..., 'UNSUPPORTED')`,而非静默丢弃。
|
||||
- 如果提供方在后续调用中需要响应 ID、签名或其他原生元数据,请将其最小无损 JSON 投影作为 `finish.replayState` 发出。重建历史时验证该状态。只有历史提供方路由和目标提供方路由当前由完全相同的适配器实例拥有时,`LlmService` 才会传递该状态;由适配器决定同模型、跨模型或跨提供方恢复是否合法。状态缺失时,切勿仅根据提供方/模型名称推断原生回放。
|
||||
|
||||
提供方特有的 thinking 模式开关仍放在适配器的 Config 中。确切模型元数据使用一处提供方无关的能力 seam:实现 `resolveModel()`,返回提供方/模型身份以及可选的 `context` 和 `reasoning` 字段;仅当存在配置指定的默认值时才声明 `defaultEffort`;响应传给解析器的可选 `AbortSignal`。推理强度是由适配器映射到提供方请求的有序不透明 ID。请保留适配器给出的权威可选列表,包括适配器在支持时定义的 `off`;不得暴露最终协议值的具体拼写,也不得自动调整不支持的值。ID 无需与其协议表示相同。
|
||||
提供方特有的思考模式开关仍放在适配器的 Config 中。确切模型元数据使用一处提供方无关的能力 seam:实现 `resolveModel()`,返回提供方/模型身份以及可选的 `context` 和 `reasoning` 字段;仅当存在配置指定的默认值时才声明 `defaultEffort`;遵守解析模型时传入的可选 `AbortSignal`。推理(reasoning)强度是由适配器映射到提供方请求的有序不透明 ID。请保留适配器给出的权威可选列表,包括适配器在支持时定义的 `off`;不得暴露最终协议值的具体拼写,也不得自动调整不支持的值。ID 无需与其协议表示相同。
|
||||
|
||||
## 经验证有效的结构
|
||||
## 实现结构
|
||||
|
||||
将适配器拆分为可测试的阶段(llm-deepseek 的布局):协议格式(wire format)类型(`types.ts`,豁免覆盖率)→ 请求序列化器 → SSE/传输解析器 → 分片转换状态机 → 一个将它们串联的薄适配器类。每个阶段配备独立的单元测试套件。
|
||||
让协议类型、请求序列化、传输解析、分片转换和适配器类分别承担独立职责;[`llm-deepseek`](../../packages/llm/llm-deepseek/README.md) 是参考布局。
|
||||
|
||||
## 测试
|
||||
## 验证
|
||||
|
||||
- **单元测试:mock 提供方,而非 harness。** 用脚本化的 `node:http` 服务器模拟提供方的协议格式,覆盖正常路径、所有错误状态码、畸形载荷、连接提前关闭和中止——无需网络,且能满足 100% 逐文件覆盖率门禁。对基于 SDK 的适配器同样适用(将 SDK 的 baseURL 指向 mock 服务器)。
|
||||
- **恶意分帧测试。** 在任意字节位置(包括 UTF-8 字符中间)切割流载荷——真实网络环境正是如此。
|
||||
- **E2E:`tests/*.e2e.ts`**,通过 `pnpm run test:e2e` 运行,以 `describe.skipIf(!process.env.MY_KEY)` 守卫,确保无密钥的 CI 保持绿色。覆盖具有代表性的模型/提供方/API 系列以及你映射的每种提供方模式、一次包含后续轮次(历史中带工具结果)的工具调用往返,以及仅做宽松断言(子串/结构匹配、有界的 maxTokens——真实模型是非确定性的)。
|
||||
- 在 `knip.json` 中注册 e2e 文件模式(per-workspace `entry` 覆盖),否则 knip 会将其标记为未使用。
|
||||
遵循[仓库测试策略](../testing.md),该策略负责适配器覆盖、真实提供方检查和已发布入口要求。
|
||||
|
||||
@@ -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 docs/cookbook/extension-cookbook.md
|
||||
extension-cookbook.md: 36ab56dcdce1166ef69cec7834f6c17be72d89c3
|
||||
extension-cookbook.zh.md: 8c8f9486ec592fcc80f1053f54adce2e33798d4b
|
||||
extension-cookbook.md: 1d5705672396d66d5625567aefec5006ae126e67
|
||||
extension-cookbook.zh.md: 2f8e0ccb745baefb857f1065040f3225142d264f
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
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).
|
||||
Reference shapes for the harness extension surface. The snippets omit imports and helper implementations and are not copy-paste-complete. For concrete authoring paths, see the [package checklist](adding-a-package.md), [first-tool tutorial](../user/develop/basic/tool.md), [tool reference](adding-a-tool.md), and [LLM adapter guide](adding-an-llm-adapter.md); the [architecture](../architecture.md) owns the system and extension-seam map.
|
||||
|
||||
## A tool plugin
|
||||
|
||||
@@ -64,7 +62,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
## An external protocol driver
|
||||
|
||||
A *protocol driver* adapts a wire peer to `ctx.agents`; it may serve a UI or an automation client. A stdio driver owns stdout, creates or resumes agents through the factory, maps the protocol's requests to `followup()` or `cancel()`, and settles each request exactly once from durable `turn/end`. Tear agents down with `AgentHandle.dispose()` so disposal reaches quiescence.
|
||||
A *protocol driver* adapts a wire peer to `ctx.agents`; it may serve a UI or an automation client. A stdio driver owns stdout, creates or resumes agents through the factory, and maps protocol requests to `followup()` or `cancel()`. A low-level prompt request returns its durable enqueue receipt; it does not acquire a result by correlating `MessageId` with `turn/end`. Publish whole-agent status separately. An automation method may wait from its receipt through the next idle and summarize that explicitly owned interval, while a UI normally keeps observing the open-ended event stream. Tear agents down with `AgentHandle.dispose()` so disposal reaches quiescence.
|
||||
|
||||
[`packages/acp/acp`](../../packages/acp/acp) is the automation-only worked example: it exposes fresh text sessions over Agent Client Protocol JSON-RPC stdio, emits committed assistant text, and registers a one-shot machine permission answerer for agents it owns. Its [README](../../packages/acp/acp/README.md) owns the exact method and lifecycle contract.
|
||||
|
||||
@@ -84,14 +82,15 @@ export function apply(ctx: Context) {
|
||||
}
|
||||
}
|
||||
})
|
||||
// Inbound "prompt": create/resume an agent and feed it; settle on turn end.
|
||||
// Inbound "prompt": create/resume an agent, feed it, and return its enqueue receipt.
|
||||
// Whole-agent status is a separate notification; no turn end belongs to this prompt.
|
||||
// Teardown reaches quiescence via AgentHandle.dispose() (stop + await exit).
|
||||
}
|
||||
```
|
||||
|
||||
## Runnable wirings
|
||||
|
||||
Runnable leaves load their plugin trees from `examples/*/cordis.yml`; the root `demo:*` scripts and those leaf directories are the authoritative inventory. Interactive leaves use [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo), non-interactive leaves use [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo), ACP leaves use [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), and the app packages share [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo).
|
||||
Runnable leaves load their plugin trees from `examples/*/cordis.yml`; the root `demo:*` scripts and those leaf directories are the authoritative inventory. Non-interactive leaves use [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo), ACP leaves use [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo), JSON-RPC leaves use [`@deepseek-ai/dsh-jsonrpc-demo`](../../packages/examples/jsonrpc-demo), and the app packages share [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo).
|
||||
|
||||
## The feature → mechanism map
|
||||
|
||||
@@ -101,12 +100,12 @@ Every product feature maps to a listener on a documented extension seam — the
|
||||
|
||||
| Product feature | Plugin mechanism |
|
||||
|---|---|
|
||||
| Hook system (user + project level) | listeners on `agent/session-start`, `agent/prompt-submit`, `agent/request`, `tools/pre-execute`, `tools/post-execute`, and `agent/turn-stopping`; the waterfall seams return typed decisions, while `agent/turn-stopping` may steer another step; the `dsh-hooks-claude` / `dsh-hooks-codex` bridges map hook config files onto these seams |
|
||||
| Hook system (user + project level) | listeners on `agent/session-start`, `agent/pre-step`, `agent/request`, `tools/pre-execute`, `tools/post-execute`, and `agent/turn-stopping`; the waterfall seams return typed decisions, while `agent/turn-stopping` may steer another step; the `dsh-hooks-claude` / `dsh-hooks-codex` bridges map hook config files onto these seams |
|
||||
| `/goal` | `ctx.goals` owns durable state, `dsh-goal-session` schedules same-session rounds through the public `Agent`, and separate command/tool producers expose human/model control |
|
||||
| `/loop` | on the `turn/end` session event, `followup()` the next iteration; or force-continue |
|
||||
| Dynamic workflow | `ctx.workflows` + the worker-thread engine + the `workflow` tool; structured in-process children enforce output with scoped prompt/tool registrations, a monotonic tool guard, final `tools/result` commit (including enclosing `run_code`), and the structured-output execution's monotonic `concludeTurn()` marker |
|
||||
| Queued + steering messages | core `Agent.followup()` / `Agent.steer()` |
|
||||
| Context compaction (auto + manual) | the `ctx.compact` seam + `dsh-compact-basic`; automatic pressure runs on serial `agent/step`, canonical overflow recovery runs on `agent/request-error`, and manual callers use the same compact service ([compaction Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) — the model-facing `/compact` consumer tool is deferred) |
|
||||
| Context compaction (auto + manual) | the `ctx.compact` seam + `dsh-compact-basic`; automatic pressure runs on serial `agent/pre-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 |
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
[English](extension-cookbook.md) | 中文
|
||||
|
||||
> 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 和辅助实现,无法直接复制运行。具体编写路径见[包检查清单](adding-a-package.md)、[第一个工具教程](../user/develop/basic/tool.md)、[工具参考](adding-a-tool.md)和 [LLM(大语言模型)适配器指南](adding-an-llm-adapter.md);系统与扩展 seam 映射由[架构文档](../architecture.md)负责。
|
||||
|
||||
## 工具插件
|
||||
|
||||
@@ -64,7 +62,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
## 外部协议驱动
|
||||
|
||||
*协议驱动*将协议对端接入 `ctx.agents`;它可以服务于 UI 或自动化客户端。stdio 驱动拥有 stdout,通过工厂创建或恢复 agent(智能体),将协议请求映射为 `followup()` 或 `cancel()`,并根据持久的 `turn/end` 对每个请求恰好结算一次。通过 `AgentHandle.dispose()` 拆除 agent,以使 dispose(资源释放)达到完全停稳。
|
||||
*协议驱动*将协议对端接入 `ctx.agents`;它可以服务于 UI 或自动化客户端。stdio 驱动拥有 stdout,通过工厂创建或恢复 agent(智能体),并将协议请求映射为 `followup()` 或 `cancel()`。底层提示词请求返回其持久入队回执;它不会通过关联 `MessageId` 与 `turn/end` 获得结果。整个 agent 的状态应单独发布。自动化方法可以从回执等待到下一次 idle,并概括这一显式拥有的区间;UI 通常则会持续观察开放式事件流。通过 `AgentHandle.dispose()` 拆除 agent,以使 dispose(资源释放)达到完全停稳。
|
||||
|
||||
[`packages/acp/acp`](../../packages/acp/acp) 是仅面向自动化的完整示例:它通过 ACP(Agent Client Protocol)JSON-RPC stdio 提供全新文本会话,发出已提交的助手文本,并为其拥有的 agent 注册一次性机器权限应答器。其 [README](../../packages/acp/acp/README.md) 拥有精确的方法和生命周期契约。
|
||||
|
||||
@@ -84,14 +82,15 @@ export function apply(ctx: Context) {
|
||||
}
|
||||
}
|
||||
})
|
||||
// Inbound "prompt": create/resume an agent and feed it; settle on turn end.
|
||||
// Inbound "prompt": create/resume an agent, feed it, and return its enqueue receipt.
|
||||
// Whole-agent status is a separate notification; no turn end belongs to this prompt.
|
||||
// Teardown reaches quiescence via AgentHandle.dispose() (stop + await exit).
|
||||
}
|
||||
```
|
||||
|
||||
## 可运行的组装示例
|
||||
|
||||
可运行叶子从 `examples/*/cordis.yml` 加载各自的插件树;根目录的 `demo:*` 脚本和这些叶子目录是权威清单。交互式叶子使用 [`@deepseek-ai/dsh-tui-demo`](../../packages/examples/tui-demo),非交互式叶子使用 [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo),ACP 叶子使用 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),应用包共享 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo)。
|
||||
可运行叶子从 `examples/*/cordis.yml` 加载各自的插件树;根目录的 `demo:*` 脚本和这些叶子目录是权威清单。非交互式叶子使用 [`@deepseek-ai/dsh-cli-demo`](../../packages/examples/cli-demo),ACP 叶子使用 [`@deepseek-ai/dsh-acp-demo`](../../packages/examples/acp-demo),JSON-RPC 叶子使用 [`@deepseek-ai/dsh-jsonrpc-demo`](../../packages/examples/jsonrpc-demo),应用包共享 [`@deepseek-ai/dsh-agent-spine-demo`](../../packages/examples/agent-spine-demo)。
|
||||
|
||||
## 功能→机制映射
|
||||
|
||||
@@ -101,12 +100,12 @@ export function apply(ctx: Context) {
|
||||
|
||||
| 产品功能 | 插件机制 |
|
||||
|---|---|
|
||||
| 钩子系统(用户级 + 项目级) | `agent/session-start`、`agent/prompt-submit`、`agent/request`、`tools/pre-execute`、`tools/post-execute` 和 `agent/turn-stopping` 上的监听器;waterfall seam 返回类型化决策,`agent/turn-stopping` 则可通过 steering 触发下一步;`dsh-hooks-claude` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些 seam 上 |
|
||||
| 钩子系统(用户级 + 项目级) | `agent/session-start`、`agent/pre-step`、`agent/request`、`tools/pre-execute`、`tools/post-execute` 和 `agent/turn-stopping` 上的监听器;waterfall seam 返回类型化决策,`agent/turn-stopping` 则可通过 steering 触发下一步;`dsh-hooks-claude` / `dsh-hooks-codex` 桥接器将钩子配置文件映射到这些 seam 上 |
|
||||
| `/goal` | `ctx.goals` 管理持久状态,`dsh-goal-session` 通过公共 `Agent` 调度同会话回合,独立的命令/工具生产方分别提供人类/模型控制 |
|
||||
| `/loop` | 在 `turn/end` 会话事件上 `followup()` 下一次迭代;或强制继续 |
|
||||
| 动态工作流 | `ctx.workflows` + worker-thread 引擎 + `workflow` 工具;结构化的进程内子任务通过作用域化的 prompt/工具注册、单调工具守卫、最终 `tools/result` 提交(包括外层 `run_code`)和结构化输出执行的单调 `concludeTurn()` 标记来强制输出 |
|
||||
| 排队消息 + steering(中途引导) | 核心 `Agent.followup()` / `Agent.steer()` |
|
||||
| 上下文压缩(context compaction)(自动 + 手动) | `ctx.compact` seam + `dsh-compact-basic`;自动压力检查运行在串行 `agent/step`,规范化溢出恢复运行在 `agent/request-error`,手动调用方使用同一个压缩服务([压缩 Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)——面向模型的 `/compact` 消费方工具已推迟) |
|
||||
| 上下文压缩(context compaction)(自动 + 手动) | `ctx.compact` seam + `dsh-compact-basic`;自动压力检查运行在串行 `agent/pre-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()` |
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/cookbook/maintaining-dsh-code-review.md
|
||||
maintaining-dsh-code-review.md: 2b5d0d926ae922f2650daac33cf35991cb71c5e5
|
||||
maintaining-dsh-code-review.zh.md: c0e8b64fde3a67174878b4b0665712c9ba2e67c0
|
||||
maintaining-dsh-code-review.zh.md: 56d274c080fcd95b6d18219f4bf0e0e7485f624e
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 维护 dsh-code-review skill
|
||||
# 维护 dsh-code-review skill(技能)
|
||||
|
||||
[English](maintaining-dsh-code-review.md) | 中文
|
||||
|
||||
[`dsh-code-review`](../../.agents/skills/dsh-code-review/SKILL.md) skill(技能)由一名指定操作员通过私有的周期维护工具持续更新。本实操手册(cookbook)既是该操作员和接任者的入口,也帮助仓库贡献者理解为何 skill 更新会以小型周期 PR(Pull Request)的形式出现,而不是一次性审计。工作流本身由[人工评审 skill 维护 Agent Note(agent 决策记录)](../../.agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md)规定。
|
||||
[`dsh-code-review`](../../.agents/skills/dsh-code-review/SKILL.md) skill 由一名指定操作员通过私有的周期维护工具持续更新。本实操手册既是该操作员和接任者的入口,也帮助仓库贡献者理解为何 skill 更新会以小型周期 PR(Pull Request)的形式出现,而不是一次性审计。工作流本身由[人工评审 skill 维护 Agent Note](../../.agents/notes/proposed/process/2026-07-13-human-review-skill-maintenance.md)规定。
|
||||
|
||||
## 维护者会收到什么
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
1. 选择指定窗口内合并、且合并 commit 可从 `origin/master` 到达的 PR(每天运行默认选择 2 个 UTC 日,每周运行选择 7 日)。合并 commit 无法到达的 PR(例如父分支被 squash 的堆叠分支),或超出 250 个 commit 获取上限的 PR,会记录到 `skipped-pulls.json` 并跳过,不会中止本次运行。
|
||||
2. 收集合并前带 commit 锚点的人工评审反馈(行内评论和评审提交),然后比较反馈时与最终落地的 PR patch。它不获取 PR 会话评论,因为 GitHub 当前状态无法为这些评论提供可抵抗 force-push 的反馈时基线;它也不会把只存在于目标分支的变更作为采纳证据。
|
||||
3. 两个独立配置的评审适配器先对来源和采纳情况分类,再根据当前 skill 对双方一致认定已采纳的条目分类。
|
||||
4. 主适配器起草完整修订版 `SKILL.md`;两个适配器评审同一份 diff;只要仍有阻塞发现,循环就会继续,直到双方批准。
|
||||
4. 主适配器起草完整修订版 `SKILL.md`;两个适配器评审同一份 diff;只要仍有阻塞性问题,循环就会继续,直到双方批准。
|
||||
5. 工具声明成功前,会针对候选版本运行 `pnpm run doc-sync` 和 `pnpm run lint`。
|
||||
|
||||
每次运行都把产物保存在操作员的机器上。保存的 diff、候选 `SKILL.md` 和提升 manifest(元数据清单)按时间戳命名,存放在 `~/dsh-code-review-outputs/` 下。manifest 记录源 master commit 与 skill blob、源反馈 ID 和 URL、已落地证据范围、适配器裁决和门禁结果;每个适配器的原始 I/O 留在私有临时目录中,该目录路径会写入通知和 `~/Library/Logs/dsh-code-review-maintainer/` 下的每日日志。维护 worktree 在每次运行后都会恢复为干净状态,避免操作员直接在维护副本中编辑。
|
||||
@@ -37,7 +37,7 @@
|
||||
```sh
|
||||
rm ~/dsh-code-review-outputs/2026-07-16T02-00-00Z.{diff,SKILL.md,manifest.json}
|
||||
```
|
||||
- **暂存成批。** 如果更新很小,可以把候选版本留待与后续版本合并。源 skill 检查仍然适用;如果 `master` 先发生变化,请重新运行分析,或手动 rebase 并重新评审 diff。
|
||||
- **留待成批处理。** 如果更新很小,可以把候选版本留待与后续版本合并。源 skill 检查仍然适用;如果 `master` 先发生变化,请重新运行分析,或手动 rebase 并重新评审 diff。
|
||||
- **提升。** 在仓库的干净 `master` checkout 中运行提升辅助工具。它会刷新 `master`、验证当前 skill 与记录的源 blob 一致、应用保存的 diff,并创建一份 draft PR,其正文包含 manifest 的来源摘要。如果 skill 已发生漂移,它会停止而不是覆盖更新后的指导;操作员仍需在 GitHub 上评审 PR,并选择合并或关闭。
|
||||
|
||||
```sh
|
||||
|
||||
@@ -1,6 +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
|
||||
responding-to-pr-review-on-a-stack.md: 3fb7eb943eeb8d703303be3f6a844870cc26fd47
|
||||
responding-to-pr-review-on-a-stack.zh.md: d96323b853c093265931904c20996df335f82926
|
||||
# pnpm run verify-translation-pairing --write docs/cookbook/responding-to-pr-review-on-a-stack.md
|
||||
responding-to-pr-review-on-a-stack.md: 6bb7be3daf8613666f02f21e96bf7b1ac8cb3606
|
||||
responding-to-pr-review-on-a-stack.zh.md: 94c2bd2cdd03b9eeca1e180b09bb71471db91c0a
|
||||
|
||||
@@ -2,25 +2,31 @@
|
||||
|
||||
English | [中文](responding-to-pr-review-on-a-stack.zh.md)
|
||||
|
||||
Review comments may target several PRs in a dependent stack (`A ← B ← C …`). This guide explains how to resolve them without corrupting the stack. The two invariants it rests on are standing orders in the root [AGENTS.md](../../AGENTS.md) § Conventions: merge commits only, and never rewrite a pushed branch.
|
||||
Review comments may target several PRs in a dependent stack (`A ← B ← C …`). Keep that chain linked through GitHub's official stacked-PR feature. This guide owns review-fix placement and propagation; the [dsh-merging-stacked-prs](../../.agents/skills/dsh-merging-stacked-prs/SKILL.md) skill owns linkage checks and landing.
|
||||
|
||||
## Ground rules
|
||||
|
||||
1. **One worktree per PR branch.** Each PR's fixes happen in that PR's own worktree; parallel fixes never share a checkout.
|
||||
2. **Bring a child up to date by merging the parent down** (`git merge <parent-branch>` into the child, a new merge commit). Never rebase/amend/force-push a pushed branch: rewriting diverges it from what the parent PR and GitHub recorded, breaks the stacked-merge graph, and erases the review-fix history.
|
||||
3. **A fix lands on the PR that INTRODUCED the issue, then flows down.** When a comment on PR `B` points at code `B` introduced, fix it on `B` and merge `B` into `C` — even if `C` also carries the file. Originating the fix downstream leaves `B` shipping the unfixed code and hides the fix from `B`'s reviewer.
|
||||
4. **Each review fix is a separate commit, never an amend.** The "fix review findings" commit documents what the review caught. Amending is fine only for your own not-yet-pushed, not-yet-reviewed work.
|
||||
2. **GitHub's stack object is authoritative.** Base branches establish the expected dependency order, while `PullRequest.stack` and `stackEntry.position` prove that GitHub recognizes it. Do not treat a matching branch chain as an official stack without checking those fields.
|
||||
3. **A fix lands on the PR that INTRODUCED the issue, then flows up-stack.** When a comment on PR `B` points at code `B` introduced, fix it on `B` and propagate `B` into `C` — even if `C` also carries the file. Originating the fix downstream leaves `B` shipping the unfixed code and hides the fix from `B`'s reviewer.
|
||||
4. **Each review fix remains a distinct commit.** A later rebase may change its OID, but do not amend a reviewed fix out of the branch history. Amend only your own not-yet-pushed, not-yet-reviewed work.
|
||||
5. **Choose merge-forward or rebase deliberately.** Both histories are allowed after review. A rewritten push must be lease-protected and must abort rather than overwrite a concurrently advanced remote head; raw `--force` is forbidden.
|
||||
|
||||
## Resolve comments through the stack
|
||||
|
||||
1. Triage every comment on the merits before acting: verify the claim against the code — a reviewer flagging the right symptom can still mis-diagnose the cause.
|
||||
2. Map each accepted finding to its originating PR, fix it there, then merge down the chain in order.
|
||||
3. Delegated fixes are trust-but-verify: a sub-agent's report describes intent, not necessarily what landed. Re-run the gates yourself on the actual tree, and for a regression guard, prove it FAILS on the unfixed code (introduce the regression, watch red, revert) — a guard that passes both ways guards nothing. A sub-agent that reframes a problem as already-handled is a signal to dig in personally.
|
||||
4. Reply in the review thread (`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`), not as a top-level comment, stating the fix and the commit that carries it.
|
||||
5. Before merging the stack, check dependents: deleting a PR's base branch auto-closes the dependent PR — check each branch with `gh pr list --state open --base <branch> --json number --jq length` (non-zero = open dependents), and merge without `--delete-branch` where a child still bases on the branch. The full landing procedure is the [dsh-merging-stacked-prs](../../.agents/skills/dsh-merging-stacked-prs/SKILL.md) skill.
|
||||
1. Triage every comment on the merits before acting: verify the claim against the code — a reviewer flagging the right symptom can still misdiagnose the cause.
|
||||
2. Map each accepted finding to its originating PR and fix it there.
|
||||
3. Propagate the fixed layer through every affected child in order:
|
||||
- **Merge-forward:** merge the fixed parent branch into its child, validate the child, and continue upward. Preserve each in-progress checkpoint under the [incremental-retargeting decision](../../.agents/notes/implemented/process/2026-07-26-incremental-pr-base-retargeting.md).
|
||||
- **Native cascading rebase:** use `gh stack rebase`, validate the rewritten layers, then publish with `gh stack push`; or use `gh stack sync`, which may publish first and therefore requires immediate post-sync validation under [dsh-pre-push-checks](../../.agents/skills/dsh-pre-push-checks/SKILL.md).
|
||||
4. Treat delegated fixes as trust-but-verify: a sub-agent's report describes intent, not necessarily what landed. Re-run the gates yourself on the actual tree, and for a regression guard, prove it FAILS on the unfixed code (introduce the regression, watch red, revert) — a guard that passes both ways guards nothing. A sub-agent that reframes a problem as already handled is a signal to dig in personally.
|
||||
5. Reply in the review thread (`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`), not as a top-level comment, stating the fix and the current commit or head that carries it.
|
||||
6. After any rewritten push, re-read unresolved threads, approvals, mergeability, and checks. A force-pushed commit OID or outdated inline anchor is not current evidence that the finding remains resolved.
|
||||
7. Land only through the official stack procedure. If the PRs are not yet linked, the landing skill automatically links a same-author chain, asks before linking mixed authors, and hard-stops when native stack support is unavailable.
|
||||
|
||||
## Verify
|
||||
|
||||
- Every fixed PR shows a new commit (no force-push icon in the PR timeline).
|
||||
- Each child PR's diff against its parent still shows only its own changes.
|
||||
- The gates pass on every PR in the stack, not just the top.
|
||||
- Every fixed PR's current diff contains the intended correction at the layer that introduced the issue.
|
||||
- GraphQL reports one official stack in the expected order, and each child diff against its parent shows only that child's changes.
|
||||
- Unresolved threads, approvals, mergeability, and checks were re-audited after every rewritten push.
|
||||
- The relevant gates pass on every affected PR in the stack, not just the top.
|
||||
|
||||
@@ -2,25 +2,31 @@
|
||||
|
||||
[English](responding-to-pr-review-on-a-stack.md) | 中文
|
||||
|
||||
评审意见可能同时针对一条依赖堆叠(`A ← B ← C …`)中的多个 PR(Pull Request)。本指南说明如何在不破坏堆叠的前提下解决这些意见。它依赖的两个不变式是根 [AGENTS.md](../../AGENTS.md) § Conventions 中的常设指令:只用 merge commit,以及永远不改写已推送的分支。
|
||||
评审意见可能同时针对一条依赖堆叠(`A ← B ← C …`)中的多个 PR(Pull Request)。请通过 GitHub 官方的堆叠 PR 功能保持这条链的关联。本指南负责评审修复的归属与传播;[dsh-merging-stacked-prs](../../.agents/skills/dsh-merging-stacked-prs/SKILL.md) skill(技能)负责检查关联关系和落地。
|
||||
|
||||
## 基本规则
|
||||
|
||||
1. **每个 PR 分支一个 worktree。** 每个 PR 的修复在该 PR 自己的 worktree 中进行;并行修复绝不共享同一个 checkout。
|
||||
2. **通过将父分支向下合并来更新子分支**(在子分支中执行 `git merge <parent-branch>`,产生一个新的 merge commit)。绝不对已推送的分支做 rebase/amend/force-push:改写会使分支与父 PR 及 GitHub 记录的内容产生分歧,破坏堆叠合并图,并抹去评审修复历史。
|
||||
3. **修复落在引入问题的那个 PR 上,然后向下流动。** 当 PR `B` 上的评论指向 `B` 引入的代码时,在 `B` 上修复,再将 `B` 合并到 `C`——即使 `C` 也包含该文件。把修复发起在下游会导致 `B` 带着未修复的代码交付,并对 `B` 的评审者隐藏修复。
|
||||
4. **每个评审修复是一个独立 commit,绝不 amend。** "修复评审发现"的 commit 记录了评审捕获的内容。只有你自己尚未推送、尚未评审的工作才可以 amend。
|
||||
2. **GitHub 的 stack 对象是权威依据。** base 分支确定预期的依赖顺序,`PullRequest.stack` 和 `stackEntry.position` 则证明 GitHub 已识别该堆叠。未经检查这些字段,不得仅凭分支链吻合就将其视为官方堆叠。
|
||||
3. **修复落在引入问题的那个 PR 上,然后沿堆叠向上流动。** 当 PR `B` 上的评论指向 `B` 引入的代码时,在 `B` 上修复,再将 `B` 的变更传播到 `C`,即使 `C` 也包含该文件。把修复发起在下游会导致 `B` 带着未修复的代码交付,并对 `B` 的评审者隐藏修复。
|
||||
4. **每项评审修复都保留为独立 commit。** 后续 rebase 可能改变其 OID,但不得通过 amend 把已经评审的修复从分支历史中抹去。只有你自己尚未推送且尚未评审的工作才可以 amend。
|
||||
5. **明确选择 merge-forward 或 rebase。** 评审后允许采用这两种历史更新方式。改写历史的推送必须受 lease 保护;如果远端 head 在此期间前移,操作必须中止,不得将其覆盖。禁止直接使用 `--force`。
|
||||
|
||||
## 沿堆叠解决评审意见
|
||||
|
||||
1. 在行动之前先就事论事地审视每条评论:对照代码验证其论断——评审者指出了正确的症状,但仍可能误诊原因。
|
||||
2. 将每个被接受的发现映射到其发起 PR,在那里修复,然后按顺序沿链向下合并。
|
||||
3. 委派的修复需要信任但验证:子 agent(智能体)的报告描述的是意图,不一定是实际落地的内容。请亲自在实际代码树上重新运行门禁;对于回归守卫,要证明它在未修复的代码上**失败**(引入回归、观察变红、再还原)——两种情况都通过的守卫什么也守不住。子 agent 将问题重新定性为「已处理」时,这是一个需要亲自深入的信号。
|
||||
4. 在评审线程中回复(`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`),而非发顶层评论;说明修复内容及承载修复的 commit。
|
||||
5. 合并堆叠之前,检查依赖方:删除一个 PR 的 base 分支会自动关闭依赖它的 PR。用 `gh pr list --state open --base <branch> --json number --jq length` 检查每个分支(非零 = 有打开的依赖方),当子 PR 仍以该分支为 base 时,合并时不带 `--delete-branch`。完整的落地流程见 [dsh-merging-stacked-prs](../../.agents/skills/dsh-merging-stacked-prs/SKILL.md) skill(技能)。
|
||||
2. 将每个被接受的发现映射到其发起 PR,并在那里修复。
|
||||
3. 将修复后的层按顺序传播到每个受影响的子 PR:
|
||||
- **Merge-forward:** 将修复后的父分支合并到其子分支,验证子分支,然后继续沿堆叠向上传播。依照[增量更新 base 的决策](../../.agents/notes/implemented/process/2026-07-26-incremental-pr-base-retargeting.md),保留每个正在处理的检查点。
|
||||
- **原生级联 rebase:** 使用 `gh stack rebase`,验证所有已改写的层,然后通过 `gh stack push` 发布;也可以使用 `gh stack sync`,该命令可能先发布,因此必须按照 [dsh-pre-push-checks](../../.agents/skills/dsh-pre-push-checks/SKILL.md) 在同步后立即验证。
|
||||
4. 委派的修复需要信任但验证:子 agent(智能体)的报告描述的是意图,不一定是实际落地的内容。请亲自在实际代码树上重新运行门禁;对于回归守卫,要证明它在未修复的代码上**失败**(引入回归、观察变红、再还原)——两种情况都通过的守卫什么也守不住。子 agent 将问题重新定性为「已处理」时,这是一个需要亲自深入的信号。
|
||||
5. 在评审线程中回复(`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`),而非发顶层评论;说明修复内容及当前承载修复的 commit 或 head。
|
||||
6. 每次改写推送后,都要重新读取未解决线程、批准状态、可合并性和检查结果。经 force-push 改写的 commit OID 或已过时的内联锚点,都不足以证明该发现当前仍处于已解决状态。
|
||||
7. 仅可通过官方堆叠流程落地。如果这些 PR 尚未关联,落地 skill 会自动关联作者相同的链;如果作者不同,则先询问用户;如果原生堆叠支持不可用,则硬性停止流程。
|
||||
|
||||
## 验证
|
||||
|
||||
- 每个已修复的 PR 显示一个新 commit(PR 时间线中没有 force-push 图标)。
|
||||
- 每个子 PR 相对其父 PR 的 diff 仍然只包含自身的变更。
|
||||
- 门禁在堆叠中的每个 PR 上都通过,而不仅仅是顶部。
|
||||
- 每个已修复 PR 的当前 diff 都在引入问题的那一层包含预期修正。
|
||||
- GraphQL 报告的官方堆叠只有一个且顺序符合预期;每个子 PR 相对于父 PR 的 diff 只显示该子 PR 自身的变更。
|
||||
- 每次改写推送后,均重新审计了未解决线程、批准状态、可合并性和检查结果。
|
||||
- 相关门禁在堆叠中的每个受影响 PR 上都通过,而不仅仅是顶部。
|
||||
|
||||
@@ -256,8 +256,8 @@ Dispose and immediately reload this plugin with its current config.
|
||||
*
|
||||
* @param config — the new raw config; validated before anything restarts.
|
||||
* @param noSave — hint for persistence hooks not to write the change back.
|
||||
* @returns nothing; the restart runs behind the `internal/update` waterfall.
|
||||
* @throws {ValidationError} when the new config fails validation.
|
||||
* @returns the update waterfall result; the default restart returns a promise.
|
||||
* @throws when validation, an update listener, or the restarted plugin fails.
|
||||
*/
|
||||
update(config: any, noSave = false)
|
||||
```
|
||||
@@ -269,7 +269,7 @@ Runs the `internal/update` waterfall first, so update hooks (and HMR) can veto o
|
||||
- `config` — the new raw config; validated before anything restarts.
|
||||
- `noSave` — hint for persistence hooks not to write the change back.
|
||||
|
||||
**Returns** nothing; the restart runs behind the `internal/update` waterfall.
|
||||
**Returns** the update waterfall result; the default restart returns a promise.
|
||||
|
||||
[Source](../../../vendor/cordis/src/fiber.ts#L734)
|
||||
|
||||
|
||||
@@ -9,31 +9,10 @@ This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verifie
|
||||
|
||||
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`), **bail** (synchronous in-order dispatch until one listener returns a bail value; the scoped input-mutation events use it for an applied/not-applied answer).
|
||||
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`).
|
||||
|
||||
## `agent/*`
|
||||
|
||||
### `agent/cancel-requested` — emit
|
||||
|
||||
Effective broad cancellation was requested, before queued/outbox work is cleared or the active turn is aborted. This observe-only notification cannot veto cancellation; listener failures are contained.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Effective broad cancellation was requested, before queued/outbox work
|
||||
* is cleared or the active turn is aborted. This observe-only notification
|
||||
* cannot veto cancellation; listener failures are contained.
|
||||
* @param agent - the agent whose current work is being cancelled.
|
||||
* @param cause - the explicit typed cancellation cause.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/cancel-requested'(this: Scoped<Agent>, agent: Agent, cause: AgentCancelCause): void
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentCancelCause](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:286`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/created` — emit
|
||||
|
||||
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.
|
||||
@@ -54,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:218`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:178`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/disposed` — emit
|
||||
|
||||
@@ -74,16 +53,16 @@ An agent left the registry; AgentLoop emits this after driver quiescence and sco
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:227`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:187`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/error` — emit
|
||||
|
||||
A step or turn errored. The machine reports a failure here (plus the logger) even when the error has no in-turn position for a durable record.
|
||||
A step or turn errored. The machine reports a failure here even when the error has no in-turn position for a durable record.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* A step or turn errored. The machine reports a failure here (plus the
|
||||
* logger) even when the error has no in-turn position for a durable record.
|
||||
* A step or turn errored. The machine reports a failure here even when
|
||||
* the error has no in-turn position for a durable record.
|
||||
* @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.
|
||||
@@ -96,98 +75,87 @@ A step or turn errored. The machine reports a failure here (plus the logger) eve
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:400`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:302`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/inbox/dequeue` — emit
|
||||
### `agent/inbox/claimed` — emit
|
||||
|
||||
The driver claimed one item out of the inbox: a queued item at a turn boundary, or steering drained between steps. Fires after the item leaves its FIFO and before it becomes a durable message.
|
||||
One message left the inbox inside its open turn. If the proposed step is rejected, the claimed message ends here: it is neither discarded nor re-emitted as a user/message, and the turn closes without a step.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* The driver claimed one item out of the inbox: a queued item at a turn
|
||||
* boundary, or steering drained between steps. Fires after the item leaves
|
||||
* its FIFO and before it becomes a durable message.
|
||||
* @param agent - the agent whose inbox item was claimed.
|
||||
* @param message - the claimed message.
|
||||
* @param placement - the FIFO that claimed this occurrence; together with
|
||||
* `message.id`, it matches the earliest outstanding enqueue in that FIFO.
|
||||
* One message left the inbox inside its open turn. If the proposed step
|
||||
* is rejected, the claimed message ends here: it is neither discarded nor
|
||||
* re-emitted as a user/message, and the turn closes without a step.
|
||||
* @param agent - the agent whose inbox changed.
|
||||
* @param event - the claimed message and owning turn.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/dequeue'( this: Scoped<Agent>, agent: Agent, message: UserMessage, placement: InboxPlacement, ): void
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [InboxPlacement](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/inbox/discard` — emit
|
||||
|
||||
Pending inbox items were dropped without delivering them, so every enqueue occurrence receives exactly one terminal `agent/inbox/dequeue` OR `agent/inbox/discard`. `cancel()` without `keepInbox`, including disposal, emits this after `agent/cancel-requested` when applicable and before aborting the active work. Fires once per drop with every dropped item.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Pending inbox items were dropped without delivering them, so every
|
||||
* enqueue occurrence receives exactly one terminal `agent/inbox/dequeue` OR
|
||||
* `agent/inbox/discard`. `cancel()` without `keepInbox`, including disposal,
|
||||
* emits this after `agent/cancel-requested` when applicable and before
|
||||
* aborting the active work. Fires once per drop with every dropped item.
|
||||
* @param agent - the agent whose inbox items were dropped.
|
||||
* @param messages - the discarded messages in FIFO order (queued then steering); never empty.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/discard'(this: Scoped<Agent>, agent: Agent, messages: UserMessage[]): void
|
||||
'agent/inbox/claimed'(this: Scoped<Agent>, agent: Agent, event: { message: UserMessage; turn: number }): void
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:276`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:215`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/inbox/enqueue` — emit
|
||||
### `agent/inbox/discarded` — emit
|
||||
|
||||
An item entered the queued or steering inbox. `placement` is the acceptance-time routing result; listeners must not reconstruct it from later agent or session state.
|
||||
One message was discarded from the live inbox.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* An item entered the queued or steering inbox. `placement` is the
|
||||
* acceptance-time routing result; listeners must not reconstruct it from
|
||||
* later agent or session state.
|
||||
* @param agent - the owning agent.
|
||||
* @param message - accepted content, source, and correlation identity.
|
||||
* @param placement - resolved queued or steering placement.
|
||||
* One message was discarded from the live inbox.
|
||||
* @param agent - the agent whose inbox changed.
|
||||
* @param event - the discarded message.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/enqueue'(this: Scoped<Agent>, agent: Agent, message: UserMessage, placement: InboxPlacement): void
|
||||
'agent/inbox/discarded'(this: Scoped<Agent>, agent: Agent, event: { message: UserMessage }): void
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [InboxPlacement](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/session.md)
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:247`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:223`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/prompt-submit` — waterfall
|
||||
### `agent/inbox/inserted` — emit
|
||||
|
||||
Allow, rewrite, or block one claimed prompt before it becomes a user message or opens a turn. Call `next()` for the unchanged default. The signal controls only this admission attempt; listeners may cooperate with it but must not retain it for a later attempt or turn.
|
||||
One message entered the live inbox.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Allow, rewrite, or block one claimed prompt before it becomes a user
|
||||
* message or opens a turn. Call `next()` for the unchanged default. The
|
||||
* signal controls only this admission attempt; listeners may cooperate with
|
||||
* it but must not retain it for a later attempt or turn.
|
||||
* @param agent - the agent whose turn claimed the message.
|
||||
* @param message - the frozen claimed message, including identity and source.
|
||||
* @param signal - the current turn's explicit abort signal.
|
||||
* One message entered the live inbox.
|
||||
* @param agent - the agent whose inbox changed.
|
||||
* @param event - the inserted message.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/inbox/inserted'(this: Scoped<Agent>, agent: Agent, event: { message: UserMessage }): void
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:205`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/pre-step` — waterfall
|
||||
|
||||
Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Reject a proposed step or replace the messages that enter it. Calling
|
||||
* `next()` preserves the current messages.
|
||||
* @param agent - the agent proposing the step.
|
||||
* @param messages - messages removed from the inbox for this step.
|
||||
* @param context - proposed turn and step coordinates plus cancellation.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'agent/prompt-submit'(this: Scoped<Agent>, agent: Agent, message: UserMessage, signal: AbortSignal, next: () => Promise<PromptDecision>): Promise<PromptDecision>
|
||||
'agent/pre-step'(this: Scoped<Agent>, agent: Agent, messages: UserMessage[], context: PreStepContext, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [PromptDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/session.md)
|
||||
Types: [Agent](../core-data-structures/core.md) · [PreStepContext](../core-data-structures/core.md) · [PreStepDecision](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [UserMessage](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:313`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:247`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/request` — waterfall
|
||||
|
||||
@@ -211,37 +179,30 @@ Replace the frozen call configuration. `await next()` yields the config the mach
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [LlmCallConfig](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:339`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:260`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/request-error` — waterfall
|
||||
|
||||
Handle a model-request failure after its failed step has closed but before the failed turn closes. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns the error, or calls `next()` to delegate. The default `undefined` leaves the failure terminal.
|
||||
Handle one failed model-request attempt before the loop retries or closes its step. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns recovery, or calls `next()` to delegate. The default `undefined` leaves the failure terminal.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Handle a model-request failure after its failed step has closed but
|
||||
* before the failed turn closes. A listener returns `{ kind: 'retry' }`
|
||||
* without calling `next()` when it owns the error, or calls `next()` to
|
||||
* delegate. The default `undefined` leaves the failure terminal.
|
||||
* Handle one failed model-request attempt before the loop retries or closes
|
||||
* its step. A listener returns `{ kind: 'retry' }` without calling `next()`
|
||||
* when it owns recovery, or calls `next()` to delegate. The default
|
||||
* `undefined` leaves the failure terminal.
|
||||
* @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 failure - serializable facts normalized at the final adapter boundary.
|
||||
* @param priorFailures - immutable failures that already authorized another
|
||||
* retry turn in this consecutive sequence.
|
||||
* @param retryPolicy - immutable policy of the adapter registration that served
|
||||
* the failed request, or `undefined` if no final adapter served it.
|
||||
* @param context - request coordinates, provider, normalized failure, and serving policy.
|
||||
* @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, failure: LlmFailure, priorFailures: readonly LlmFailure[], retryPolicy: ResolvedRetryPolicy | undefined, signal: AbortSignal, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
|
||||
'agent/request-error'(this: Scoped<Agent>, agent: Agent, context: RequestFailureContext, signal: AbortSignal, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [LlmFailure](../core-data-structures/llm-streaming.md) · [RequestError](../core-data-structures/core.md) · [RequestErrorAction](../core-data-structures/core.md) · [ResolvedRetryPolicy](../core-data-structures/llm-streaming.md) · [Scoped](../core-data-structures/scope.md)
|
||||
Types: [Agent](../core-data-structures/core.md) · [RequestErrorAction](../core-data-structures/core.md) · [RequestFailureContext](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:358`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:272`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/session-start` — emit
|
||||
|
||||
@@ -263,41 +224,17 @@ 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:299`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/settled` — emit
|
||||
|
||||
One drain chain reached its terminal turn: that turn's `turn/end` is already committed. Automatically recovered failed turns do not emit this notification, and neither does a run that aborts or fails before its `turn/start` commits — there is no durable turn to settle against. `reason` says why; model-request recovery is exhausted when an error reaches it.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* One drain chain reached its terminal turn: that turn's `turn/end` is
|
||||
* already committed. Automatically recovered failed turns do not emit this
|
||||
* notification, and neither does a run that aborts or fails before its
|
||||
* `turn/start` commits — there is no durable turn to settle against.
|
||||
* `reason` says why; model-request recovery is exhausted when an error
|
||||
* reaches it.
|
||||
* @param agent - the agent whose turn closed.
|
||||
* @param turn - the terminal turn number.
|
||||
* @param reason - why the terminal turn ended, with live error facts when it failed.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent/settled'(this: Scoped<Agent>, agent: Agent, turn: number, reason: SettleReason): void
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [SettleReason](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:387`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:235`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/status` — emit
|
||||
|
||||
Agent status changed (`idle` ⇄ `running`). `send()` does not enter `running` synchronously; drive lifecycle from this event.
|
||||
Agent status changed (`idle` ⇄ `running`). A waking delivery enters `running` synchronously after reserving cancellation; `idle` means no driver remains scheduled or active.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Agent status changed (`idle` ⇄ `running`). `send()` does not enter
|
||||
* `running` synchronously; drive lifecycle from this event.
|
||||
* Agent status changed (`idle` ⇄ `running`). A waking delivery enters
|
||||
* `running` synchronously after reserving cancellation; `idle` means no
|
||||
* driver remains scheduled or active.
|
||||
* @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.
|
||||
@@ -308,35 +245,11 @@ Agent status changed (`idle` ⇄ `running`). `send()` does not enter `running` s
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentStatus](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:236`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/step` — serial
|
||||
|
||||
Awaited serial checkpoint before EVERY request of a turn is built (the first as well as each post-tools continuation). The single "between steps" extension point: inject context, steer, or edit the session log here — the request's history derives from the log right after this settles.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Awaited serial checkpoint before EVERY request of a turn is built (the
|
||||
* first as well as each post-tools continuation). The single "between
|
||||
* steps" extension point: inject context, steer, or edit the session log
|
||||
* here — the request's history derives from the log right after this settles.
|
||||
* @param agent - the agent about to send a request.
|
||||
* @param turn - the open turn number.
|
||||
* @param step - the step number about to open.
|
||||
* @param signal - the turn abort signal.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @mode serial
|
||||
*/
|
||||
'agent/step'(this: Scoped<Agent>, agent: Agent, turn: number, step: number, signal: AbortSignal): Promise<void> | void
|
||||
```
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:326`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:197`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent/turn-stopping` — serial
|
||||
|
||||
The turn is about to close: the model owes no response (no live tool calls, no fresh steering). Awaited before the boundary commits — a listener that objects steers (`agent.steer(...)`) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. Data decides, so listener order cannot change the outcome. The inverse control (stop a tool loop early) is data too: a tool result carrying `concludesTurn` ends the turn at its step.
|
||||
The turn is about to close: the model owes no response (no live tool calls, no fresh steering). Awaited before the boundary commits — a listener that objects steers (`agent.steer(...)`) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. Data decides, so listener order cannot change the outcome. The inverse control (stop a tool loop early) is data too: a tool result carrying `concludesTurn` ends the turn at its step. The conclusion never short-circuits already-submitted next-step work: same-step `additionalContexts` or racing steering still runs, and the turn closes only when that inbox drains.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -346,7 +259,10 @@ The turn is about to close: the model owes no response (no live tool calls, no f
|
||||
* re-reads its inbox: fresh steering runs another step, none closes the
|
||||
* turn. Data decides, so listener order cannot change the outcome. The
|
||||
* inverse control (stop a tool loop early) is data too: a tool result
|
||||
* carrying `concludesTurn` ends the turn at its step.
|
||||
* carrying `concludesTurn` ends the turn at its step. The conclusion
|
||||
* never short-circuits already-submitted next-step work: same-step
|
||||
* `additionalContexts` or racing steering still runs, and the turn
|
||||
* closes only when that inbox drains.
|
||||
* @param agent - the agent whose turn is at its stop boundary.
|
||||
* @param turn - the turn about to close.
|
||||
* @param signal - the current turn's explicit abort signal.
|
||||
@@ -358,7 +274,7 @@ The turn is about to close: the model owes no response (no live tool calls, no f
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:373`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:290`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
## `agent-loop/*`
|
||||
|
||||
@@ -381,7 +297,7 @@ A declarative agent entry failed before it could publish a live agent. Consumers
|
||||
|
||||
Types: [SessionId](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent-loop/src/index.ts:148`](../../packages/core/agent-loop/src/index.ts)
|
||||
Source: [`packages/core/agent-loop/src/index.ts:182`](../../packages/core/agent-loop/src/index.ts)
|
||||
|
||||
## `approval/*`
|
||||
|
||||
@@ -422,6 +338,32 @@ A command was registered or unregistered. This is an unfiltered registry notific
|
||||
|
||||
Source: [`packages/ui/commands/src/index.ts:161`](../../packages/ui/commands/src/index.ts)
|
||||
|
||||
## `credentials/*`
|
||||
|
||||
### `credentials/updated` — emit
|
||||
|
||||
Committed change to a provider-managed credential source: a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Listener failures are contained and logged — a sync throw and an async rejection alike — without changing the committed operation's outcome, except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Committed change to a provider-managed credential source: a `set`, an
|
||||
* `unset`, or an external edit observed in storage. Ambient
|
||||
* process-environment changes are not observable and never emit. Listener
|
||||
* failures are contained and logged — a sync throw and an async rejection
|
||||
* alike — without changing the committed operation's outcome, except
|
||||
* `INVARIANT`-coded failures, which rethrow after every listener ran;
|
||||
* that rethrow reaches the emitter only from synchronous listeners, so
|
||||
* invariant checks on this event must not be async functions.
|
||||
* @param ref - the reference whose stored value changed.
|
||||
* @mode emit
|
||||
*/
|
||||
'credentials/updated'(ref: CredentialRef): void
|
||||
```
|
||||
|
||||
Types: [CredentialRef](../core-data-structures/credentials.md)
|
||||
|
||||
Source: [`packages/credentials/credentials/src/index.ts:67`](../../packages/credentials/credentials/src/index.ts)
|
||||
|
||||
## `domain/*`
|
||||
|
||||
### `domain/changed` — emit
|
||||
@@ -507,13 +449,12 @@ Source: [`packages/fs/fs/src/index.ts:54`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
### `goal/changed` — emit
|
||||
|
||||
Goal mutation accepted by one live agent. The matching context event is already appended or queued in that agent's active tool-batch FIFO. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
Goal mutation accepted by one live agent. The matching `goal/change` session event has already committed. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Goal mutation accepted by one live agent. The matching context event is
|
||||
* already appended or queued in that agent's active tool-batch FIFO.
|
||||
* Listener failures are contained.
|
||||
* Goal mutation accepted by one live agent. The matching `goal/change`
|
||||
* session event has already committed. Listener failures are contained.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @param agent - agent whose session owns the goal.
|
||||
* @param change - fresh current projection or clear tombstone.
|
||||
@@ -524,10 +465,29 @@ Goal mutation accepted by one live agent. The matching context event is already
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [GoalChanged](../core-data-structures/goal.md) · [Scoped](../core-data-structures/scope.md)
|
||||
|
||||
Source: [`packages/goal/goal/src/domain.ts:135`](../../packages/goal/goal/src/domain.ts)
|
||||
Source: [`packages/goal/goal/src/domain.ts:141`](../../packages/goal/goal/src/domain.ts)
|
||||
|
||||
## `llm/*`
|
||||
|
||||
### `llm/adapters-updated` — emit
|
||||
|
||||
The provider topology changed: an adapter registered or unregistered routes, or the configurable-provider directory gained or lost entries. This is a payload-free registry notification fired at each commit point (including registration disposal); consumers re-read `listProviders()`, `listModels()`, or `listConfigurableProviders()` for the new state. Observer failures are contained and cannot veto the registry mutation.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* The provider topology changed: an adapter registered or unregistered
|
||||
* routes, or the configurable-provider directory gained or lost entries.
|
||||
* This is a payload-free registry notification fired at each commit point
|
||||
* (including registration disposal); consumers re-read `listProviders()`,
|
||||
* `listModels()`, or `listConfigurableProviders()` for the new state.
|
||||
* Observer failures are contained and cannot veto the registry mutation.
|
||||
* @mode emit
|
||||
*/
|
||||
'llm/adapters-updated'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/llm/llm/src/index.ts:69`](../../packages/llm/llm/src/index.ts)
|
||||
|
||||
### `llm/stream` — waterfall
|
||||
|
||||
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.
|
||||
@@ -575,7 +535,7 @@ Creation announcement during session publication. A synchronous throw vetoes and
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [Session](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/session/src/index.ts:71`](../../packages/core/session/src/index.ts)
|
||||
Source: [`packages/core/session/src/index.ts:73`](../../packages/core/session/src/index.ts)
|
||||
|
||||
### `session/disposed` — emit
|
||||
|
||||
@@ -596,7 +556,7 @@ Emitted once when an announced session leaves the store, including publication r
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [Session](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/session/src/index.ts:81`](../../packages/core/session/src/index.ts)
|
||||
Source: [`packages/core/session/src/index.ts:83`](../../packages/core/session/src/index.ts)
|
||||
|
||||
### `session/event` — emit
|
||||
|
||||
@@ -619,17 +579,16 @@ Post-commit, fire-and-forget append feed. The listener snapshot resolves before
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [Session](../core-data-structures/session.md) · [SessionEvent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/index.ts:93`](../../packages/core/session/src/index.ts)
|
||||
Source: [`packages/core/session/src/index.ts:95`](../../packages/core/session/src/index.ts)
|
||||
|
||||
### `session/flush` — parallel
|
||||
|
||||
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.
|
||||
Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the session's owner scope.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* 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
|
||||
* caller awaits all of them, with no waterfall veto. 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
|
||||
@@ -640,86 +599,88 @@ Awaited parallel durability checkpoint: every listener runs and the caller await
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [Session](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/session/src/index.ts:103`](../../packages/core/session/src/index.ts)
|
||||
Source: [`packages/core/session/src/index.ts:104`](../../packages/core/session/src/index.ts)
|
||||
|
||||
## `slash/*`
|
||||
## `settings/*`
|
||||
|
||||
### `slash/input-begin-command` — bail
|
||||
### `settings/document-updated` — emit
|
||||
|
||||
Applies one command claim to the scoped Input. Dispatched with the session's scope carrier; the owning session's input listener returns `true` only after the phase and span CAS checks pass and the machine actually mutated — producers treat anything else as "not applied".
|
||||
One registered namespace's RAW user section changed, whether or not the resolved value did. `settings/updated` is the consumer-facing event and stays deep-equal-gated; this one exists for configuration surfaces, which must learn that a field went from inherited to overridden (same resolved value, different meaning) and that their held revision is stale. Listener containment matches `settings/updated`.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Applies one command claim to the scoped Input. Dispatched with the
|
||||
* session's scope carrier; the owning session's input listener returns
|
||||
* `true` only after the phase and span CAS checks pass and the machine
|
||||
* actually mutated — producers treat anything else as "not applied".
|
||||
* @param request - Claim and menu-time span CAS.
|
||||
* @mode bail
|
||||
* One registered namespace's RAW user section changed, whether or not the
|
||||
* resolved value did. `settings/updated` is the consumer-facing event and
|
||||
* stays deep-equal-gated; this one exists for configuration surfaces,
|
||||
* which must learn that a field went from inherited to overridden (same
|
||||
* resolved value, different meaning) and that their held revision is
|
||||
* stale. Listener containment matches `settings/updated`.
|
||||
* @param ns - the namespace whose stored section changed.
|
||||
* @param revision - the namespace's new revision.
|
||||
* @mode emit
|
||||
*/
|
||||
'slash/input-begin-command'(request: BeginCommandRequest): true | undefined
|
||||
'settings/document-updated'(ns: SettingsNamespace, revision: number): void
|
||||
```
|
||||
|
||||
Source: [`packages/client/ui-slash/src/types.ts:230`](../../packages/client/ui-slash/src/types.ts)
|
||||
Types: [SettingsNamespace](../core-data-structures/settings.md)
|
||||
|
||||
### `slash/input-consume-token` — bail
|
||||
Source: [`packages/settings/settings/src/index.ts:150`](../../packages/settings/settings/src/index.ts)
|
||||
|
||||
Consumes one command token after business success (popup settle / menu-pick execute). Same carrier routing and applied-truth contract.
|
||||
### `settings/updated` — emit
|
||||
|
||||
Committed change to one registered namespace's resolved value. Emitted after the provider persisted (for `update`) or published (`provider`) the change; never emitted when the resolved value is deep-equal. Listener failures are contained and logged — a sync throw and an async rejection alike — except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Consumes one command token after business success (popup settle /
|
||||
* menu-pick execute). Same carrier routing and applied-truth contract.
|
||||
* @param request - Exact span or bare-token guard.
|
||||
* @mode bail
|
||||
* Committed change to one registered namespace's resolved value. Emitted
|
||||
* after the provider persisted (for `update`) or published (`provider`)
|
||||
* the change; never emitted when the resolved value is deep-equal.
|
||||
* Listener failures are contained and logged — a sync throw and an async
|
||||
* rejection alike — except `INVARIANT`-coded failures, which rethrow
|
||||
* after every listener ran; that rethrow reaches the emitter only from
|
||||
* synchronous listeners, so invariant checks on this event must not be
|
||||
* async functions.
|
||||
* @param ns - the namespace whose resolved value changed.
|
||||
* @param next - the new resolved value.
|
||||
* @param prev - the previous resolved value.
|
||||
* @param source - whether the change entered through `update()` or the provider.
|
||||
* @mode emit
|
||||
*/
|
||||
'slash/input-consume-token'(request: ConsumeTokenRequest): true | undefined
|
||||
'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void
|
||||
```
|
||||
|
||||
Source: [`packages/client/ui-slash/src/types.ts:244`](../../packages/client/ui-slash/src/types.ts)
|
||||
Types: [SettingsNamespace](../core-data-structures/settings.md) · [SettingsUpdateSource](../core-data-structures/settings.md)
|
||||
|
||||
### `slash/input-insert-reference` — bail
|
||||
Source: [`packages/settings/settings/src/index.ts:137`](../../packages/settings/settings/src/index.ts)
|
||||
|
||||
Inserts one reference into the scoped Input (same carrier routing and applied-truth contract as begin-command).
|
||||
## `skills/*`
|
||||
|
||||
### `skills/change` — emit
|
||||
|
||||
A skill provider, runtime contribution, or provider-backed catalog may have changed. This is an unfiltered invalidation notification; consumers refetch the catalog for their own lookup options. Listener failures are contained and cannot veto the registry mutation.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Inserts one reference into the scoped Input (same carrier routing and
|
||||
* applied-truth contract as begin-command).
|
||||
* @param request - Reference and menu-time span CAS.
|
||||
* @mode bail
|
||||
* A skill provider, runtime contribution, or provider-backed catalog may
|
||||
* have changed. This is an unfiltered invalidation notification; consumers
|
||||
* refetch the catalog for their own lookup options. Listener failures are
|
||||
* contained and cannot veto the registry mutation.
|
||||
* @mode emit
|
||||
*/
|
||||
'slash/input-insert-reference'(request: InsertReferenceRequest): true | undefined
|
||||
'skills/change'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/client/ui-slash/src/types.ts:237`](../../packages/client/ui-slash/src/types.ts)
|
||||
|
||||
### `slash/input-insert-text` — bail
|
||||
|
||||
Replaces the trigger token span with literal text — the plain-text reference path (decision 21). Same carrier routing and applied-truth contract; the draft gains ordinary characters, no occurrence entry.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Replaces the trigger token span with literal text — the plain-text
|
||||
* reference path (decision 21). Same carrier routing and applied-truth
|
||||
* contract; the draft gains ordinary characters, no occurrence entry.
|
||||
* @param request - Replacement text and menu-time span CAS.
|
||||
* @mode bail
|
||||
*/
|
||||
'slash/input-insert-text'(request: InsertTextRequest): true | undefined
|
||||
```
|
||||
|
||||
Source: [`packages/client/ui-slash/src/types.ts:252`](../../packages/client/ui-slash/src/types.ts)
|
||||
Source: [`packages/skill/skill/src/index.ts:188`](../../packages/skill/skill/src/index.ts)
|
||||
|
||||
## `subagent/*`
|
||||
|
||||
### `subagent/end` — emit
|
||||
|
||||
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.
|
||||
A published child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* A ready child settled. Scope-filtered dispatch uses the same delegating
|
||||
* A published 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.
|
||||
@@ -731,7 +692,7 @@ A ready child settled. Scope-filtered dispatch uses the same delegating parent c
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:140`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:160`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
### `subagent/provider-added` — emit
|
||||
|
||||
@@ -748,7 +709,7 @@ A provider became resolvable in the registry.
|
||||
|
||||
Types: [SubagentProvider](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:114`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:134`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
### `subagent/provider-removed` — emit
|
||||
|
||||
@@ -763,20 +724,20 @@ A provider left the registry. Accepted runs remain holder-owned.
|
||||
'subagent/provider-removed'(name: string): void
|
||||
```
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:120`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:140`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
### `subagent/start` — emit
|
||||
|
||||
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`.
|
||||
A provider established a published 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`.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* A provider established a ready child. For in-process providers,
|
||||
* A provider established a published 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.
|
||||
* @param info - the provider and published child identity.
|
||||
* @dshScopeScan unsupported
|
||||
* @mode emit
|
||||
*/
|
||||
@@ -785,17 +746,17 @@ A provider established a ready child. For in-process providers, `ctx.agents.get(
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [SubagentService](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:131`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:151`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
## `system-prompt/*`
|
||||
|
||||
### `system-prompt/assemble` — waterfall
|
||||
|
||||
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. A supplied signal controls only this explicit assembly request and must not be retained to control later turns.
|
||||
Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Expert waterfall over the assembled sections, tools, and variables.
|
||||
* Expert waterfall over the assembled sections, contexts, tools, and variables.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
|
||||
* receive only that scope's assemblies. The returned value is authoritative.
|
||||
* A supplied signal controls only this explicit assembly request and must not
|
||||
@@ -874,7 +835,7 @@ A tool was registered or unregistered, or a scoped restriction changed (the avai
|
||||
'tools/change'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:156`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:167`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
### `tools/code-dispatch-log` — waterfall
|
||||
|
||||
@@ -898,7 +859,7 @@ Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bri
|
||||
|
||||
Types: [CodeDispatchLog](../core-data-structures/tools.md) · [ContentBlock](../core-data-structures/core.md) · [Scoped](../core-data-structures/scope.md) · [ToolRegistry](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:138`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:149`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
### `tools/execute` — waterfall
|
||||
|
||||
@@ -920,7 +881,7 @@ Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a nor
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [ToolDispatchExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:113`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:124`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
### `tools/post-execute` — waterfall
|
||||
|
||||
@@ -943,7 +904,7 @@ Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts
|
||||
|
||||
Types: [PostToolDecision](../core-data-structures/tools.md) · [Scoped](../core-data-structures/scope.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:125`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:136`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
### `tools/pre-execute` — waterfall
|
||||
|
||||
@@ -964,7 +925,7 @@ Allow, deny, or ask before dispatch. `next()` delegates to allow; missing approv
|
||||
|
||||
Types: [PreToolDecision](../core-data-structures/tools.md) · [Scoped](../core-data-structures/scope.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:102`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:113`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
### `tools/result` — emit
|
||||
|
||||
@@ -983,7 +944,7 @@ Observe the frozen, lossless-JSON final outcome. Listener failures are contained
|
||||
|
||||
Types: [Scoped](../core-data-structures/scope.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolRegistry](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:146`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:157`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
## `workflow/*`
|
||||
|
||||
@@ -1011,13 +972,13 @@ Source: [`packages/workflow/workflow/src/index.ts:81`](../../packages/workflow/w
|
||||
|
||||
### `workflow/agent-start` — emit
|
||||
|
||||
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.
|
||||
One `agent()` call established a published child run. Paired with Events['workflow/agent-end'] by `agent.seq`. A call that never receives a published run from the provider emits neither event in this pair.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* One `agent()` call established a ready child run. Paired with
|
||||
* One `agent()` call established a published 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
|
||||
* receives a published 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.
|
||||
@@ -1119,7 +1080,8 @@ The framework events every plugin also sees, beyond the harness vocabulary above
|
||||
- `internal/listener` — A listener was registered. ([`vendor/cordis/src/events.ts:340`](../../vendor/cordis/src/events.ts))
|
||||
- `internal/dispatch` — An event is being dispatched to listeners. ([`vendor/cordis/src/events.ts:342`](../../vendor/cordis/src/events.ts))
|
||||
- `hmr/change` — A watched source file changed on disk. ([`vendor/hmr/src/index.ts:20`](../../vendor/hmr/src/index.ts))
|
||||
- `hmr/reload` — Plugins are being reloaded after a change. ([`vendor/hmr/src/index.ts:21`](../../vendor/hmr/src/index.ts))
|
||||
- `hmr/reload` — Plugins are being reloaded after a change. ([`vendor/hmr/src/index.ts:22`](../../vendor/hmr/src/index.ts))
|
||||
- `hmr/config-update-failed` — A watched config-file refresh failed. ([`vendor/hmr/src/index.ts:29`](../../vendor/hmr/src/index.ts))
|
||||
- `exit` — The process is exiting on a signal. ([`vendor/loader/src/index.ts:23`](../../vendor/loader/src/index.ts))
|
||||
- `loader/config-update` — The loader config tree changed. ([`vendor/loader/src/index.ts:24`](../../vendor/loader/src/index.ts))
|
||||
- `loader/entry-init` — A config entry is being initialized. ([`vendor/loader/src/index.ts:25`](../../vendor/loader/src/index.ts))
|
||||
|
||||
@@ -44,7 +44,7 @@ async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandl
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [AgentOptions](../core-data-structures/core.md) · [SessionHeader](../core-data-structures/persistence.md) · [SessionId](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent-loop/src/index.ts:196`](../../packages/core/agent-loop/src/index.ts)
|
||||
Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts)
|
||||
|
||||
## `ctx.agents` — `AgentRegistry`
|
||||
|
||||
@@ -216,13 +216,22 @@ roots(): Agent[]
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [SessionId](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/agent/src/index.ts:215`](../../packages/core/agent/src/index.ts)
|
||||
Source: [`packages/core/agent/src/index.ts:242`](../../packages/core/agent/src/index.ts)
|
||||
|
||||
## `ctx.approval` — `ApprovalService`
|
||||
|
||||
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.
|
||||
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 the runtime-context snapshot and switch notices.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Switch one live agent's policy and queue the transition for its next model
|
||||
* step. Session initialization uses {@link setApprovalPolicy} directly
|
||||
* because there is no previously visible policy to change.
|
||||
* @param agent - the live agent whose policy is changing.
|
||||
* @param policy - the new effective policy.
|
||||
*/
|
||||
setPolicy(agent: Agent, policy: ApprovalPolicy): void
|
||||
|
||||
/**
|
||||
* Ask the composed answerers to decide one readonly same-process request.
|
||||
* The service borrows the request, agent, session, and live signal directly.
|
||||
@@ -251,9 +260,9 @@ async request(req: ApprovalRequest): Promise<ApprovalOutcome>
|
||||
overrideOf(session: Session): ApprovalPolicy | undefined
|
||||
```
|
||||
|
||||
Types: [ApprovalOutcome](../core-data-structures/approval.md) · [ApprovalPolicy](../core-data-structures/approval.md) · [ApprovalRequest](../core-data-structures/approval.md) · [Session](../core-data-structures/session.md)
|
||||
Types: [Agent](../core-data-structures/core.md) · [ApprovalOutcome](../core-data-structures/approval.md) · [ApprovalPolicy](../core-data-structures/approval.md) · [ApprovalRequest](../core-data-structures/approval.md) · [Session](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/ui/user-approval/src/index.ts:217`](../../packages/ui/user-approval/src/index.ts)
|
||||
Source: [`packages/ui/user-approval/src/index.ts:193`](../../packages/ui/user-approval/src/index.ts)
|
||||
|
||||
## `ctx.bash` — `BashExecutor` (abstract seam)
|
||||
|
||||
@@ -293,11 +302,11 @@ 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:51`](../../packages/bash/bash/src/index.ts)
|
||||
Source: [`packages/bash/bash/src/index.ts:53`](../../packages/bash/bash/src/index.ts)
|
||||
|
||||
## `ctx.bashEnv` — `BashEnvRegistry`
|
||||
|
||||
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.
|
||||
Registry (`ctx.bashEnv`) for trusted, per-execution `DSH_*` variables. The namespace is rebuilt for every model shell 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.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -309,7 +318,7 @@ Registry (`ctx.bashEnv`) for trusted, per-execution `DSH_*` variables. The names
|
||||
register(contributor: BashEnvContributor): () => void
|
||||
|
||||
/**
|
||||
* Build the trusted `DSH_*` snapshot for one bash tool execution.
|
||||
* Build the trusted `DSH_*` snapshot for one shell tool execution.
|
||||
* @param execution - the current tool execution.
|
||||
* @returns an immutable environment overlay containing built-ins and current contributions.
|
||||
*/
|
||||
@@ -324,11 +333,11 @@ list(): BashEnvVariableInfo[]
|
||||
|
||||
Types: [DshEnvironment](../core-data-structures/subprocess.md) · [ToolExecution](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/bash/tool-bash/src/index.ts:104`](../../packages/bash/tool-bash/src/index.ts)
|
||||
Source: [`packages/bash/bash-env/src/index.ts:89`](../../packages/bash/bash-env/src/index.ts)
|
||||
|
||||
## `ctx.clientModuleHost` — `ClientModuleHostService`
|
||||
|
||||
The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot sweep reports it).
|
||||
The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -368,7 +377,7 @@ onRebuilt(listener: (id: string, rev: string) => void): () => void
|
||||
onGraphChanged(listener: () => void): () => void
|
||||
```
|
||||
|
||||
Source: [`packages/client/modules/src/index.ts:143`](../../packages/client/modules/src/index.ts)
|
||||
Source: [`packages/client/modules/src/index.ts:184`](../../packages/client/modules/src/index.ts)
|
||||
|
||||
## `ctx.codeRuntime` — `CodeRuntime` (abstract seam)
|
||||
|
||||
@@ -463,6 +472,27 @@ Abstract compaction service. Implementations own trigger policy, retention, and
|
||||
*/
|
||||
abstract compactIfNeeded( agent: CompactAgentContext, trigger: CompactionTrigger, signal: AbortSignal, ): Promise<CompactionResult | null>
|
||||
|
||||
/**
|
||||
* Explicitly compact useful history even below automatic pressure thresholds.
|
||||
* Implementations synchronously start an idle task before any asynchronous
|
||||
* work, select a useful range without writing on a no-op, then
|
||||
* append a standalone `compact/start` before summarization. That durable
|
||||
* marker is the compaction lock until one `compact/end` attempt. Later waking
|
||||
* prompts remain accepted in FIFO order and start only after the optional
|
||||
* durability checkpoint and idle-task settlement. Context injected while the
|
||||
* summary runs may sit between the marker pair; only the selected span must
|
||||
* remain stable.
|
||||
*
|
||||
* @param agent - idle agent whose durable history should be compacted.
|
||||
* @param signal - cancellation scoped to this compaction request.
|
||||
* @returns the compaction result, or `null` when no safe useful range exists.
|
||||
* @throws {@link ManualCompactionError} for expected busy, agent-cancellation,
|
||||
* changed-span, summarization/shrink, commit-stage, or persistence failures;
|
||||
* an aborted request preserves its exact abort reason. Failed attempts remain
|
||||
* visible in the log.
|
||||
*/
|
||||
abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): Promise<CompactionResult | null>
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -486,7 +516,53 @@ abstract compactRegion( start: number, end: number, agent: CompactAgentContext,
|
||||
|
||||
Types: [CompactionResult](../core-data-structures/compaction.md) · [CompactionTrigger](../core-data-structures/compaction.md)
|
||||
|
||||
Source: [`packages/compact/compact/src/index.ts:54`](../../packages/compact/compact/src/index.ts)
|
||||
Source: [`packages/compact/compact/src/index.ts:93`](../../packages/compact/compact/src/index.ts)
|
||||
|
||||
## `ctx.credentials` — `Credentials` (abstract seam)
|
||||
|
||||
Abstract credential service. Providers implement the four operations over their source layers; one seam-wide rule binds them all: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Resolve one reference to its current value. Resolution is per call:
|
||||
* consumers re-resolve at each operation and must not cache across
|
||||
* operations — that per-operation read is what makes a changed credential
|
||||
* reach the next operation without a restart.
|
||||
* @param ref - the reference to resolve.
|
||||
* @returns the value and its source, or `undefined` while unconfigured.
|
||||
*/
|
||||
abstract resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined>
|
||||
|
||||
/**
|
||||
* Describe one reference for configuration surfaces without exposing the
|
||||
* value.
|
||||
* @param ref - the reference to describe.
|
||||
* @returns configured state, supplying source, and writability.
|
||||
*/
|
||||
abstract describe(ref: CredentialRef): Promise<CredentialInfo>
|
||||
|
||||
/**
|
||||
* Durably store one value in the provider-managed writable source. Rejects
|
||||
* while a read-only source shadows the reference — the write would appear
|
||||
* to succeed while resolution keeps returning the shadowing value — and
|
||||
* rejects an empty value (use {@link unset}).
|
||||
* @param ref - the reference to store.
|
||||
* @param value - the non-empty secret value.
|
||||
*/
|
||||
abstract set(ref: CredentialRef, value: string): Promise<void>
|
||||
|
||||
/**
|
||||
* Remove one reference from the provider-managed writable source; removing
|
||||
* an absent reference is a no-op. Rejects while a read-only source shadows
|
||||
* the reference, like {@link set}.
|
||||
* @param ref - the reference to remove.
|
||||
*/
|
||||
abstract unset(ref: CredentialRef): Promise<void>
|
||||
```
|
||||
|
||||
Types: [CredentialInfo](../core-data-structures/credentials.md) · [CredentialRef](../core-data-structures/credentials.md) · [ResolvedCredential](../core-data-structures/credentials.md)
|
||||
|
||||
Source: [`packages/credentials/credentials/src/index.ts:77`](../../packages/credentials/credentials/src/index.ts)
|
||||
|
||||
## `ctx.directoryPicker` — `DirectoryPicker` (abstract seam)
|
||||
|
||||
@@ -689,7 +765,7 @@ clear(agent: Agent, ref: GoalRef): GoalRef
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md) · [CreateGoalRequest](../core-data-structures/goal.md) · [EditGoalRequest](../core-data-structures/goal.md) · [GoalBlockReason](../core-data-structures/goal.md) · [GoalRef](../core-data-structures/goal.md) · [GoalView](../core-data-structures/goal.md)
|
||||
|
||||
Source: [`packages/goal/goal/src/index.ts:197`](../../packages/goal/goal/src/index.ts)
|
||||
Source: [`packages/goal/goal/src/index.ts:181`](../../packages/goal/goal/src/index.ts)
|
||||
|
||||
## `ctx.httpServer` — `HttpServerService`
|
||||
|
||||
@@ -704,6 +780,14 @@ The web-shape HTTP carrier service. Activation listens immediately (route regist
|
||||
*/
|
||||
register(route: WebRoute): () => void
|
||||
|
||||
/**
|
||||
* Register an exact-path HTTP upgrade route. Duplicate paths throw because
|
||||
* one socket can have only one protocol owner.
|
||||
* @param route - pathname and handler owning negotiation plus socket use.
|
||||
* @returns the disposer removing the route.
|
||||
*/
|
||||
registerUpgrade(route: WebUpgradeRoute): () => void
|
||||
|
||||
/**
|
||||
* Register an index.html transform, applied to every index response in
|
||||
* registration order.
|
||||
@@ -713,7 +797,7 @@ register(route: WebRoute): () => void
|
||||
tapIndex(transform: (html: string) => string): () => void
|
||||
```
|
||||
|
||||
Source: [`packages/host/webserver/src/index.ts:55`](../../packages/host/webserver/src/index.ts)
|
||||
Source: [`packages/host/webserver/src/index.ts:63`](../../packages/host/webserver/src/index.ts)
|
||||
|
||||
## `ctx.invariants` — `InvariantService`
|
||||
|
||||
@@ -744,9 +828,9 @@ The abstract `llm` service: an adapter registry plus a streaming model-call surf
|
||||
* 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.
|
||||
* @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
|
||||
*/
|
||||
registerAdapter(providers: string[], adapter: LlmAdapter): () => void
|
||||
registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle
|
||||
|
||||
/**
|
||||
* Describe provider routes with a registered adapter.
|
||||
@@ -754,6 +838,22 @@ registerAdapter(providers: string[], adapter: LlmAdapter): () => void
|
||||
*/
|
||||
listProviders(): LlmProviderInfo[]
|
||||
|
||||
/**
|
||||
* Declare provider routes an adapter plugin can activate through
|
||||
* configuration. Registration is all-or-nothing: an empty list, invalid
|
||||
* entry, or a provider already declared by any registration throws
|
||||
* `LlmError` without registering the rest. Disposed with the fiber.
|
||||
* @param entries - every configurable provider this plugin owns.
|
||||
* @returns the disposer that withdraws all of them.
|
||||
*/
|
||||
registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): () => void
|
||||
|
||||
/**
|
||||
* List every declared configurable provider, registered or dormant.
|
||||
* @returns detached directory entries in declaration order.
|
||||
*/
|
||||
listConfigurableProviders(): LlmConfigurableProvider[]
|
||||
|
||||
/**
|
||||
* Resolve the retry policy captured when one provider route was registered.
|
||||
* @param provider - registered provider route to inspect.
|
||||
@@ -782,7 +882,7 @@ async resolveModelInfo( provider: string, model: string, signal?: AbortSignal, )
|
||||
|
||||
/**
|
||||
* Validate a conversation call config against its exact model capability and
|
||||
* materialize an adapter-configured default. Unsupported explicit efforts
|
||||
* materialize adapter-configured defaults. Unsupported explicit efforts
|
||||
* reject before provider I/O; no clamping or aliasing is performed. This
|
||||
* standalone query does not bind a later dispatch; use {@link prepareCall}
|
||||
* when logging and streaming must share one adapter registration.
|
||||
@@ -803,24 +903,22 @@ async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<Ll
|
||||
async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>
|
||||
|
||||
/**
|
||||
* 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 remains fixed through asynchronous exact-model resolution
|
||||
* and dispatch. 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.
|
||||
* Stream one model call as raw chunks (token-level deltas). Replay state is
|
||||
* retained only when the same adapter instance owns its historical provider
|
||||
* and the target provider. Final adapter selection remains fixed through
|
||||
* asynchronous exact-model resolution and dispatch. Adapter selection,
|
||||
* dispatch, and iteration failures become terminal `error` or `aborted`
|
||||
* finish chunks; middleware, nested-call, cleanup, and consumer failures
|
||||
* remain thrown.
|
||||
* @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>
|
||||
```
|
||||
|
||||
Types: [GenerateOptions](../core-data-structures/core.md) · [LlmAdapter](../core-data-structures/llm-streaming.md) · [LlmCallConfig](../core-data-structures/core.md) · [LlmModelInfo](../core-data-structures/core.md) · [LlmProviderInfo](../core-data-structures/core.md) · [LlmResolvedModelInfo](../core-data-structures/core.md) · [PreparedLlmCall](../core-data-structures/llm-streaming.md) · [ResolvedRetryPolicy](../core-data-structures/llm-streaming.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
|
||||
Types: [AdapterRegistrationHandle](../core-data-structures/core.md) · [GenerateOptions](../core-data-structures/core.md) · [LlmAdapter](../core-data-structures/llm-streaming.md) · [LlmCallConfig](../core-data-structures/core.md) · [LlmConfigurableProvider](../core-data-structures/core.md) · [LlmModelInfo](../core-data-structures/core.md) · [LlmProviderInfo](../core-data-structures/core.md) · [LlmResolvedModelInfo](../core-data-structures/core.md) · [PreparedLlmCall](../core-data-structures/llm-streaming.md) · [ResolvedRetryPolicy](../core-data-structures/llm-streaming.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
|
||||
|
||||
Source: [`packages/llm/llm/src/index.ts:191`](../../packages/llm/llm/src/index.ts)
|
||||
Source: [`packages/llm/llm/src/index.ts:232`](../../packages/llm/llm/src/index.ts)
|
||||
|
||||
## `ctx.permission` — `PermissionService`
|
||||
|
||||
@@ -872,7 +970,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:144`](../../packages/ui/permission/src/index.ts)
|
||||
Source: [`packages/ui/permission/src/index.ts:159`](../../packages/ui/permission/src/index.ts)
|
||||
|
||||
## `ctx.planMode` — `PlanModeService`
|
||||
|
||||
@@ -908,7 +1006,7 @@ set(agent: Agent, active: boolean): 'committed' | 'queued' | 'cancelled' | 'noop
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/plan/plan-mode/src/index.ts:179`](../../packages/plan/plan-mode/src/index.ts)
|
||||
Source: [`packages/plan/plan-mode/src/index.ts:183`](../../packages/plan/plan-mode/src/index.ts)
|
||||
|
||||
## `ctx.pty` — `PtyService`
|
||||
|
||||
@@ -978,7 +1076,7 @@ signal(owner: Agent, id: PtySessionId, signal: PtySignal): Promise<PtySignalResu
|
||||
* @param reason - diagnostic cleanup reason.
|
||||
* @returns true for a newly closed session, false when the same close is already in flight.
|
||||
*/
|
||||
async kill(owner: Agent, id: PtySessionId, reason = 'model request'): Promise<boolean>
|
||||
async kill(owner: Agent, id: PtySessionId, reason: string = 'model request'): Promise<boolean>
|
||||
|
||||
/**
|
||||
* List fresh snapshots for exactly one owner.
|
||||
@@ -1013,11 +1111,11 @@ 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:131`](../../packages/sandbox/sandbox/src/index.ts)
|
||||
Source: [`packages/sandbox/sandbox/src/index.ts:148`](../../packages/sandbox/sandbox/src/index.ts)
|
||||
|
||||
## `ctx.sandboxPolicy` — `SandboxPolicyService`
|
||||
|
||||
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode and fallback workspace root. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
|
||||
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -1041,7 +1139,7 @@ overrideOf(session: Session): SandboxMode | undefined
|
||||
|
||||
Types: [SandboxExecutionPolicy](../core-data-structures/sandbox.md) · [SandboxMode](../core-data-structures/sandbox.md) · [SandboxPolicyRequest](../core-data-structures/sandbox.md) · [Session](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/sandbox/sandbox-policy/src/index.ts:68`](../../packages/sandbox/sandbox-policy/src/index.ts)
|
||||
Source: [`packages/sandbox/sandbox-policy/src/index.ts:91`](../../packages/sandbox/sandbox-policy/src/index.ts)
|
||||
|
||||
## `ctx.sessionPersistence` — `SessionPersistence` (abstract seam)
|
||||
|
||||
@@ -1077,46 +1175,59 @@ abstract create(meta: SessionHeader): Promise<void>
|
||||
abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void>
|
||||
|
||||
/**
|
||||
* 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. Implementations
|
||||
* MUST NOT crash-repair an identity still bound to a live Session: a balanced
|
||||
* live log may return with its stored header as a durable snapshot, while an
|
||||
* open live turn rejects.
|
||||
* A coordinator-backed cold load reserves the identity across storage awaits,
|
||||
* so concurrent publication of a same-id live Session rejects.
|
||||
* Returned events are detached, and every identified message is deeply
|
||||
* frozen. Coordinator-backed implementations upgrade supported pre-identity
|
||||
* message events before validation; other malformed messages reject before
|
||||
* any stored event is returned.
|
||||
* Prepare the exact unpublished Session used by resume. Implementations may
|
||||
* reuse object graphs retained by an earlier {@link inspect} after confirming
|
||||
* their durable revision is still current; disposal releases an unpublished
|
||||
* reservation. Revision retries require the durable log to remain unchanged
|
||||
* for one read/check round trip; continuous external writers may delay completion.
|
||||
* @param id - persisted session to prepare.
|
||||
* @param signal - optional cancellation for preparation work.
|
||||
* @returns one owned unpublished Session preparation.
|
||||
*/
|
||||
async prepare(id: SessionId, signal?: AbortSignal): Promise<SessionPreparation>
|
||||
|
||||
/**
|
||||
* Load an immutable balanced logical view and commit any required cold
|
||||
* recovery. 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. Implementations MUST NOT crash-repair an
|
||||
* identity still bound to a live Session: a balanced live log may return as a
|
||||
* durable snapshot, while an open live turn rejects. Returned values may be
|
||||
* shared with immutable live or prepared state and must not be mutated.
|
||||
* Revision-based implementations may wait for one stable read/check round trip.
|
||||
* @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[] }>
|
||||
abstract load(id: SessionId): Promise<SessionInspection>
|
||||
|
||||
/**
|
||||
* Inspect a header and its valid contiguous stored prefix without repairing
|
||||
* a torn tail, closing an interrupted turn, or publishing coordinator state.
|
||||
* This read is serialized with writes for the same id and returns detached
|
||||
* values with upgraded, deeply frozen identified messages, so observers
|
||||
* cannot mutate message identity/content or backend-owned state. Other
|
||||
* malformed messages reject.
|
||||
* Inspect an immutable logical session without committing recovery or
|
||||
* publishing it. A cold complete interrupted turn receives synthetic closers
|
||||
* in memory and a torn physical tail remains untouched. An already-live
|
||||
* Session instead yields its current immutable snapshot, which may contain an
|
||||
* open turn and its `session/end-seed` boundary. Coordinator-backed
|
||||
* implementations retain the exact cold unpublished Session for bounded
|
||||
* reuse by a later {@link prepare}. A stale ready source is reloaded; a source
|
||||
* already committing or reserved for resume remains exclusive, and inspection
|
||||
* may borrow its immutable view. Callers borrow only the immutable header and
|
||||
* log. Continuous external writers may delay revision convergence.
|
||||
* @param id - the persisted session to inspect.
|
||||
* @param signal - optional cancellation for queued and backend read work.
|
||||
* @returns the header and valid stored event prefix exactly as observed.
|
||||
* @returns the validated header and current logical event log.
|
||||
*/
|
||||
abstract inspect(id: SessionId, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
|
||||
abstract inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection>
|
||||
|
||||
/**
|
||||
* Read the stored events from `fromSeq` onward — the read-from-seq
|
||||
* primitive for read models that resume from a watermark (e.g. a persisted
|
||||
* projection cache folding only the tail past its checkpoint). Like
|
||||
* {@link inspect} it is non-mutating and detached: no torn-tail truncation,
|
||||
* no synthetic closers, no coordinator-state publication; only events from
|
||||
* the valid contiguous stored prefix are returned, so a torn fragment never
|
||||
* reaches the caller. `fromSeq` at or beyond the stored prefix returns an
|
||||
* empty event list (never an error). Backends whose medium can seek by seq
|
||||
* projection cache folding only the tail past its checkpoint). Unlike
|
||||
* {@link inspect}, it is a detached physical suffix read: no preparation
|
||||
* cache, torn-tail truncation, synthetic closers, or coordinator-state
|
||||
* publication. Only events from the valid contiguous stored prefix are
|
||||
* returned, so a torn fragment never reaches the caller. `fromSeq` at or
|
||||
* beyond the stored prefix returns an empty event list (never an error).
|
||||
* Backends whose medium can seek by seq
|
||||
* (SQLite) read only the suffix; sequential media (JSONL, both encodings)
|
||||
* still parse the whole artifact and skip forward — the primitive bounds
|
||||
* what is RETURNED and refolded, not every backend's physical read.
|
||||
@@ -1147,9 +1258,9 @@ abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
|
||||
abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
|
||||
```
|
||||
|
||||
Types: [SessionEvent](../core-data-structures/core.md) · [SessionHeader](../core-data-structures/persistence.md) · [SessionId](../core-data-structures/core.md) · [SessionLocation](../core-data-structures/persistence.md) · [SessionPersistenceSnapshot](../core-data-structures/persistence.md)
|
||||
Types: [SessionEvent](../core-data-structures/core.md) · [SessionHeader](../core-data-structures/persistence.md) · [SessionId](../core-data-structures/core.md) · [SessionInspection](../core-data-structures/persistence.md) · [SessionLocation](../core-data-structures/persistence.md) · [SessionPersistenceSnapshot](../core-data-structures/persistence.md) · [SessionPreparation](../core-data-structures/persistence.md)
|
||||
|
||||
Source: [`packages/session-persistence/session-persistence/src/index.ts:52`](../../packages/session-persistence/session-persistence/src/index.ts)
|
||||
Source: [`packages/session-persistence/session-persistence/src/index.ts:70`](../../packages/session-persistence/session-persistence/src/index.ts)
|
||||
|
||||
## `ctx.sessionProjectionCache` — `SessionProjectionCache`
|
||||
|
||||
@@ -1447,7 +1558,7 @@ Exact-read consumer that prepares immutable cross-session message context.
|
||||
* @param signal - optional cancellation boundary for host autocomplete teardown.
|
||||
* @returns candidates labeled by latest title or, when absent, session id.
|
||||
*/
|
||||
async listCandidates( agent: Agent, query = '', limit = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>
|
||||
async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>
|
||||
|
||||
/**
|
||||
* Snapshot all references before enqueue and return one aggregated durable context.
|
||||
@@ -1480,7 +1591,7 @@ Persistence is intentionally not implemented here — persistence plugins subscr
|
||||
* {@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
|
||||
* loop's final events are published 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).
|
||||
@@ -1501,16 +1612,20 @@ create(id?: SessionId, options?: CreateSessionOptions): Session
|
||||
* `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.
|
||||
* before the driver's closing events commit, dropping them.
|
||||
*
|
||||
* @param id - the session id; omitted, the store mints `session-<n>`.
|
||||
* @param options - seed events and/or creation metadata for the header.
|
||||
* @param options - seed events and/or creation metadata for the header. With
|
||||
* `seedSource: 'persistence'`, metadata and events must be fresh detached
|
||||
* graphs whose ownership transfers to this call: they are validated and
|
||||
* frozen in place through {@link Session.fromRestore}, so the caller must
|
||||
* retain no mutable aliases.
|
||||
* @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
|
||||
prepare(id?: SessionId, options?: PrepareSessionOptions): Session
|
||||
|
||||
/**
|
||||
* Enter a {@link prepare}d session into the store: install the module-private
|
||||
@@ -1548,15 +1663,17 @@ announce(session: Session): void
|
||||
/**
|
||||
* 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.
|
||||
* store owns the carrier, so callers (the checkpoint policy's per-request
|
||||
* barrier, goal-session's idle checkpoint, teardown drains, and consumers
|
||||
* that flush themselves before reading storage) 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.
|
||||
* @returns whether at least one durability listener participated, after every
|
||||
* listener has settled successfully.
|
||||
* @throws the first registered listener failure after every listener settles.
|
||||
*/
|
||||
async flush(session: Session): Promise<void>
|
||||
async flush(session: Session): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Look up a live session.
|
||||
@@ -1588,9 +1705,9 @@ list(): Session[]
|
||||
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
|
||||
```
|
||||
|
||||
Types: [CreateSessionOptions](../core-data-structures/persistence.md) · [Session](../core-data-structures/session.md) · [SessionId](../core-data-structures/core.md)
|
||||
Types: [CreateSessionOptions](../core-data-structures/persistence.md) · [PrepareSessionOptions](../core-data-structures/persistence.md) · [Session](../core-data-structures/session.md) · [SessionId](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/index.ts:694`](../../packages/core/session/src/index.ts)
|
||||
Source: [`packages/core/session/src/index.ts:800`](../../packages/core/session/src/index.ts)
|
||||
|
||||
## `ctx.sessionTitle` — `SessionTitleService`
|
||||
|
||||
@@ -1604,6 +1721,19 @@ Log-backed title fold plus asynchronous fallback generation.
|
||||
*/
|
||||
get(session: Session): SessionTitleSnapshot | undefined
|
||||
|
||||
/**
|
||||
* Accept an explicit user title. Appends a `session/title` event with the
|
||||
* `user` source, which pins the title: in-flight automatic generation is
|
||||
* superseded and later user messages schedule none (an explicit
|
||||
* {@link SessionTitleService.refresh} remains the deliberate unpin).
|
||||
* @param session - exact live session to rename.
|
||||
* @param title - raw user input; normalized before acceptance.
|
||||
* @returns the accepted title snapshot.
|
||||
* @throws {SessionTitleInvalidError} when the title normalizes to empty.
|
||||
* @throws {Error} when the session is not live or the service is disposed.
|
||||
*/
|
||||
rename(session: Session, title: string): SessionTitleSnapshot
|
||||
|
||||
/**
|
||||
* Explicitly retry the registered provider, or materialize the built-in
|
||||
* fallback when no provider is registered.
|
||||
@@ -1624,41 +1754,136 @@ register(provider: SessionTitleProvider): () => Promise<void>
|
||||
|
||||
Types: [Session](../core-data-structures/session.md) · [SessionTitleProvider](../core-data-structures/session-title.md) · [SessionTitleSnapshot](../core-data-structures/session-title.md)
|
||||
|
||||
Source: [`packages/session-title/session-title/src/index.ts:240`](../../packages/session-title/session-title/src/index.ts)
|
||||
Source: [`packages/session-title/session-title/src/index.ts:261`](../../packages/session-title/session-title/src/index.ts)
|
||||
|
||||
## `ctx.settings` — `Settings` (abstract seam)
|
||||
|
||||
Abstract settings service. Providers implement raw-document storage (`load`/`persist`) and push external changes through Settings.publish; the base class owns namespace registration, resolution, validation, change detection, and the `settings/updated` commit event.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Prepare the provider's user-editable document for a native editor. File
|
||||
* providers may materialize an absent document before returning its path;
|
||||
* non-file providers return undefined.
|
||||
* @returns the absolute local document path, or undefined for non-file storage.
|
||||
*/
|
||||
prepareDocument(): Promise<string | undefined>
|
||||
|
||||
/**
|
||||
* Register a namespace schema and receive its owner scope. The registration
|
||||
* is an effect on the calling plugin's fiber: disposing that fiber removes
|
||||
* the namespace and its observers. An invalid stored section fails the
|
||||
* registration itself — the earliest point where the schema can judge it.
|
||||
* @param ns - unique namespace; duplicate registration fails loud.
|
||||
* @param schema - schemastery schema resolving this namespace's value.
|
||||
* @param options - composition `base` layer and effect timing.
|
||||
* @returns the owner scope for reads, observation, and updates.
|
||||
*/
|
||||
register<T>(ns: SettingsNamespace, schema: z<T>, options?: SettingsRegisterOptions<T>): SettingsScope<T>
|
||||
|
||||
/**
|
||||
* Describe every registered namespace for configuration surfaces, including
|
||||
* the composition `base` and raw user layers so a form can mark which fields
|
||||
* the user overrode (presence in `user`) and what a reset returns to.
|
||||
* @param options - redaction switch; wire surfaces must redact.
|
||||
* @returns one descriptor per registered namespace, in registration order.
|
||||
*/
|
||||
describe(options?: SettingsDescribeOptions): SettingsDescriptor[]
|
||||
|
||||
/**
|
||||
* Read one registered namespace's resolved value.
|
||||
* @param ns - the namespace to read.
|
||||
* @returns the resolved value, or `undefined` while unregistered.
|
||||
*/
|
||||
get(ns: SettingsNamespace): unknown
|
||||
|
||||
/**
|
||||
* Merge a patch into one registered namespace's user layer, validate the
|
||||
* resolved candidate, persist through the provider, then commit and emit.
|
||||
* A validation failure rejects before anything is persisted. Writes to one
|
||||
* namespace are serialized: concurrent updates apply in call order, each
|
||||
* merging over the previous write's committed section.
|
||||
* @param ns - the registered namespace to update.
|
||||
* @param patch - plain-object patch over the user section.
|
||||
* @param expectedRevision - the descriptor `revision` the caller read; a
|
||||
* namespace that moved past it rejects with {@link SettingsConflictError}.
|
||||
*/
|
||||
async update(ns: SettingsNamespace, patch: object, expectedRevision?: number): Promise<void>
|
||||
|
||||
/**
|
||||
* Replace one registered namespace's user section wholesale, validate,
|
||||
* persist, then commit and emit. Keys absent from `section` fall back to the
|
||||
* composition `base` and schema defaults — this is the removal/reset path a
|
||||
* merge-only patch cannot express (`replace({})` re-inherits everything).
|
||||
* @param ns - the registered namespace to replace.
|
||||
* @param section - the complete next user section.
|
||||
* @param expectedRevision - the descriptor `revision` the caller read; a
|
||||
* namespace that moved past it rejects with {@link SettingsConflictError}.
|
||||
*/
|
||||
async replace(ns: SettingsNamespace, section: object, expectedRevision?: number): Promise<void>
|
||||
|
||||
/**
|
||||
* Apply path-addressed edits to one registered namespace's user section,
|
||||
* validate, persist, then commit and emit. The ops are applied to the
|
||||
* section as it stands when the write reaches the front of the queue, so a
|
||||
* caller never has to restate fields it did not touch — and, crucially,
|
||||
* cannot delete fields it never saw. This is the write path for any caller
|
||||
* holding a redacted view; `replace` remains the wholesale reset.
|
||||
* @param ns - the registered namespace to edit.
|
||||
* @param ops - ordered path edits; later ops observe earlier ones.
|
||||
* @param expectedRevision - the descriptor `revision` the caller read; a
|
||||
* namespace that moved past it rejects with {@link SettingsConflictError}.
|
||||
*/
|
||||
async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise<void>
|
||||
```
|
||||
|
||||
Types: [SettingsDescribeOptions](../core-data-structures/settings.md) · [SettingsDescriptor](../core-data-structures/settings.md) · [SettingsNamespace](../core-data-structures/settings.md) · [SettingsPathOp](../core-data-structures/settings.md) · [SettingsRegisterOptions](../core-data-structures/settings.md) · [SettingsScope](../core-data-structures/settings.md)
|
||||
|
||||
Source: [`packages/settings/settings/src/index.ts:365`](../../packages/settings/settings/src/index.ts)
|
||||
|
||||
## `ctx.skills` — `SkillService`
|
||||
|
||||
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.
|
||||
Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted invocation-neutral summaries, and loads full skill bodies on demand.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* 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`.
|
||||
* @param create - synchronous factory receiving this registration's lifecycle and invalidation control.
|
||||
* @returns the exact Cordis effect disposer that unregisters this provider;
|
||||
* composite effects may yield it directly to preserve teardown ordering.
|
||||
*/
|
||||
registerProvider(provider: SkillProvider): () => void
|
||||
registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => 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.
|
||||
* @param skill - the complete skill definition to expose for discovery.
|
||||
* @param skill - the skill definition input; omitted invocation and provider fields receive defaults.
|
||||
* @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
|
||||
*/
|
||||
register(skill: SkillRegistration): () => void
|
||||
|
||||
/**
|
||||
* List model-invocable skill summaries for a workspace. Lookup options and
|
||||
* provider candidates are readonly same-process values borrowed throughout
|
||||
* discovery.
|
||||
* List invocation-neutral skill summaries for a workspace. Consumers apply
|
||||
* model or user invocation policy at their operational boundary. 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.
|
||||
* @returns all sorted winning summaries.
|
||||
*/
|
||||
async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]>
|
||||
|
||||
/**
|
||||
* Observe the current invocation-neutral catalog and whether discovery completed within a stable revision.
|
||||
* Incomplete observations are never cached, allowing consumers to retain last-good state and
|
||||
* retry on their next request boundary.
|
||||
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
|
||||
* @returns sorted summaries plus discovery-completeness state.
|
||||
*/
|
||||
async snapshot(options: SkillLookupOptions = {}): Promise<SkillCatalogSnapshot>
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1670,9 +1895,9 @@ async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]>
|
||||
async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined>
|
||||
```
|
||||
|
||||
Types: [SkillDefinition](../core-data-structures/skills.md) · [SkillLookupOptions](../core-data-structures/skills.md) · [SkillProvider](../core-data-structures/skills.md) · [SkillRegistration](../core-data-structures/skills.md) · [SkillSummary](../core-data-structures/skills.md)
|
||||
Types: [SkillCatalogSnapshot](../core-data-structures/skills.md) · [SkillDefinition](../core-data-structures/skills.md) · [SkillLookupOptions](../core-data-structures/skills.md) · [SkillProvider](../core-data-structures/skills.md) · [SkillProviderControl](../core-data-structures/skills.md) · [SkillRegistration](../core-data-structures/skills.md) · [SkillSummary](../core-data-structures/skills.md)
|
||||
|
||||
Source: [`packages/skill/skill/src/index.ts:141`](../../packages/skill/skill/src/index.ts)
|
||||
Source: [`packages/skill/skill/src/index.ts:209`](../../packages/skill/skill/src/index.ts)
|
||||
|
||||
## `ctx.spillStore` — `SpillStore` (abstract seam)
|
||||
|
||||
@@ -1766,9 +1991,92 @@ Source: [`packages/storage/storage-domain/src/index.ts:69`](../../packages/stora
|
||||
|
||||
## `ctx.subagents` — `SubagentService`
|
||||
|
||||
Named provider registry and capability-checked start surface.
|
||||
Named provider registry with one-shot runs, durable discovery, and continuable-child operations.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Establish one durable continuable child and deliver its initial prompt.
|
||||
* Resolves when the child's inbox accepts that prompt, without waiting for the
|
||||
* turn to start or for the message to reach the Session log; any earlier
|
||||
* failure rejects with no ids and rolls back the child entirely.
|
||||
* @param spec - provider, delegation request, and caller cancellation.
|
||||
* @returns the durable child id and the accepted prompt's message id.
|
||||
* @throws when continuation services are unavailable or materialization fails.
|
||||
*/
|
||||
async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>
|
||||
|
||||
/**
|
||||
* Deliver one later message to a continuable child as its next FIFO turn. A
|
||||
* resident child's Agent inbox accepts it directly (waking a `waiting`
|
||||
* Activation), while an absent one is cold-resumed from its persisted
|
||||
* Session. The Agent inbox is the only queue, so every accepted message has
|
||||
* one observable order.
|
||||
* @param parent - the exact live direct parent authorizing this delivery.
|
||||
* @param childId - durable child session id.
|
||||
* @param content - user-role content to deliver.
|
||||
* @param options - durable provenance and caller cancellation, which stops the
|
||||
* operation only before inbox acceptance.
|
||||
* @returns the accepted message's inbox id.
|
||||
* @throws when continuation services are unavailable, parent authority is
|
||||
* rejected, or the message was not admitted.
|
||||
*/
|
||||
async followup( parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise<MessageId>
|
||||
|
||||
/**
|
||||
* Deliver selected content from one live continuable child to its durable
|
||||
* direct parent. The child is the authority credential; callers cannot name a
|
||||
* recipient. Reporting does not conclude the child's turn or Activation.
|
||||
* @param child - exact live reporting child.
|
||||
* @param content - selected model-facing content.
|
||||
* @param options - parent scheduling and pre-acceptance cancellation.
|
||||
* @returns the stable identity of the parent-accepted message.
|
||||
* @throws when continuation services are unavailable, sender authorization
|
||||
* fails, or the direct parent is not live.
|
||||
*/
|
||||
async reportFrom( child: Agent, content: ContentBlock[], options: SubagentReportOptions, ): Promise<MessageId>
|
||||
|
||||
/**
|
||||
* Compose one deployment capability into every continuable child's
|
||||
* unpublished creation context on fresh creation and cold resume. Grants wait
|
||||
* for the next Activation; removing the contribution revokes every resident
|
||||
* installation immediately.
|
||||
* @param contribution - synchronous child-scope installer.
|
||||
* @returns the exact Cordis effect disposer.
|
||||
*/
|
||||
registerContinuableSetup(contribution: ContinuableSetupContribution): () => void
|
||||
|
||||
/**
|
||||
* Close continuable admission below exact live parent Agents, stop only their
|
||||
* visible descendant Activations synchronously, then await admitted scoped
|
||||
* materializations and release those forests child-first. The scoped cutoff
|
||||
* lasts until each exact parent leaves the registry; unrelated parent trees
|
||||
* remain live.
|
||||
* @param parents - exact host-owned parent Agents entering teardown.
|
||||
* @returns once every retained descendant Activation released its `AgentHandle`.
|
||||
* @throws an aggregate error after all branches settle when any failed.
|
||||
*/
|
||||
async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>
|
||||
|
||||
/**
|
||||
* Enumerate the parent's direct session-backed subagents from the
|
||||
* live-preferred session corpus without loading or resuming an Agent. Session
|
||||
* query supplies lineage, candidate order, event reads, and live state; this
|
||||
* service interprets descriptor mode, activity, and per-child diagnostics
|
||||
* without consulting Agent registrations, Activations, or providers.
|
||||
*
|
||||
* The trace and exact descriptor read receive `signal`; the full event-list
|
||||
* read has no signal parameter, so the scan rechecks cancellation around
|
||||
* every await and between candidates. Query rejections that settle after an
|
||||
* abort become a stable `SubagentError` with code `CANCELLED`.
|
||||
* @param parentSessionId - parent session whose direct children are listed.
|
||||
* @param signal - caller-owned cancellation forwarded where supported and
|
||||
* observed around every query await.
|
||||
* @returns children and per-child diagnostics in stable trace order.
|
||||
* @throws {@link SubagentError} when session query is unavailable or the
|
||||
* caller cancels the scan.
|
||||
*/
|
||||
listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentListEntry[]>
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1792,20 +2100,21 @@ getProvider(name: string): SubagentProvider | undefined
|
||||
list(): string[]
|
||||
|
||||
/**
|
||||
* Establish a ready child on the named provider. Capability and semantic
|
||||
* Establish a published 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.
|
||||
* emits no run lifecycle events. Post-publication turn and infrastructure
|
||||
* failures settle through the returned run.
|
||||
* @param name - the provider to use.
|
||||
* @param request - child prompt, parent, signal, and optional capabilities.
|
||||
* @returns the ready holder-owned run.
|
||||
* @param request - child label, prompt, parent, signal, and optional capabilities.
|
||||
* @returns the published holder-owned run.
|
||||
*/
|
||||
async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
|
||||
```
|
||||
|
||||
Types: [SubagentProvider](../core-data-structures/subagent.md) · [SubagentRun](../core-data-structures/subagent.md) · [SubagentStartRequest](../core-data-structures/subagent.md)
|
||||
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [ContinuableSetupContribution](../core-data-structures/subagent.md) · [ContinuableStart](../core-data-structures/subagent.md) · [ContinuableStartSpec](../core-data-structures/subagent.md) · [MessageId](../core-data-structures/core.md) · [SessionId](../core-data-structures/core.md) · [SubagentFollowupOptions](../core-data-structures/subagent.md) · [SubagentListEntry](../core-data-structures/subagent.md) · [SubagentProvider](../core-data-structures/subagent.md) · [SubagentReportOptions](../core-data-structures/subagent.md) · [SubagentRun](../core-data-structures/subagent.md) · [SubagentStartRequest](../core-data-structures/subagent.md)
|
||||
|
||||
Source: [`packages/subagent/subagent/src/index.ts:181`](../../packages/subagent/subagent/src/index.ts)
|
||||
Source: [`packages/subagent/subagent/src/index.ts:165`](../../packages/subagent/subagent/src/index.ts)
|
||||
|
||||
## `ctx.subprocess` — `SubprocessService` (abstract seam)
|
||||
|
||||
@@ -1830,7 +2139,7 @@ abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle
|
||||
|
||||
Types: [SubprocessHandle](../core-data-structures/subprocess.md) · [SubprocessSpawnSpec](../core-data-structures/subprocess.md)
|
||||
|
||||
Source: [`packages/subprocess/subprocess/src/index.ts:88`](../../packages/subprocess/subprocess/src/index.ts)
|
||||
Source: [`packages/subprocess/subprocess/src/index.ts:91`](../../packages/subprocess/subprocess/src/index.ts)
|
||||
|
||||
## `ctx.systemPrompt` — `SystemPrompt`
|
||||
|
||||
@@ -1847,6 +2156,14 @@ Registry service for the prompt inputs assembled before each model step.
|
||||
*/
|
||||
section(section: PromptSection): () => void
|
||||
|
||||
/**
|
||||
* Register ordered dynamic context in the calling context's scope. Scoped
|
||||
* entries shadow global entries with the same name.
|
||||
* @param context - the context contribution to register.
|
||||
* @returns the exact Cordis effect disposer.
|
||||
*/
|
||||
context(context: PromptContext): () => void
|
||||
|
||||
/**
|
||||
* Register a tool-schema provider in the calling context's scope. Global and
|
||||
* matching scoped providers both contribute; returning the reserved
|
||||
@@ -1876,9 +2193,9 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine
|
||||
async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
|
||||
```
|
||||
|
||||
Types: [AssembleContext](../core-data-structures/system-prompt.md) · [PromptSection](../core-data-structures/system-prompt.md) · [ToolProviderResult](../core-data-structures/system-prompt.md)
|
||||
Types: [AssembleContext](../core-data-structures/system-prompt.md) · [PromptContext](../core-data-structures/system-prompt.md) · [PromptSection](../core-data-structures/system-prompt.md) · [ToolProviderResult](../core-data-structures/system-prompt.md)
|
||||
|
||||
Source: [`packages/core/system-prompt/src/index.ts:246`](../../packages/core/system-prompt/src/index.ts)
|
||||
Source: [`packages/core/system-prompt/src/index.ts:314`](../../packages/core/system-prompt/src/index.ts)
|
||||
|
||||
## `ctx.tasks` — `TaskService` (abstract seam)
|
||||
|
||||
@@ -2022,7 +2339,8 @@ Replay owner for one service-wide estimator and isolated per-session folds.
|
||||
measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement
|
||||
|
||||
/**
|
||||
* Heuristically price one model-visible message.
|
||||
* Heuristically price one model-visible message (instance face of the pure
|
||||
* `estimateMessage` export from `estimate.ts`).
|
||||
* @param message - message to price without mutation.
|
||||
* @returns content and role-framing tokens under the fixed service heuristic.
|
||||
*/
|
||||
@@ -2031,7 +2349,7 @@ estimateMessage(message: Message): number
|
||||
|
||||
Types: [EpochHeader](../core-data-structures/session.md) · [Message](../core-data-structures/core.md) · [Session](../core-data-structures/session.md) · [TokenMeasurement](../core-data-structures/token-meter.md)
|
||||
|
||||
Source: [`packages/llm/token-meter/src/index.ts:82`](../../packages/llm/token-meter/src/index.ts)
|
||||
Source: [`packages/llm/token-meter/src/index.ts:74`](../../packages/llm/token-meter/src/index.ts)
|
||||
|
||||
## `ctx.toolResultPrune` — `ToolResultPruneService`
|
||||
|
||||
@@ -2057,7 +2375,10 @@ 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.
|
||||
* points at the shadowed node for durable provenance and replay, and is
|
||||
* immediately preceded by a `compact/prune` shadow-price event pricing the
|
||||
* shadowed node through the injected token meter, so pure consumers can
|
||||
* subtract it without per-node state.
|
||||
* @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
|
||||
@@ -2068,7 +2389,7 @@ 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:40`](../../packages/compact/compact-tool-result-prune/src/index.ts)
|
||||
Source: [`packages/compact/compact-tool-result-prune/src/index.ts:44`](../../packages/compact/compact-tool-result-prune/src/index.ts)
|
||||
|
||||
## `ctx.tools` — `ToolRegistry`
|
||||
|
||||
@@ -2151,30 +2472,68 @@ async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
|
||||
|
||||
Types: [ScopeKey](../core-data-structures/scope.md) · [ToolDefinition](../core-data-structures/tools.md) · [ToolExecutionInput](../core-data-structures/tools.md) · [ToolExecutionMode](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) · [ToolGuard](../core-data-structures/tools.md) · [ToolRestriction](../core-data-structures/tools.md) · [ToolSchema](../core-data-structures/tools.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:700`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:714`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
## `ctx.tui` — `TuiExtensionService` (abstract seam)
|
||||
## `ctx.typert` — `TypertRegistry`
|
||||
|
||||
Optional terminal-local interaction service provided by one mounted TUI.
|
||||
|
||||
The concrete provider retains pi-tui, focus, and terminal lifecycle state. Plugins receive only effect-owned overlay sessions.
|
||||
Registry of generated schemas and package reflection.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Queue an interactive overlay owned by the calling plugin fiber.
|
||||
*
|
||||
* The TUI displays one overlay at a time in FIFO order. Disposing the caller
|
||||
* removes a queued overlay or closes an active one before plugin teardown
|
||||
* settles. This live presentation is neither logged nor replayed.
|
||||
*
|
||||
* @param request - component factory, layout constraints, and cancellation.
|
||||
* @returns the effect-owned overlay session.
|
||||
* @throws when the TUI has begun shutting down.
|
||||
* Register one generated contribution atomically for the calling fiber.
|
||||
* Duplicate package-face identities or schema keys reject the whole batch.
|
||||
* @param contribution - generated schemas and package metadata.
|
||||
* @returns the exact effect disposer that removes this contribution.
|
||||
*/
|
||||
abstract openOverlay(request: TuiOverlayRequest): TuiOverlaySession
|
||||
register(contribution: TypertContribution): () => void
|
||||
|
||||
/**
|
||||
* Look up one schema by `<package>#<name>`.
|
||||
* @param key - global schema key.
|
||||
* @returns the live schema record, or `undefined` when absent.
|
||||
*/
|
||||
get(key: string): TypertSchemaRecord | undefined
|
||||
|
||||
/**
|
||||
* Resolve one required schema.
|
||||
* @param key - global schema key.
|
||||
* @returns the live schema record.
|
||||
* @throws when the key is malformed, the package face is absent, or the schema is not contributed.
|
||||
*/
|
||||
resolve(key: string): TypertSchemaRecord
|
||||
|
||||
/**
|
||||
* Enumerate live schemas in registration order.
|
||||
* @param filter - optional package and face restriction.
|
||||
* @returns matching schema records.
|
||||
*/
|
||||
list(filter: TypertSchemaFilter = {}): TypertSchemaRecord[]
|
||||
|
||||
/**
|
||||
* Look up generated reflection for one package face.
|
||||
* @param packageName - exact npm package name.
|
||||
* @param face - face to query; defaults to the host runtime.
|
||||
* @returns the live package record, or `undefined` when absent.
|
||||
*/
|
||||
getPackage(packageName: string, face: TypertFace = 'host'): TypertPackageRecord | undefined
|
||||
|
||||
/**
|
||||
* Enumerate generated package reflection in registration order.
|
||||
* @param filter - optional package and face restriction.
|
||||
* @returns matching package records.
|
||||
*/
|
||||
listPackages(filter: TypertPackageFilter = {}): TypertPackageRecord[]
|
||||
|
||||
/**
|
||||
* Project a live Zod schema to JSON Schema without caching the result.
|
||||
* @param key - global schema key.
|
||||
* @param params - Zod projection parameters.
|
||||
* @returns a fresh JSON Schema document.
|
||||
*/
|
||||
toJSONSchema(key: string, params?: z.core.ToJSONSchemaParams): z.core.JSONSchema.BaseSchema
|
||||
```
|
||||
|
||||
Source: [`packages/ui/tui/src/index.ts:247`](../../packages/ui/tui/src/index.ts)
|
||||
Source: [`packages/typert/registry/src/index.ts:67`](../../packages/typert/registry/src/index.ts)
|
||||
|
||||
## `ctx.userInteraction` — `UserInteractionService`
|
||||
|
||||
@@ -2200,7 +2559,7 @@ async ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
|
||||
|
||||
Types: [AskUserQuestionAnswer](../core-data-structures/user-interaction.md) · [AskUserQuestionRequest](../core-data-structures/user-interaction.md) · [UserInteractionProvider](../core-data-structures/user-interaction.md)
|
||||
|
||||
Source: [`packages/ui/user-interaction/src/index.ts:50`](../../packages/ui/user-interaction/src/index.ts)
|
||||
Source: [`packages/ui/user-interaction/src/index.ts:51`](../../packages/ui/user-interaction/src/index.ts)
|
||||
|
||||
## `ctx.web` — `WebService`
|
||||
|
||||
@@ -2289,7 +2648,7 @@ Durable workspace registry. Startup waits for `sessionPersistence`, builds one c
|
||||
* original error and a non-directory rejects. Repeated calls for the same
|
||||
* canonical path return the existing entity without changing its title.
|
||||
* A newly created workspace is prepended to the durable registry order.
|
||||
* A different canonical path cannot create a duplicate display title.
|
||||
* Different canonical paths may share a display title.
|
||||
* @param path - Existing directory to own, in any path spelling.
|
||||
* @param title - Display title used only when a new record is created.
|
||||
* @returns the existing or newly durable workspace.
|
||||
@@ -2321,6 +2680,15 @@ list(): Workspace[]
|
||||
*/
|
||||
delete(id: WorkspaceId): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Archive one session durably. The session must exist (live or in session
|
||||
* persistence); its workspace accounting — or lack of one — is irrelevant.
|
||||
* An already archived id resolves without writing.
|
||||
* @param sessionId - The session to archive.
|
||||
* @returns resolution after durability.
|
||||
*/
|
||||
archiveSession(sessionId: SessionId): Promise<void>
|
||||
|
||||
/**
|
||||
* Resolve by canonical directory path without creating or mutating a
|
||||
* workspace. A missing path rejects during `realpath`; an existing unowned
|
||||
@@ -2331,7 +2699,9 @@ delete(id: WorkspaceId): Promise<boolean>
|
||||
async resolveByPath(path: string): Promise<Workspace | undefined>
|
||||
```
|
||||
|
||||
Source: [`packages/workspace/workspace/src/index.ts:78`](../../packages/workspace/workspace/src/index.ts)
|
||||
Types: [SessionId](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/workspace/workspace/src/index.ts:81`](../../packages/workspace/workspace/src/index.ts)
|
||||
|
||||
## Inherited `ctx` members (cordis core + loader/hmr/timer)
|
||||
|
||||
|
||||
65905
docs/cordis-paper.pdf
Normal file
65905
docs/cordis-paper.pdf
Normal file
File diff suppressed because one or more lines are too long
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/cordis-primer.md
|
||||
cordis-primer.md: ee65e6e702ecaeb506ce7334032c38e09c936cda
|
||||
cordis-primer.zh.md: ee4f6864ba7864fc95b5eb8e31acbcaea6e99825
|
||||
cordis-primer.zh.md: 051dd7c956a4db107a4ef7d1414435f9c2b2603d
|
||||
|
||||
@@ -10,7 +10,7 @@ Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。
|
||||
- **上下文是服务的容器。** 一个服务占据一个稳定的 `ctx.<key>`(如 `ctx.tools`、`ctx.llm`、`ctx.sessions`);其他插件通过 key 查找服务,而非导入具体实现。
|
||||
- **通过 `inject` 声明服务依赖。** 插件声明所需的服务后,会等待这些服务就绪才启动;加载顺序通过服务依赖表达,而非手动编排启动序列。
|
||||
- **类型化事件用于通信。** 服务通过 TypeScript 声明合并注册事件名,然后以 `emit`、`waterfall`(瀑布式事件)、`parallel` 或 `serial` 方式分发,分别对应监听者观察、包装、并行扇出或按序执行。
|
||||
- **注册是可逆的副作用。** 提示词片段、工具 schema、适配器、提供方和监听器通过 `ctx.effect()` 或 `ctx.on()` 安装,reload 和 teardown 时可预期地回卷。
|
||||
- **注册是可逆的副作用。** 提示词片段、工具 schema、适配器、提供方和监听器通过 `ctx.effect()` 或 `ctx.on()` 安装,reload 和 teardown 时会按预期撤销。
|
||||
|
||||
## 分发模式
|
||||
|
||||
@@ -45,4 +45,4 @@ Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。
|
||||
|
||||
将行为封装为插件:工具流水线事件属于 `ctx.tools`,模型流式输出属于 `ctx.llm`,实时 agent(智能体)协调属于 `ctx.agents`。拦截和策略优先使用事件;直接能力调用优先使用服务方法。
|
||||
|
||||
每个注册都应有对应的 disposer(dispose(资源释放)函数):要么从 `ctx.effect()` 返回一个,要么使用 Cordis 提供的辅助方法自动处理。如果 teardown 顺序有要求,请将相关工作放在同一个 effect 中,以确保资源释放按预期顺序回卷。
|
||||
每个注册都应有对应的 disposer(资源释放函数):要么从 `ctx.effect()` 返回一个,要么使用 Cordis 提供的辅助方法自动处理。如果 teardown 顺序有要求,请将相关工作放在同一个 effect 中,以确保资源按预期顺序释放。
|
||||
|
||||
@@ -1,6 +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
|
||||
01-first-plugin.md: b730b7ad7dc9ebd2e5dc8af4f7a83bd58ac7d4d8
|
||||
01-first-plugin.zh.md: 1e6901c048f5268ddead0faca85eaf5eef9c7533
|
||||
# pnpm run verify-translation-pairing --write docs/cordis-tutorial/01-first-plugin.md
|
||||
01-first-plugin.md: c9f4889398222793005fce6832d0917a20d0be30
|
||||
01-first-plugin.zh.md: b9d6994fad8e26cdfc52db0fbcef5631d5d53b03
|
||||
|
||||
@@ -48,7 +48,7 @@ The process exits on its own once nothing is left running. What happened:
|
||||
2. The Loader read `cordis.yml`, resolved `./hello.ts`, and mounted it as a child plugin.
|
||||
3. Cordis called your `apply(ctx)`.
|
||||
|
||||
There is no framework bootstrap code in your file: a plugin describes what it contributes, and `cordis.yml` composes the application. The [TUI agent](../../examples/tui-agent/cordis.yml), for example, is a longer plugin composition.
|
||||
There is no framework bootstrap code in your file: a plugin describes what it contributes, and `cordis.yml` composes the application. The [`dsh` base](../../apps/cli/config/base.cordis.yml), for example, is a longer plugin composition that deployment overlays patch.
|
||||
|
||||
## The two other plugin shapes
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ hello from my first plugin
|
||||
2. Loader 读取 `cordis.yml`,解析 `./hello.ts`,然后将其作为子插件挂载。
|
||||
3. Cordis 调用你的 `apply(ctx)`。
|
||||
|
||||
你的文件中没有框架启动代码:插件描述自己的贡献,`cordis.yml` 则组合应用。例如,[TUI agent(智能体)](../../examples/tui-agent/cordis.yml) 就是一个更长的插件组合。
|
||||
你的文件中没有框架启动代码:插件描述自己的贡献,`cordis.yml` 则组合应用。例如,[`dsh` base](../../apps/cli/config/base.cordis.yml) 就是一份更长的插件组合,由部署 overlay 对它进行修补。
|
||||
|
||||
## 其他两种插件形态
|
||||
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/cordis-tutorial/02-lifecycle-and-effects.md
|
||||
02-lifecycle-and-effects.md: f1b39e06e9d25c51ab2d76503025e2b6ffe90c73
|
||||
02-lifecycle-and-effects.zh.md: a6021ed7475a0045d480810747244274eb5b4198
|
||||
02-lifecycle-and-effects.zh.md: 2e98e3af6d2f2b1b9cbb8ea38559bc1ffbf7e43b
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[English](02-lifecycle-and-effects.md) | 中文
|
||||
|
||||
Cordis 插件可能因配置编辑、热重载、显式资源释放或所需服务消失而卸载。通过 Cordis API 建立的注册属于 effect,会在所属插件卸载时撤销;在这些 API 之外管理的资源必须包装在 `ctx.effect()` 中。
|
||||
Cordis 插件可能因修改配置、热重载、显式资源释放或所需服务消失而卸载。通过 Cordis API 建立的注册属于 effect,会在所属插件卸载时撤销;在这些 API 之外管理的资源必须包装在 `ctx.effect()` 中。
|
||||
|
||||
## Effect
|
||||
|
||||
对于 Cordis 尚未管理的资源,例如定时器、连接或 watcher,应将其包装在 `ctx.effect()` 中并返回 disposer(dispose(资源释放)函数):
|
||||
对于 Cordis 尚未管理的资源,例如定时器、连接或 watcher,应将其包装在 `ctx.effect()` 中并返回 disposer(资源释放函数):
|
||||
|
||||
创建 `lifecycle.ts`,将它放在 `tmp/cordis-tutorial` 中:
|
||||
|
||||
@@ -67,7 +67,7 @@ disposed
|
||||
|
||||
## Fiber 状态机
|
||||
|
||||
每个已加载插件实例都拥有一个 fiber,并依次经过以下状态:
|
||||
每个已加载插件实例都拥有一个 fiber,并在以下状态之间转换:
|
||||
|
||||
```
|
||||
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
|
||||
@@ -86,8 +86,8 @@ PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
|
||||
你很少需要亲自编写 `ctx.effect()`,因为内置注册 API 本身已经是 effect:
|
||||
|
||||
- `ctx.on(event, listener)`:监听器会在卸载时移除([第 4 章](04-events.md))。
|
||||
- `ctx.plugin(child)`:子插件会随父插件一同 dispose。
|
||||
- 服务注册属于 effect。`ctx.tools.register(...)` 等 harness 注册表也会把返回的 disposer 附着到调用插件上,因此会自动回卷([第 7 章](07-into-the-harness.md))。
|
||||
- `ctx.plugin(child)`:子插件会随父插件一同 dispose(资源释放)。
|
||||
- 服务注册属于 effect。`ctx.tools.register(...)` 等 harness 注册表也会把返回的 disposer 附着到调用插件上,因此会自动撤销([第 7 章](07-into-the-harness.md))。
|
||||
|
||||
对于 Cordis 不管理的资源,应在 `ctx.effect()` 内获取它,并返回用于释放资源的 disposer。此后 Cordis 会在卸载期间调用该释放逻辑,热重载时也不例外。
|
||||
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/cordis-tutorial/03-services.md
|
||||
03-services.md: 5848132c6ad18338fa893954d45fc20005db6199
|
||||
03-services.zh.md: 3c77d0451df9062f1a344e7474e6be141b709197
|
||||
03-services.zh.md: 0f599f082573364e6ad38278e1914d8faf67faa1
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](03-services.md) | 中文
|
||||
|
||||
**服务**是一个插件提供、其他插件通过 `ctx` 消费的命名功能。在 harness 中,`ctx.tools`、`ctx.llm` 和 `ctx.agents` 都是服务。消费方只命名 `'tools'` 之类的功能,而不导入其提供方,因此配置可以选择提供方,无需修改消费方。
|
||||
**服务**是一个插件提供、其他插件通过 `ctx` 消费的具名能力。在 harness 中,`ctx.tools`、`ctx.llm` 和 `ctx.agents` 都是服务。消费方只指定 `'tools'` 之类的能力,而不导入其提供方,因此配置可以选择提供方,无需修改消费方。
|
||||
|
||||
## 提供服务
|
||||
|
||||
@@ -73,9 +73,9 @@ Hello, world!
|
||||
|
||||
## 加载后仍会跟踪依赖关系
|
||||
|
||||
`inject` 并非一次性的启动检查。如果应用运行期间所需服务消失,例如提供方被卸载或热替换,每个依赖插件也会随之卸载,并在服务恢复后再次加载。结合 effect([第 2 章](02-lifecycle-and-effects.md)),这能防止运行中的消费方保留对不可用服务的引用:依赖消失时,它自己的注册也会回卷。
|
||||
`inject` 并非一次性的启动检查。如果应用运行期间所需服务消失,例如提供方被卸载或热替换,每个依赖插件也会随之卸载,并在服务恢复后再次加载。结合 effect([第 2 章](02-lifecycle-and-effects.md)),这能防止运行中的消费方保留对不可用服务的引用:依赖消失时,它自己的注册也会撤销。
|
||||
|
||||
这也是配置中可以替换服务的原因:卸载 `dsh-bash-local` 配置项,挂载另一个 `bash` 提供方,所有注入 `'bash'` 的插件都会干净地重启并使用新实现。
|
||||
这也是配置中可以替换服务的原因:卸载 Cordis 配置项 `dsh-bash-local`,挂载另一个 `bash` 提供方,所有注入 `'bash'` 的插件都会重新启动并使用新实现。
|
||||
|
||||
## 可选依赖
|
||||
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/cordis-tutorial/04-events.md
|
||||
04-events.md: 18f39dc1b693e5fb7e1793ec4b7dcac9cf24db95
|
||||
04-events.zh.md: f55a61ff2f43ea42968893d07eb92ea0613b921a
|
||||
04-events.zh.md: 3fdafb50303f49dca179bcaea32db211a66241f6
|
||||
|
||||
@@ -91,7 +91,7 @@ export function apply(ctx: Context) {
|
||||
|
||||
每个 harness 事件都会在生成的[事件目录](../cordis-catalog/events.md)中记录其模式。
|
||||
|
||||
## Waterfall:转换或短路
|
||||
## waterfall:转换或短路
|
||||
|
||||
waterfall 是实现拦截的模式。每个监听器都会收到参数和一个 `next()` continuation;它可以转换 `next()` 的返回值,也可以不调用 `next()` 就直接返回,从而短路链条的其余部分。Cordis 文档把后一种行为称为否决。创建 `waterfall-demo.ts`:
|
||||
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/cordis-tutorial/05-config.md
|
||||
05-config.md: fc19add239636fa9e7071d9c77e48595caec1f08
|
||||
05-config.zh.md: 52a75e40672c9a08d285677dd14dcd404b925e5a
|
||||
05-config.zh.md: 0c8170f518f0c87ab5c754606436496a4ff9d51e
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](05-config.md) | 中文
|
||||
|
||||
每个 `cordis.yml` 配置项都可以携带 `config` 块,插件则声明一个 schema,在运行 `apply` 前验证该块。错误配置会导致加载失败,并给出准确的错误:插件绝不会在配置不完整时启动。
|
||||
`cordis.yml` 中的每个 Cordis 配置项都可以携带 `config` 块,插件则声明一个 schema,在运行 `apply` 前验证该块。错误配置会导致加载失败,并给出准确的错误:插件绝不会在配置不完整时启动。
|
||||
|
||||
## 可配置插件
|
||||
|
||||
@@ -65,7 +65,7 @@ ValidationError: invalid config:
|
||||
- $.targets expected array but got not-an-array (at targets)
|
||||
```
|
||||
|
||||
插件的 fiber 进入 FAILED 状态,本教程的启动器打印错误后以状态码 1 退出。如果某个插件的 schema 有效配置命名了不可用的资源或提供方,该插件也应当在能解析该引用时立即拒绝。
|
||||
插件的 fiber 进入 FAILED 状态,本教程的启动器打印错误后以状态码 1 退出。如果某个插件的配置通过了 schema 验证,但其中指定的资源或提供方不可用,该插件也应当在能解析该引用时立即拒绝。
|
||||
|
||||
## 计算得到的配置值
|
||||
|
||||
@@ -77,8 +77,8 @@ ValidationError: invalid config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
```
|
||||
|
||||
`!!js` **仅在 `config` 内有效**。配置项元数据(`name`、`id`、`disabled`、`inject` 等)是静态的;`disabled: !!js ...` 会生成一个真值表达式对象,始终禁用该配置项。详见 [loader 配置](../cordis-primer.md#loader-configuration)。
|
||||
`!!js` **仅在 `config` 内有效**。Cordis 配置项的元数据(`name`、`id`、`disabled`、`inject` 等)是静态的;`disabled: !!js ...` 会生成一个真值表达式对象,始终禁用该 Cordis 配置项。详见 [loader 配置](../cordis-primer.md#loader-configuration)。
|
||||
|
||||
下一章:[组合与 HMR](06-composition-and-hmr.md):将 `cordis.yml` 视为应用。
|
||||
下一章:[组合与 HMR(热模块替换)](06-composition-and-hmr.md):将 `cordis.yml` 视为应用。
|
||||
|
||||
[](https://github.com/deepseek-harness/deepseek-harness)
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/cordis-tutorial/06-composition-and-hmr.md
|
||||
06-composition-and-hmr.md: 66d6a9d93fe39baa881940ba32388979e2678505
|
||||
06-composition-and-hmr.zh.md: ebe63fc26607ae6d9344c4795a7975496ed901b5
|
||||
06-composition-and-hmr.zh.md: 7c0a94b0abcc0f153f59391fd009f1e0b40500e5
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[English](06-composition-and-hmr.md) | 中文
|
||||
|
||||
到目前为止构建的每项功能都是插件,`cordis.yml` 则选择应用的插件树。本章会改变这种组合、热重载一个插件,并诊断始终无法加载的插件。
|
||||
到目前为止构建的每项能力都是插件,`cordis.yml` 则选择应用的插件树。本章会改变这种组合、热重载一个插件,并诊断始终无法加载的插件。
|
||||
|
||||
## 配置项不只有名称
|
||||
## Cordis 配置项不只有名称
|
||||
|
||||
配置项除了 `name` 和 `config`,还接受其他元数据:
|
||||
Cordis 配置项除了 `name` 和 `config`,还接受其他元数据:
|
||||
|
||||
```yaml
|
||||
- id: greeter # stable identity for this entry
|
||||
@@ -16,9 +16,9 @@
|
||||
disabled: true # keep the entry, skip mounting it
|
||||
```
|
||||
|
||||
`id` 为配置项提供稳定标识,使 loader 能区分修改现有配置项与先删除再添加。`disabled: true` 会卸载插件而不删除其配置项;改回原值后,插件以及所有因依赖其服务而处于 PENDING 的插件都会再次加载。
|
||||
`id` 为 Cordis 配置项提供稳定标识,使 loader 能区分修改现有 Cordis 配置项与先删除再添加。`disabled: true` 会卸载插件而不删除其 Cordis 配置项;改回原值后,插件以及所有因依赖其服务而处于 PENDING 的插件都会再次加载。
|
||||
|
||||
组可以嵌套一份配置项子列表,并将其作为一个单元加载和卸载;`isolate` 则为一个组提供某项服务名称的独立实例,因此两个组可以各自看到配置不同的 `bash`,互不影响。这些概念值得在用到之前先了解;[Cordis 入门](../cordis-primer.md)和[服务隔离示例](../user/develop/framework/service.md#service-isolation)介绍了详细内容。
|
||||
组可以嵌套一份 Cordis 配置项子列表,并将其作为一个单元加载和卸载;`isolate` 则为一个组提供某项服务名称的独立实例,因此两个组可以各自看到配置不同的 `bash`,互不影响。这些概念值得在用到之前先了解;[Cordis 入门](../cordis-primer.md)和[服务隔离示例](../user/develop/framework/service.md#service-isolation)介绍了详细内容。
|
||||
|
||||
## 热模块替换
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
name: './hello.ts'
|
||||
```
|
||||
|
||||
列表中增加了两个支持插件:HMR 通过 Cordis logger 服务记录日志,因此没有 console exporter 时看不到其消息;它还会 `inject` `timer` 服务来实现去抖,如果没有 `@cordisjs/plugin-timer`,它就会永远停在 PENDING,而且不发出任何提示。下一节就讨论这种静默状态。
|
||||
列表中增加了两个辅助插件:HMR 通过 Cordis logger 服务记录日志,因此没有控制台导出器时看不到其消息;它还会 `inject` `timer` 服务来实现去抖,如果没有 `@cordisjs/plugin-timer`,它就会永远停在 PENDING,而且不发出任何提示。下一节就讨论这种静默状态。
|
||||
|
||||
HMR 通过 Loader 的原生辅助工具读取 Node 的 loader 内部结构。请在 tsx 下运行 Cordis:
|
||||
|
||||
@@ -56,7 +56,7 @@ hello from my first plugin
|
||||
hello from my EDITED plugin
|
||||
```
|
||||
|
||||
旧实例先卸载(其所有 effect 都会回卷),新代码随后加载,`apply` 再次运行。按 Ctrl-C 停止进程。编辑 `cordis.yml` 本身也会触发更新:loader 按 `id` 比较配置项,只挂载、卸载或重新配置发生变化的部分。这就是上述配置项显式携带 `id` 的原因:不带该字段的配置项在每次读取时都会获得一个新生成的 id,所以只要配置文件发生任何编辑,即使自身文本未变,它也会被视为先删除再添加并重新挂载。
|
||||
旧实例先卸载(其所有 effect 都会回卷),新代码随后加载,`apply` 再次运行。按 Ctrl-C 停止进程。编辑 `cordis.yml` 本身也会触发更新:loader 按 `id` 比较 Cordis 配置项,只挂载、卸载或重新配置发生变化的部分。这就是上述 Cordis 配置项显式携带 `id` 的原因:不带该字段的 Cordis 配置项在每次读取时都会获得一个新生成的 id,所以只要配置文件发生任何编辑,即使自身文本未变,它也会被视为先删除再添加并重新挂载。
|
||||
|
||||
## 诊断始终无法加载的插件
|
||||
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/cordis-tutorial/07-into-the-harness.md
|
||||
07-into-the-harness.md: 6ec42c50fe5059955734fe7bc46117538dafaffc
|
||||
07-into-the-harness.zh.md: 32b21b008837e2972a53db9d893788dc6a7de9a9
|
||||
07-into-the-harness.zh.md: 903adb903aa4c4355b92eb34e89f218a0295767c
|
||||
|
||||
@@ -46,7 +46,7 @@ export function apply(ctx: Context) {
|
||||
}
|
||||
```
|
||||
|
||||
这里的每个模式都来自前几章:`inject: ['tools']`([第 3 章](03-services.md))会让插件等待工具注册表就绪;`ctx.tools.register(...)` 会把注册 disposer 附着到插件([第 2 章](02-lifecycle-and-effects.md)),因此卸载时会注销工具。`defineTool` 将 `parameters` 规约转换为向模型展示的 JSON Schema,推导 `args` 的类型,并在 `execute` 运行前校验模型提供的参数。工具返回由 `output.schema` 声明的规范值;`output.render` 则另行生成原生且持久的结果内容。
|
||||
这里的每个模式都来自前几章:`inject: ['tools']`([第 3 章](03-services.md))会让插件等待工具注册表就绪;`ctx.tools.register(...)` 会把注册 disposer 附着到插件([第 2 章](02-lifecycle-and-effects.md)),因此卸载时会注销工具。`defineTool` 将 `parameters` 规约转换为向模型展示的 JSON Schema,推导 `args` 的类型,并在 `execute` 运行前校验模型提供的参数。工具返回由 `output.schema` 声明的规范值;`output.render` 则作为 Native renderer(原生渲染器),另行生成可持久化的结果内容。
|
||||
|
||||
## 观察插件
|
||||
|
||||
@@ -91,7 +91,7 @@ node --import tsx ../../vendor/cordis/bin.js
|
||||
tool replied: [{"type":"text","text":"Hello, Cordis!"}]
|
||||
```
|
||||
|
||||
logger 会先触发:`tools/result` 在结果物化过程中发出,早于 `execute` 的 promise 向调用方返回结果。两个插件都不知道另一个插件存在,它们由注册表服务和事件连接。
|
||||
logger 会先触发:`tools/result` 在结果物化过程中发出,发生在 `execute` 向调用方返回的 promise 兑现之前。两个插件都不知道另一个插件存在,它们由注册表服务和事件连接。
|
||||
|
||||
## 从这里走向完整 agent(智能体)
|
||||
|
||||
@@ -100,7 +100,7 @@ logger 会先触发:`tools/result` 在结果物化过程中发出,早于 `ex
|
||||
后续可以阅读:
|
||||
|
||||
- [构建工具](../user/develop/basic/tool.md):深入了解 `defineTool`,包括呈现和更丰富的 schema。
|
||||
- [三层功能设计](../user/develop/practice/index.md):harness 如何组织可替换功能。
|
||||
- [三层能力设计](../user/develop/practice/index.md):harness 如何组织可替换能力。
|
||||
- 生成的[服务](../cordis-catalog/services.md)与[事件](../cordis-catalog/events.md)目录:可以注入和监听的所有内容。
|
||||
- [架构](../architecture.md):这些插件所处的系统地图。
|
||||
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/cordis-tutorial/index.md
|
||||
index.md: af622ad4e35829c6283c40f1b0019d7959dac973
|
||||
index.zh.md: 35bad552ecce9c0496b0ed88b041a8109c81945b
|
||||
index.zh.md: 0b7684a9532a1efdcc3ea2d067da23852d146e2f
|
||||
|
||||
@@ -6,7 +6,7 @@ Cordis 是 DeepSeek Harness SDK 底层的插件框架:它是一个小型运行
|
||||
|
||||
本教程面向 agent 开发者。你不需要深入掌握 TypeScript;下文的 [TypeScript 说明](#typescript-notes)会解释可能陌生的语法,并且每一章都会给出确切命令和预期输出。
|
||||
|
||||
如果你想阅读精简的概念参考,而不是逐步实践,请参阅 [Cordis 入门](../cordis-primer.md)。详尽的 API 参考见生成的[事件](../cordis-catalog/events.md)与[服务](../cordis-catalog/services.md)目录,以及 [Cordis 核心 API](../cordis-catalog/core/context.md)页面。
|
||||
如果你想阅读精简的概念参考,而不是逐步实践,请参阅 [Cordis 入门](../cordis-primer.md)。详尽的 API 参考见生成的[事件](../cordis-catalog/events.md)与[服务](../cordis-catalog/services.md)目录,以及 [Cordis 核心 API](../cordis-catalog/core/context.md) 页面。
|
||||
|
||||
## 准备工作
|
||||
|
||||
@@ -39,7 +39,7 @@ node --import tsx ../../vendor/cordis/bin.js
|
||||
2. [生命周期与 effect](02-lifecycle-and-effects.md):由 Cordis 管理的注册会在所属插件卸载时撤销。
|
||||
3. [服务](03-services.md):在 `ctx` 上公开一项能力,并通过 `inject` 依赖它。
|
||||
4. [事件](04-events.md):类型化事件、广播分发和 waterfall(瀑布式事件)的短路行为。
|
||||
5. [配置](05-config.md):读取 `cordis.yml` 中经过校验的配置,并在输入错误时快速失败。
|
||||
5. [配置](05-config.md):读取 `cordis.yml` 中经过校验的配置,并在输入错误时明确报错。
|
||||
6. [组合与 HMR(热模块替换)](06-composition-and-hmr.md):把配置文件作为插件树,使用热重载,并诊断始终无法加载的插件。
|
||||
7. [进入 harness](07-into-the-harness.md):基于真实的 harness 服务注册一个可由模型调用的工具。
|
||||
|
||||
|
||||
@@ -1,6 +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
|
||||
approval.md: 0de13eb1504b5ecc4ac0eafefefa2c87562c89b4
|
||||
approval.zh.md: b7ba10449fccfdb0dd5b52374e14038e3ec76ff8
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/approval.md
|
||||
approval.md: f1889b25e2bbbcb157b0bced070b1f157a867504
|
||||
approval.zh.md: 48222991312f9ae97c9249f1232b261d2393d282
|
||||
|
||||
@@ -42,13 +42,12 @@ type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
|
||||
* (exactly today's behavior).
|
||||
* - `'never'` — never prompt anyone: every ask resolves `'rejected'`
|
||||
* deterministically. The strict headless stance (CI, unattended runs) and
|
||||
* the only policy value stated in the system prompt — unlike `'ask'`, its
|
||||
* outcome is knowable without asking, so stating it cannot overclaim.
|
||||
* the policy whose outcome is knowable without asking.
|
||||
*/
|
||||
type ApprovalPolicy = 'ask' | 'never'
|
||||
```
|
||||
|
||||
The prompt section states the deterministic `never` behavior and records either policy with a source-owned marker. The pre-step narrator reads that marker from the logged request header after restart; it does not infer state from deployment persona prose.
|
||||
Both policies contribute their complete current meaning to the cache-safe runtime-context snapshot. The sourced `user/message` is the durable model-visible input; changing approval state appends a new full snapshot after retained history without rewriting the request header's system prompt.
|
||||
|
||||
## Approval request
|
||||
|
||||
@@ -87,4 +86,4 @@ interface ApprovalRequest {
|
||||
|
||||
`ctx.approval.request(req)` requires the requesting session to be inside an open turn. It appends `approval/asked`, obtains one outcome, appends the matching `approval/decided`, and resolves with that outcome. The `never` policy is enforced inside the service before waterfall dispatch, so even an answerer registered later with `prepend` cannot bypass it. Answerers return an outcome when they own the request or call `next()` to delegate; the first answer occupies the single decision slot.
|
||||
|
||||
The audit events are log-only and do not enter the model transcript. Model-visible behavior is the caller's derived tool result, while the request header records the prompt policy that the model actually saw. Service disposal removes its prompt section and pre-step narrator together; answerer listeners are independently effect-bound to their owning plugins.
|
||||
The audit events are log-only and do not enter the model transcript. Model-visible behavior is the caller's derived tool result plus the current runtime-context snapshot. Service disposal removes its context contribution; answerer listeners are independently effect-bound to their owning plugins.
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](approval.md) | 中文
|
||||
|
||||
[dsh-user-approval](../../packages/ui/user-approval) 的用户审批 seam 回答一个问题:这个具体操作是否可以继续?它拥有共享的请求/结果词汇、`ctx.approval` 分发服务、`approval/request` 应答者 waterfall(瀑布式事件)、仅记录日志的审计事件对,以及按会话的 `ask`/`never` 策略。UI 通道可以提供人类应答者;[ACP(Agent Client Protocol)自动化桥接层](../../packages/acp/acp)为其拥有的 agent 提供一次性机器决策。调用方如 [dsh-tools](../../packages/core/tools) 和 [dsh-tool-bash](../../packages/bash/tool-bash) 消费闭合的结果,除非结果为 `allowed-once`,否则一律拒绝。
|
||||
[dsh-user-approval](../../packages/ui/user-approval) 的用户审批 seam 回答一个问题:这个具体操作是否可以继续?它拥有共享的请求/结果词汇、`ctx.approval` 分发服务、`approval/request` 应答者 waterfall(瀑布式事件)、仅记录日志的审计事件对,以及按会话的 `ask`/`never` 策略。UI 通道可以提供人类应答者;[ACP(Agent Client Protocol)自动化桥接层](../../packages/acp/acp)为其拥有的 agent(智能体)提供一次性机器决策。调用方如 [dsh-tools](../../packages/core/tools) 和 [dsh-tool-bash](../../packages/bash/tool-bash) 消费闭合的结果,除非结果为 `allowed-once`,否则一律拒绝。
|
||||
|
||||
源码:[`packages/ui/user-approval/src/index.ts`](../../packages/ui/user-approval/src/index.ts)
|
||||
|
||||
## 标识与结果
|
||||
|
||||
每个请求都会获得一个全新的 `ApprovalRequestId`。该品牌类型将 `approval/asked` 与 `approval/decided` 审计事件配对,同时不会让审批 id 与工具调用 id 或 agent(智能体)/会话 id 互换。
|
||||
每个请求都会获得一个全新的 `ApprovalRequestId`。该品牌类型将 `approval/asked` 与 `approval/decided` 审计事件配对,同时不会让审批 id 与工具调用 id 或 agent/会话 id 互换。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -18,7 +18,7 @@
|
||||
type ApprovalRequestId = Branded<'ApprovalRequestId'>
|
||||
```
|
||||
|
||||
`ApprovalOutcome` 是闭合的,且默认拒绝。`allowed-once` 仅授权所询问的那一个操作;调用方对 `rejected`、`cancelled` 和 `unavailable` 均执行拒绝。缺失、无所有权、抛异常或不合规的应答者会产生 `unavailable`,而非放行。
|
||||
`ApprovalOutcome` 是闭合的,且失败时拒绝。`allowed-once` 仅授权所询问的那一个操作;调用方对 `rejected`、`cancelled` 和 `unavailable` 均执行拒绝。缺失、不负责该请求、抛异常或不合规的应答者会产生 `unavailable`,而非放行。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -42,13 +42,12 @@ type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
|
||||
* (exactly today's behavior).
|
||||
* - `'never'` — never prompt anyone: every ask resolves `'rejected'`
|
||||
* deterministically. The strict headless stance (CI, unattended runs) and
|
||||
* the only policy value stated in the system prompt — unlike `'ask'`, its
|
||||
* outcome is knowable without asking, so stating it cannot overclaim.
|
||||
* the policy whose outcome is knowable without asking.
|
||||
*/
|
||||
type ApprovalPolicy = 'ask' | 'never'
|
||||
```
|
||||
|
||||
提示词段落会声明 `never` 的确定性行为,并以服务自有的标记记录当前策略。重启后,步骤前叙述器从已记录的请求头中读取该标记,而非从部署 persona 行文中推断状态。
|
||||
两种策略都会将各自完整的当前含义贡献给缓存安全的运行时上下文快照。带来源的 `user/message` 是持久化且模型可见的输入;批准状态变化时,会在保留的历史后追加一份新的完整快照,而不改写请求头中的系统提示词。
|
||||
|
||||
## 审批请求
|
||||
|
||||
@@ -85,6 +84,6 @@ interface ApprovalRequest {
|
||||
|
||||
## 分发与审计
|
||||
|
||||
`ctx.approval.request(req)` 要求发起请求的会话处于一个打开的轮次内。它追加 `approval/asked`,获取一个结果,追加对应的 `approval/decided`,然后以该结果 resolve。`never` 策略在服务内部、waterfall 分发之前强制执行,因此即使后来以 `prepend` 注册的应答者也无法绕过它。应答者在拥有该请求时返回结果,否则调用 `next()` 委托;第一个应答占据唯一的决策槽位。
|
||||
`ctx.approval.request(req)` 要求发起请求的会话处于一个尚未结束的轮次内。它追加 `approval/asked`,获取一个结果,追加对应的 `approval/decided`,然后以该结果完成。`never` 策略在服务内部、waterfall 分发之前强制执行,因此即使后来以 `prepend` 注册的应答者也无法绕过它。应答者在拥有该请求时返回结果,否则调用 `next()` 委托;第一个应答占据唯一的决策槽位。
|
||||
|
||||
审计事件仅写入日志,不进入模型 transcript(文本记录)。模型可见的行为是调用方派生的工具结果,而请求头记录的是模型实际看到的提示词策略。服务 dispose(资源释放)时会一并移除其提示词段落和步骤前叙述器;应答者监听器独立地通过 effect 绑定到其所属插件。
|
||||
审计事件仅写入日志,不进入模型 transcript(文本记录)。模型可见的行为是调用方派生的工具结果与当前运行时上下文快照。服务 dispose(资源释放)时会移除其上下文贡献;应答者监听器独立地通过 effect 绑定到其所属插件。
|
||||
|
||||
@@ -1,6 +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
|
||||
bash.md: 3747244662301a256e12037ea67c21017b5ac2c5
|
||||
bash.zh.md: 9927aa8d51ee410d70bed7a2d00e40061b499e15
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/bash.md
|
||||
bash.md: f83a133c8e049bc111cfabd2b59def18a1b2d607
|
||||
bash.zh.md: 27ca9f6e5a8e05cd4ffa86e72d433b6f22005f09
|
||||
|
||||
@@ -218,4 +218,4 @@ interface BashProcessRead {
|
||||
|
||||
## The service
|
||||
|
||||
`BashExecutor` owns `resolve`, foreground `run`, background-process `start`, and the `sandboxMode` capability fact. `dsh-bash-local` owns command defaulting, timeout/abort classification, the terminal environment, and the background read merge; process groups, bounded collectors, spill files, credential scrubbing, and disposal quiescence are the [subprocess service](subprocess.md)'s. `dsh-tool-bash` owns model-facing rendering and adapts background handles into the [generic task runtime](tasks.md).
|
||||
`BashExecutor` owns `resolve`, foreground `run`, background-process `start`, and the `sandboxMode` capability fact. `dsh-bash-local` owns command defaulting, timeout/abort classification, the terminal environment, and the background read merge; process groups, bounded collectors, spill files, credential scrubbing, and disposal quiescence are the [subprocess service](subprocess.md)'s. `dsh-tool-bash` owns model-facing rendering and adapts background handles into the [generic task runtime](tasks.md). `dsh-bash` owns the shell tools' shared exit-status contract: the exported `parseExitStatus`/`ParsedExitStatus` inverts the `[exit code: N]` / `[killed by signal: X]` markers `dsh-tool-bash`'s `renderResult` and `dsh-tool-pwsh`'s `renderPwshResult` append, and both tools' `presentResult` use it to split the rendered text into the terminal card's output body and its exit-status pill.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](bash.md) | 中文
|
||||
|
||||
bash 执行 seam 分为接口([dsh-bash](../../packages/bash/bash),`ctx.bash`)、实现([dsh-bash-local](../../packages/bash/bash-local) 与 [dsh-bash-sandbox](../../packages/bash/bash-sandbox))和消费方([dsh-tool-bash](../../packages/bash/tool-bash),即 `bash` schema)。通用后台任务的 id、所有权与控制位于 [tasks.md](tasks.md);本 seam 返回一个不含任务概念的进程句柄。原始进程组机制位于[进程管理器 seam](subprocess.md)之后。
|
||||
bash 执行 seam 分为接口([dsh-bash](../../packages/bash/bash),`ctx.bash`)、实现([dsh-bash-local](../../packages/bash/bash-local) 与 [dsh-bash-sandbox](../../packages/bash/bash-sandbox))和消费方([dsh-tool-bash](../../packages/bash/tool-bash),即 `bash` schema)。通用后台任务的 task id、所有权与控制位于 [tasks.md](tasks.md);本 seam 返回一个不含任务概念的进程句柄。原始进程组机制位于[进程管理器 seam](subprocess.md)之后。
|
||||
|
||||
源码:[`packages/bash/bash/src/types.ts`](../../packages/bash/bash/src/types.ts)
|
||||
|
||||
@@ -12,7 +12,7 @@ bash 执行 seam 分为接口([dsh-bash](../../packages/bash/bash),`ctx.bash
|
||||
|
||||
## 请求与规格:`resolve()` 拆分
|
||||
|
||||
该 seam 将**面向模型/插件的请求**(`workdir`/`timeoutMs`/`stdoutMaxBytes` 可选,由配置或请求策略补全)与执行器实际使用的**完全解析后的 spec**(这些字段均为必填)分开。工具层在二者之间调用 `ctx.bash.resolve(request)`——这具体落实了仓库的「包(package) seam 上显式优于隐式」规则:`BashExecSpec` 的读者不必猜测工作目录或输出预算来自何处。
|
||||
该 seam 将**面向模型/插件的请求**(`workdir`/`timeoutMs`/`stdoutMaxBytes` 可选,由配置或请求策略补全)与执行器实际使用的**完全解析后的 spec**(这些字段均为必填)分开。工具层在二者之间调用 `ctx.bash.resolve(request)`——这具体落实了仓库的「包 seam 上显式优于隐式」规则:`BashExecSpec` 的读者不必猜测工作目录或输出预算来自何处。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -98,13 +98,13 @@ interface BashExecSpec {
|
||||
}
|
||||
```
|
||||
|
||||
`stdin` 和 `env` 是受信任的进程内插件输入,不由 `dsh-tool-bash` 暴露。本地执行器会先清除环境中的凭据,再合并调用方显式提供的 env。见 [bash-stdin-env Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)。
|
||||
`stdin` 和 `env` 是受信任的进程内插件输入,不由 `dsh-tool-bash` 暴露。本地执行器会先清除环境中的凭据,再合并调用方显式提供的 env。见 [bash-stdin-env Agent Note](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)。
|
||||
|
||||
`stdoutMaxBytes` 同样仅供受信任插件使用。它让前台消费方能在有界解析预算内请求完整 stdout,而不会改变 stderr、后台任务或面向模型的 bash 工具的常规输出上限。
|
||||
|
||||
## 前台运行:`BashRunResult`
|
||||
|
||||
一次已完成(或被终止)的前台运行的结果。正交的结果**独立报告**:一个进程可以同时超时并以退出码 0 退出(因为它捕获了信号),因此 `timedOut`、`aborted`、`signal` 和 `exitCode` 各自独立为一个字段;调用方永远不会把一次被截断的运行误读为干净的成功。
|
||||
一次已完成(或被终止)的前台运行的结果。正交的结果**独立报告**:一个进程可以同时超时并以退出码 0 退出(因为它捕获了信号),因此 `timedOut`、`aborted`、`signal` 和 `exitCode` 各自独立为一个字段;调用方永远不会把一次被提前中断的运行误读为正常成功。
|
||||
|
||||
```ts type-equiv
|
||||
/** The outcome of one completed (or killed) foreground run. */
|
||||
@@ -166,7 +166,7 @@ interface BashSandboxInfo {
|
||||
|
||||
## 后台进程:`BashProcess`
|
||||
|
||||
`start()` 返回不含 id 或所有者的句柄。`dsh-tool-bash` 将它适配为 `ctx.tasks.start()` 钩子;随后由通用运行时拥有任务标识与生命周期。`done` 在进程关闭时 resolve 且绝不 reject;进程结束后仍可读取,并且沙箱事实会在 `done` resolve 前写入。
|
||||
`start()` 返回不含 id 或所有者的句柄。`dsh-tool-bash` 将它适配为 `ctx.tasks.start()` 钩子;随后由通用运行时拥有任务标识与生命周期。`done` 在进程关闭时完成且绝不被拒绝;进程结束后仍可读取,并且沙箱事实会在 `done` 完成前写入。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -200,7 +200,7 @@ interface BashProcess {
|
||||
}
|
||||
```
|
||||
|
||||
`readOutput()` 返回增量 delta 与 spill 恢复事实:
|
||||
`readOutput()` 返回增量内容与 spill 恢复信息:
|
||||
|
||||
```ts type-equiv
|
||||
/** One incremental {@link BashProcess.readOutput} read. */
|
||||
@@ -218,4 +218,4 @@ interface BashProcessRead {
|
||||
|
||||
## 服务
|
||||
|
||||
`BashExecutor` 拥有 `resolve`、前台 `run`、后台进程 `start` 以及 `sandboxMode` 能力事实。`dsh-bash-local` 拥有命令默认值补全、超时/中止分类、终端环境以及后台读取合并;进程组、有界收集器、spill 文件、凭据清除与 dispose(资源释放)后完全停稳归[进程管理器](subprocess.md)所有。`dsh-tool-bash` 拥有面向模型的渲染,并将后台句柄适配到[通用任务运行时](tasks.md)。
|
||||
`BashExecutor` 拥有 `resolve`、前台 `run`、后台进程 `start` 以及 `sandboxMode` 能力事实。`dsh-bash-local` 拥有命令默认值补全、超时/中止分类、终端环境以及后台读取合并;进程组、有界收集器、spill 文件、凭据清除与 dispose(资源释放)后完全停稳归[进程管理器](subprocess.md)所有。`dsh-tool-bash` 拥有面向模型的渲染,并将后台句柄适配到[通用任务运行时](tasks.md)。`dsh-bash` 拥有 shell 工具共享的退出状态契约:导出的 `parseExitStatus`/`ParsedExitStatus` 是 `dsh-tool-bash` 的 `renderResult` 与 `dsh-tool-pwsh` 的 `renderPwshResult` 所追加的 `[exit code: N]` / `[killed by signal: X]` 标记的逆解析,两个工具的 `presentResult` 都用它把渲染文本拆分为 terminal 卡的输出正文与退出状态 pill。
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/code-runtime.md
|
||||
code-runtime.md: 64de3c45d4f1d1d981daa6c6f074abb667e0aa52
|
||||
code-runtime.zh.md: 4b14aeb2183010e8140540258ce8109df9f59910
|
||||
code-runtime.zh.md: daf07aaf613852a6c4a7b1aff152fcc61052fbca
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](code-runtime.md) | 中文
|
||||
|
||||
代码执行 seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md):其接口([dsh-code-runtime](../../packages/code-runtime/code-runtime),`ctx.codeRuntime`)针对宿主提供的异步 binding 运行一段模型编写的程序,并报告其打印内容与返回值。代码执行是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.md) 中。各后端的执行基底与源语言不同,这两项均为服务上的只读描述符;worker-thread 后端与工具注册表消费方的契约见 [Code Mode 基础设计](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)和[类型化返回契约](../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md)。
|
||||
代码执行 seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md):其接口([dsh-code-runtime](../../packages/code-runtime/code-runtime),`ctx.codeRuntime`)使用宿主提供的异步绑定运行一段模型编写的程序,并报告其打印内容与返回值。代码执行是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.md) 中。各后端的执行基底与源语言不同,这两项均为服务上的只读描述符;worker-thread 后端与工具注册表消费方的契约见 [Code Mode 基础设计](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) 和[类型化返回契约](../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md)。
|
||||
|
||||
源码:[`packages/code-runtime/code-runtime/src/types.ts`](../../packages/code-runtime/code-runtime/src/types.ts)
|
||||
|
||||
## 运行:请求进,结果出
|
||||
|
||||
`CodeRunRequest` 携带**运行时所需的一切**。按照「包(package)边界处显式优于隐式」的规则,默认值(时间预算、输出上限)来自实现的已校验配置,绝不是 `run()` 内部隐藏的 `??`:
|
||||
`CodeRunRequest` 携带**运行时要处理的一切内容**。按照「包边界处显式优于隐式」的规则,默认值(时间预算、输出上限)来自实现的已校验配置,绝不是 `run()` 内部隐藏的 `??`:
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -36,7 +36,7 @@ interface CodeRunRequest {
|
||||
}
|
||||
```
|
||||
|
||||
结果将错误报告为一个**字段**,而非 `run()` 的 rejection。报告失败的程序是调用方的职责,不走异常路径(与 `BashExecutor.run` 的 resolve-on-failure 契约一致):
|
||||
结果将错误报告为一个**字段**,而不是让 `run()` 返回被拒绝的 Promise。报告程序失败是调用方的职责,不走异常路径(与 `BashExecutor.run` 失败时仍正常完成的契约一致):
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -144,4 +144,4 @@ interface CodeRunFailure {
|
||||
|
||||
## 服务
|
||||
|
||||
`CodeRuntime`(`ctx.codeRuntime`,抽象服务,定义于 [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts))由 `run(request)` 加两个只读描述符组成:`language`(程序必须使用的语言,`'typescript'` 是已知值;生成语言相关展示的消费方据此切换,遇到无法展示的语言时应显式报错)和 `isolation`(执行基底,`'worker-thread'`、`'process'`、`'container'`;仅为诊断标签,**不构成安全承诺**)。实现必须保证各次运行彼此隔离(无跨运行状态),并在 dispose(资源释放)时等待系统完全停稳:teardown 完成前,进行中的运行都已终止并等待结束。
|
||||
`CodeRuntime`(`ctx.codeRuntime`,抽象服务,定义于 [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts))由 `run(request)` 加两个只读描述符组成:`language`(程序必须使用的语言,`'typescript'` 是已知值;生成语言相关展示的消费方据此切换,遇到无法展示的语言时应显式报错)和 `isolation`(执行基底,`'worker-thread'`、`'process'`、`'container'`;仅为诊断标签,**不构成安全承诺**)。实现必须保证各次运行彼此隔离(无跨运行状态),并在 dispose(资源释放)时等待系统完全停稳:teardown 要等到所有进行中的运行均已终止并结算后才完成。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/commands.md
|
||||
commands.md: bc7cf676232a4271d7bc21d6688bc598d9325957
|
||||
commands.zh.md: f90e7c93c41699e8c05f5a4b709a75acc846c460
|
||||
commands.zh.md: a2f82c06ac7ca87c5d5cfa525aaf9afa8777e9c9
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](commands.md) | 中文
|
||||
|
||||
[`dsh-commands`](../../packages/ui/commands) 的用户命令 seam。交互式适配器用它发现插件拥有的命令,并针对确切的 agent(智能体)直接执行这些命令,而不创建模型消息。[命令 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md) 负责分发与生命周期的决策依据;[包(package)README](../../packages/ui/commands/README.md) 负责组合方式与限制。
|
||||
[`dsh-commands`](../../packages/ui/commands) 的用户命令 seam。交互式适配器用它发现插件拥有的命令,并针对确切的 agent(智能体)直接执行这些命令,而不创建模型消息。[命令 Agent Note](../../.agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md) 负责分发与生命周期的决策依据;[包 README](../../packages/ui/commands/README.md) 负责组合方式与限制。
|
||||
|
||||
来源:[`packages/ui/commands/src/index.ts`](../../packages/ui/commands/src/index.ts)
|
||||
|
||||
@@ -44,7 +44,7 @@ interface CommandDefinition {
|
||||
|
||||
## 调用与结果
|
||||
|
||||
适配器拥有取消操作,并传入确切的目标 agent。`rawInput` 紧接在解析后的名称之后,并保留适配器传入的分隔符与后缀。结果会直接呈现给 UI,而不是工具结果或会话事件。
|
||||
取消由适配器负责,适配器会传入确切的目标 agent。`rawInput` 紧接在解析后的名称之后,并保留适配器传入的分隔符与后缀。结果会直接呈现给 UI,而不是工具结果或会话事件。
|
||||
|
||||
```ts type-equiv
|
||||
/** Invocation passed to one registered command handler. */
|
||||
|
||||
@@ -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 docs/core-data-structures/compaction.md
|
||||
compaction.md: 911b71d00fa4b42e9cdfa67f67d4e9b29e354a4a
|
||||
compaction.zh.md: 643a116ff2edbbb53d300b4f5ff0ad36d401130b
|
||||
compaction.md: fe64ffe2707ab4a41f1db186965b944d525684c3
|
||||
compaction.zh.md: ea694f30dcc50ef48847ffbd5efdeed36aa234c5
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](compaction.zh.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)).
|
||||
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 human consumer ([dsh-command-compact](../../packages/compact/command-compact)). 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)
|
||||
|
||||
@@ -12,12 +12,14 @@ Compaction extends [`SessionEventMap`](session.md) with three event types via de
|
||||
|
||||
| Event | Payload | Role |
|
||||
|---|---|---|
|
||||
| `compact/start` | `{ turn }` | acquires the log-recorded lock |
|
||||
| `compact/start` | `{ turn }` | acquires the log-recorded lock; a number identifies the open automatic turn, while `null` identifies a standalone manual attempt |
|
||||
| `compact/summary` | `{ summary, rawOutput?, shadowedRange, shadowedSeqs, shadowedTokenCount, provider, model, maxTokens?, usage? }` | provenance: the safe summary projection, optional complete provider output and usage, 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) |
|
||||
| `compact/end` | `{ turn, error? }` | releases the lock with the same numeric-or-null owner (`error` records an unsuccessful attempt) |
|
||||
|
||||
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.
|
||||
|
||||
The markers are lock time points, not an exclusive container. An unrelated idle injection can appear between a standalone manual start and end while summarization is pending. The manual path revalidates only its selected positional span, so that injected context survives after the replacement checkpoint. A live unmatched start blocks every entry point; an unmatched start before a newer `session/end-seed` is stale evidence from a prior lifecycle and is ignored.
|
||||
|
||||
These variants are merged inside a `declare module '@deepseek-ai/dsh-session'` block, so — unlike the top-level types on the other sub-pages — they are not pasted as a drift-checked ` ```ts type-equiv ` block (the `verify-type-equiv` extractor matches only top-level declarations by name). The payload table above is the catalog entry; follow the source link for the authoritative shapes.
|
||||
|
||||
## `CompactionResult`
|
||||
@@ -60,9 +62,24 @@ Automatic callers state why policy is running; implementations may treat confirm
|
||||
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. Every backend marks its replacement `user/message` with the package-exported `COMPACT_CHECKPOINT_SOURCE`; consumers call `isCompactCheckpointSource()` instead of coupling checkpoint recognition to one backend. 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.
|
||||
`CompactService` exposes `compactIfNeeded(agent, trigger, signal)` for automatic `pressure` or `context-overflow` policy, `compactNow(agent, signal)` for one useful idle-session reduction even below pressure, and `compactRegion(...)` for an explicit inclusive surface range. `compactNow()` runs as agent maintenance between turns, returns `null` without writing when no useful range exists, records a standalone `turn: null` bracket before summarization, and flushes a closed attempt before later queued prompts may derive from the new surface. Every backend marks its replacement `user/message` with `COMPACT_CHECKPOINT_SOURCE`; client and wire consumers import that value and `isCompactCheckpointSource()` from the cordis-free `@deepseek-ai/dsh-compact/checkpoint` subpath, while the package root re-exports both for host consumers. The predicate keeps checkpoint recognition independent of any one backend. 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/step` before request derivation. 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 returns a retry action 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.
|
||||
Expected manual failures use `ManualCompactionErrorCode`:
|
||||
|
||||
```ts type-equiv
|
||||
/** Expected failure classes for an explicit idle-session compaction request. */
|
||||
type ManualCompactionErrorCode =
|
||||
| 'busy'
|
||||
| 'cancelled'
|
||||
| 'changed'
|
||||
| 'summary'
|
||||
| 'commit'
|
||||
| 'persistence'
|
||||
```
|
||||
|
||||
`changed` and `summary` leave the conversation surface unchanged but still close and persist the failed attempt in the log. `commit` may follow partial mutation; `persistence` means the in-memory bracket closed but its flush failed. Cancellation remains separate and throws the exact abort reason after required cleanup.
|
||||
|
||||
Pressure compaction runs at serial `agent/pre-step` before request derivation. 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 returns a retry action 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.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](compaction.md) | 中文
|
||||
|
||||
压缩 seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md),与 bash 一样分为接口([dsh-compact](../../packages/compact/compact),`ctx.compact`)、实现(例如 [dsh-compact-basic](../../packages/compact/compact-basic) 后端)和消费方(延期实现的 `/compact` 工具)。压缩是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.md) 中。基于 tokenizer 或模板的后端是实现同一接口的兄弟包(package)。与 bash 不同,该接口必然依赖 `dsh-session` 和 `dsh-llm`:其动词作用于 agent 所有的 `Session`,而其持久摘要事件使用 `ContentBlock` 词汇(见[压缩能力 seam Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md))。
|
||||
压缩 seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md),与 bash 一样分为接口([dsh-compact](../../packages/compact/compact),`ctx.compact`)、实现(例如 [dsh-compact-basic](../../packages/compact/compact-basic) 后端)和面向用户的消费方([dsh-command-compact](../../packages/compact/command-compact))。压缩是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.md) 中。基于 tokenizer 或模板的后端是实现同一接口的兄弟包。与 bash 不同,该接口必然依赖 `dsh-session` 和 `dsh-llm`:其动词作用于 agent 所有的 `Session`,而其持久摘要事件使用 `ContentBlock` 词汇(见[压缩能力 seam Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md))。
|
||||
|
||||
源码:[`packages/compact/compact/src/types.ts`](../../packages/compact/compact/src/types.ts)
|
||||
|
||||
@@ -12,12 +12,14 @@
|
||||
|
||||
| 事件 | 载荷 | 作用 |
|
||||
|---|---|---|
|
||||
| `compact/start` | `{ turn }` | 获取日志记录的锁 |
|
||||
| `compact/start` | `{ turn }` | 获取日志记录的锁;数字标识打开的自动轮次,`null` 标识独立手动尝试 |
|
||||
| `compact/summary` | `{ summary, rawOutput?, shadowedRange, shadowedSeqs, shadowedTokenCount, provider, model, maxTokens?, usage? }` | provenance:安全摘要投影、可选的完整 provider 输出与 usage、被遮蔽的 surface 边界对(`start`/`end` seq——位置跨度,而非数值区间)、按 surface 顺序排列的被遮蔽 seq、估算 token 数,以及摘要调用的 envelope(`provider`、`model`,若有生成上限则还包括该上限)——写入日志后,该一次性请求可由日志 + 代码重建(见可重建性 Agent Note) |
|
||||
| `compact/end` | `{ turn, error? }` | 释放锁(摘要调用抛出异常时设置 `error`) |
|
||||
| `compact/end` | `{ turn, error? }` | 使用相同的数字或 `null` 归属值释放锁(`error` 记录失败尝试) |
|
||||
|
||||
锁括住**整个**操作:先追加 `compact/start`,然后执行摘要生成、写入 `compact/summary` 来源记录与 `user/message` 替换,最后才追加 `compact/end`。最后释放锁意味着操作中途崩溃会表现为可检测的遗留锁(有 `compact/start` 而无匹配的 `compact/end`),而非一个虚假声称压缩已完成的 `compact/end`。
|
||||
|
||||
这些标记表示锁的时间点,而不是排他的容器。摘要等待期间,不相关的空闲注入可以出现在独立的手动 start 与 end 之间。手动路径只重新验证所选位置 span,因此替换检查点之后仍保留该注入上下文。活动的未匹配 start 会阻塞所有入口点;较新 `session/end-seed` 之前的未匹配 start 是先前生命周期留下的陈旧证据,会被忽略。
|
||||
|
||||
这些变体在 `declare module '@deepseek-ai/dsh-session'` 块内合并,因此——与其他子页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。
|
||||
|
||||
## `CompactionResult`
|
||||
@@ -60,9 +62,24 @@ interface CompactionResult {
|
||||
type CompactionTrigger = 'pressure' | 'context-overflow'
|
||||
```
|
||||
|
||||
`CompactService` 暴露 `compactIfNeeded(agent, trigger, signal)` 以执行自动 `pressure` 或 `context-overflow` 策略;没有可安全执行的工作时返回 `null`。它还针对显式、两端均包含的 surface 范围暴露 `compactRegion(...)`。每个后端都使用包导出的 `COMPACT_CHECKPOINT_SOURCE` 标记其替换用的 `user/message`;消费方调用 `isCompactCheckpointSource()`,而不是把检查点识别逻辑耦合到某一个后端。实现必须把传入的 signal 转发给摘要流程。该 seam 不拥有计价 API:单例 [`ctx.tokenMeter`](token-meter.md) 直接拥有估算与回放,而 `dsh-compact-basic` 拥有保留策略、事件排序、按路由执行的摘要调用及其配置。
|
||||
`CompactService` 暴露 `compactIfNeeded(agent, trigger, signal)` 以执行自动 `pressure` 或 `context-overflow` 策略,暴露 `compactNow(agent, signal)` 以便即使未达到压力也对空闲会话进行一次有效缩减,还针对显式、两端均包含的 surface 范围暴露 `compactRegion(...)`。`compactNow()` 作为轮次之间的 agent maintenance 运行;没有有效范围时返回 `null` 且不写入;在摘要前记录独立的 `turn: null` 标记对,并在后续排队提示词能够从新表层派生前 flush 已闭合尝试。每个后端都使用 `COMPACT_CHECKPOINT_SOURCE` 标记其替换用的 `user/message`;client 与 wire 消费方从无 cordis 的 `@deepseek-ai/dsh-compact/checkpoint` 子路径导入该值和 `isCompactCheckpointSource()`,包根则为 host 消费方重新导出两者。该判定函数使检查点识别不依赖任一特定后端。实现必须把传入的 signal 转发给摘要流程。该 seam 不拥有计价 API:单例 [`ctx.tokenMeter`](token-meter.md) 直接拥有估算与回放,而 `dsh-compact-basic` 拥有保留策略、事件排序、按路由执行的摘要调用及其配置。
|
||||
|
||||
压力压缩在串行 `agent/step` 中运行,先于请求推导。一旦压力或规范化溢出满足条件,compact-basic 会在选择范围前调用可选的 [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md),再通过 `ctx.tokenMeter` 重新测量,并且可以在不生成摘要的情况下推进 surface。失败请求的恢复在失败的步骤关闭后通过 `agent/request-error` 运行;仅当 surface replacement generation 前进时才返回重试动作,即便后续摘要工作在剪枝后抛异常亦如此;取消仍然优先。区域边界保持工具调用/结果配对,但不保持整个轮次,因此一个过大轮次中较早关闭的步骤可以被压缩。`dsh-compact-basic` 拥有阈值、保留尾部策略、溢出上限与失败处理。
|
||||
预期的手动失败使用 `ManualCompactionErrorCode`:
|
||||
|
||||
```ts type-equiv
|
||||
/** Expected failure classes for an explicit idle-session compaction request. */
|
||||
type ManualCompactionErrorCode =
|
||||
| 'busy'
|
||||
| 'cancelled'
|
||||
| 'changed'
|
||||
| 'summary'
|
||||
| 'commit'
|
||||
| 'persistence'
|
||||
```
|
||||
|
||||
`changed` 和 `summary` 保持会话表层不变,但仍会闭合失败尝试并将其持久化到日志。`commit` 可能发生在部分变更之后;`persistence` 表示内存中的标记对已闭合,但 flush 失败。取消独立于这些失败,并在完成必要清理后抛出原始 abort 原因。
|
||||
|
||||
压力压缩在串行 `agent/pre-step` 中运行,先于请求推导。一旦压力或规范化溢出满足条件,compact-basic 会在选择范围前调用可选的 [`ctx.toolResultPrune`](../../packages/compact/compact-tool-result-prune/README.md),再通过 `ctx.tokenMeter` 重新测量,并且可以在不生成摘要的情况下推进 surface。失败请求的恢复在失败的步骤关闭后通过 `agent/request-error` 运行;仅当 surface replacement generation 前进时才返回重试动作,即便后续摘要工作在剪枝后抛异常亦如此;取消仍然优先。区域边界保持工具调用/结果配对,但不保持整个轮次,因此一个过大轮次中较早关闭的步骤可以被压缩。`dsh-compact-basic` 拥有阈值、保留尾部策略、溢出上限与失败处理。
|
||||
|
||||
该 seam 导出 `toolPairingBalancedBefore(session, seq)` 与 `toolPairingBalancedAfter(session, seq)`,用于这些边缘检查。两者都会验证当前 surface 成员关系,并拒绝缺失的 seq 与遗留结果;其缓存语义由[包契约](../../packages/compact/compact/README.md#tool-pairing-boundaries)规定。
|
||||
|
||||
|
||||
@@ -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 docs/core-data-structures/core.md
|
||||
core.md: b9df539136c2661537775ba9a425bdf7ef1fd958
|
||||
core.zh.md: 1c75e8484dd1184077fe0194b2b6088230d1bbf5
|
||||
core.md: 495651e1f3105afff15f68822568ff71c531da4f
|
||||
core.zh.md: c895601f39d350811ab1169533287d8f59dba703
|
||||
|
||||
@@ -24,6 +24,8 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
|
||||
| [commands.md](commands.md) | the human-command seam: definitions, adapter discovery, direct invocation, results, and parsing views |
|
||||
| [session.md](session.md) | the full `SessionEventMap` variant catalog, `TurnTrigger`/`TurnEndReason`, `deriveMessages()`, execution enclosure, and standalone events |
|
||||
| [persistence.md](persistence.md) | the durability seam: `SessionPersistence`, JSONL + SQLite backends, `session/flush`, crash recovery, `SessionHeader` |
|
||||
| [settings.md](settings.md) | the user-settings seam: `SettingsNamespace` registration, layered resolution (defaults → composition `base` → user document), owner scopes, hot commits |
|
||||
| [credentials.md](credentials.md) | the credential seam: `CredentialRef` references (never values) in configuration, per-operation resolution, UI-safe `CredentialInfo`, provider source layers |
|
||||
| [session-query.md](session-query.md) | logical records, bounded exact-event reads, relationship traces, semantic filters/documents, and full-text result pages |
|
||||
| [session-title.md](session-title.md) | durable title snapshots, source provenance, and the asynchronous provider contract |
|
||||
| [system-prompt.md](system-prompt.md) | per-assembly context, tool-provider results, prompt sections, and cooperative assembly |
|
||||
@@ -67,14 +69,13 @@ declare module '@deepseek-ai/dsh-llm' {
|
||||
}
|
||||
```
|
||||
|
||||
Six canonical maps use this pattern; a plugin author extends these:
|
||||
Five canonical maps use this pattern; a plugin author extends these:
|
||||
|
||||
| Map | Package | Derives | Catalog |
|
||||
|---|---|---|---|
|
||||
| `ContentBlockMap` | dsh-llm | `ContentBlock` | [below](#content-blocks-and-messages) |
|
||||
| `MessageSourceMap` | dsh-llm | `MessageSource` | [below](#content-blocks-and-messages) |
|
||||
| `FinishReasonMap` | dsh-llm | `FinishReason` | [below](#the-model-request-and-result) |
|
||||
| `TurnTriggerMap` | dsh-session | `TurnTrigger` | [session.md](session.md) |
|
||||
| `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.md) |
|
||||
| `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.md) |
|
||||
|
||||
@@ -159,12 +160,84 @@ Where a message came from is itself a merge-extensible sum type:
|
||||
*/
|
||||
interface MessageSourceMap {
|
||||
user: { kind: 'user' }
|
||||
plugin: { kind: 'plugin'; plugin: string }
|
||||
plugin: { kind: 'plugin'; plugin: string } & ContextFormed
|
||||
model: ModelMessageSource
|
||||
tool: ToolMessageSource
|
||||
}
|
||||
```
|
||||
|
||||
Provenance and shape are two independent axes. `kind` answers *who produced this*; the optional `form` a producer mixes in answers *what shape of information it is*, so several producers may share one presentation and one producer may emit more than one shape over a session. The vocabulary is semantic and grows one value at a time; an absent or unrecognized value is the documented default, presented as opaque content:
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* What SHAPE of information a producer-supplied context carries, declared by
|
||||
* the producer beside its provenance.
|
||||
*
|
||||
* `MessageSource.kind` answers *who produced this*; `form` answers *what kind
|
||||
* of thing it is*, and the two axes are deliberately independent — several
|
||||
* producers share one form (three snapshot producers today), and one producer
|
||||
* may emit more than one form over a session.
|
||||
*
|
||||
* The vocabulary is SEMANTIC, never visual: a value states that the content is
|
||||
* a file's instructions or a catalog of available items, and a consumer decides
|
||||
* what that looks like. Colors, icons, ordering, and collapse defaults are the
|
||||
* consumer's business and must not enter this union. It grows one value at a
|
||||
* time as producers gain the structured fields their form needs; an absent or
|
||||
* unknown value is the documented default, presented as opaque content.
|
||||
*/
|
||||
type ContextForm =
|
||||
/** Instructions read out of workspace files the model is expected to follow. */
|
||||
| 'instructions'
|
||||
/** A catalog of items available in this session, republished as it changes. */
|
||||
| 'catalog'
|
||||
/** Current state, where a later snapshot from the same producer supersedes an earlier one. */
|
||||
| 'snapshot'
|
||||
/** A one-off account of something that just happened; it supersedes nothing. */
|
||||
| 'notice'
|
||||
/** A message another agent addressed to this one. */
|
||||
| 'relay'
|
||||
/** Material lifted out of another session's log, possibly reduced on the way in. */
|
||||
| 'recall'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One named contribution to a `snapshot`-form context, in assembly order. */
|
||||
interface ContextSnapshotSection {
|
||||
/** The contributing subsystem's name. */
|
||||
readonly name: string
|
||||
/** That contribution's model-facing text, exactly as assembled. */
|
||||
readonly text: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Producer-declared {@link ContextForm} and the fields that form requires,
|
||||
* mixed into the source shapes that carry one.
|
||||
*
|
||||
* Discriminated by `form` so a producer cannot declare a shape without the
|
||||
* facts that shape is presented from: a `notice` must record its one-line
|
||||
* account, a `snapshot` its sections. Omitting `form` stays valid — an
|
||||
* undeclared context is the documented default.
|
||||
*/
|
||||
type ContextFormed =
|
||||
| { readonly form?: never }
|
||||
| { readonly form: 'instructions' }
|
||||
| { readonly form: 'catalog' }
|
||||
| {
|
||||
readonly form: 'snapshot'
|
||||
/** The named contributions this snapshot assembled, in order. */
|
||||
readonly sections: readonly ContextSnapshotSection[]
|
||||
}
|
||||
| {
|
||||
readonly form: 'notice'
|
||||
/** One-line account of what happened, shown without expanding the row. */
|
||||
readonly summary: string
|
||||
}
|
||||
| { readonly form: 'relay' }
|
||||
| { readonly form: 'recall' }
|
||||
```
|
||||
|
||||
## Streaming
|
||||
|
||||
Adapters emit a raw **chunk** protocol; the loop logs the chunks (replay fidelity) while feeding the same chunks through a `BlockAssembler` to rebuild blocks and messages. `StreamChunk` is a closed discriminated union over `type` — `block-start`, `text-delta`, `reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`.
|
||||
@@ -181,6 +254,34 @@ Source: [`packages/llm/llm/src/types.ts`](../../packages/llm/llm/src/types.ts)
|
||||
|
||||
Provider and model discovery uses small provider-neutral descriptors. A model catalog is advisory: routing still keys on a registered provider, and an adapter may accept unlisted model ids.
|
||||
|
||||
Registering an adapter returns a handle: the disposer, plus the atomic route replacement a plugin whose route set is user-configurable needs.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* What {@link LlmService.registerAdapter} returns: the disposer, plus an
|
||||
* atomic route replacement for the same adapter instance.
|
||||
*/
|
||||
interface AdapterRegistrationHandle {
|
||||
/** Release every route this registration currently holds. */
|
||||
(): void
|
||||
/**
|
||||
* Replace this registration's routes with `providers`, keeping the same
|
||||
* adapter instance. The candidate set is validated in full first — a
|
||||
* conflict with another adapter, an invalid name, or bad provider metadata
|
||||
* throws and leaves the current routes untouched — and the swap itself is
|
||||
* one synchronous section, so no request can observe a gap. An empty array
|
||||
* is legal here (a settings section that emptied holds zero routes while
|
||||
* staying registered), unlike an empty initial registration.
|
||||
*
|
||||
* Throws `LlmError` with code `REGISTRATION_DISPOSED` once the registration
|
||||
* has been released: its routes are gone and its disposer has already run,
|
||||
* so anything registered afterwards would have no owner left to release it.
|
||||
* @param providers - the complete next route set for this registration.
|
||||
*/
|
||||
replace(providers: string[]): void
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Display metadata for one registered provider route. */
|
||||
interface LlmProviderInfo {
|
||||
@@ -191,6 +292,30 @@ interface LlmProviderInfo {
|
||||
}
|
||||
```
|
||||
|
||||
Adapter plugins additionally declare which routes *could* run through `registerConfigurableProviders()`, addressing each one's user-settings section, so configuration surfaces can offer dormant providers before any route registers.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One provider route an adapter plugin can activate through configuration,
|
||||
* whether or not the route is currently registered. Configuration surfaces
|
||||
* merge this directory with `listProviders()` to offer every configurable
|
||||
* provider alongside its live/dormant state.
|
||||
*/
|
||||
interface LlmConfigurableProvider {
|
||||
/** Provider route key this entry activates when configured. */
|
||||
provider: string
|
||||
/** Human-readable provider name for configuration surfaces. */
|
||||
displayName: string
|
||||
/** User-settings namespace whose section configures this provider. */
|
||||
settingsNs: string
|
||||
/**
|
||||
* Path from that namespace's section root to this provider's profile
|
||||
* object; empty when the whole section is the profile.
|
||||
*/
|
||||
settingsPath: readonly string[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One adapter-discovered model; catalog membership is advisory, not request validation. */
|
||||
interface LlmModelInfo {
|
||||
@@ -205,7 +330,7 @@ interface LlmModelInfo {
|
||||
}
|
||||
```
|
||||
|
||||
Correctness-sensitive metadata is resolved separately from the advisory catalog and is owned by the adapter serving the exact route. Context capacity and reasoning choices share one exact-model result so consumers do not repeat authoritative model resolution.
|
||||
Correctness-sensitive metadata is resolved separately from the advisory catalog and is owned by the adapter serving the exact route. Context capacity, adapter call defaults, and reasoning choices share one exact-model result so consumers do not repeat authoritative model resolution.
|
||||
|
||||
```ts type-equiv
|
||||
/** Provider-owned context capacity for one exact provider/model route. */
|
||||
@@ -252,6 +377,8 @@ interface LlmModelReasoningInfo {
|
||||
interface LlmResolvedModelInfo extends LlmModelInfo {
|
||||
/** Provider-owned context capacity when known. */
|
||||
context?: LlmModelContext
|
||||
/** Adapter-configured per-request output cap materialized when callers omit one. */
|
||||
defaultMaxTokens?: number
|
||||
/** Adapter-owned selectable reasoning levels when exposed. */
|
||||
reasoning?: LlmModelReasoningInfo
|
||||
}
|
||||
@@ -338,9 +465,9 @@ 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, and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
|
||||
The loop builds each request from logged state. `EpochHeader` records call config, adapter-default provenance, rendered prompt, and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
|
||||
|
||||
`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. After the waterfall, the loop prepares the exact model capability under the turn signal, rejects unsupported explicit effort ids without clamping, materializes an adapter-configured default, and logs the effective value. The prepared call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
|
||||
`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus provenance under the turn signal. The prepared call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
|
||||
|
||||
On the wire, a loop-built request reads the `system` slot (the rendered prompt assembly) followed by the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The dev invariant recomputes exactly this equation against every loop-built request.
|
||||
|
||||
@@ -363,6 +490,17 @@ interface LlmCallConfig {
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Effective config fields supplied by exact-model adapter resolution rather
|
||||
* than by the caller's request proposal.
|
||||
*/
|
||||
interface LlmCallConfigAdapterDefaults {
|
||||
reasoningEffort?: true
|
||||
maxTokens?: true
|
||||
}
|
||||
```
|
||||
|
||||
## Sessions
|
||||
|
||||
A `Session` is an **append-only log** of typed `SessionEvent`s — the single source of truth. The LLM message history is *derived* from the log (`deriveMessages()`), not stored separately. The event vocabulary derives from `SessionEventMap`:
|
||||
@@ -378,7 +516,7 @@ Source: [`packages/core/session/src/types.ts`](../../packages/core/session/src/t
|
||||
*
|
||||
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
|
||||
* they only exist on {@link SurfaceEventType} variants (`user/message`,
|
||||
* `assistant/message`, `tool/result`, `steering/message`).
|
||||
* `assistant/message`, `tool/result`).
|
||||
* Non-surface events (boundary markers, chunks, usage, errors) never carry
|
||||
* surface metadata — the compiler enforces this at `Session.append()`
|
||||
* call sites.
|
||||
@@ -406,7 +544,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}[T]
|
||||
```
|
||||
|
||||
The twelve event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `user/message`, `assistant/chunk`, `assistant/message`, `tool/call`, `tool/result`, `steering/message`, `todo/write`, `request/header`), the `deriveMessages()` projection rules, the `TurnTrigger`/`TurnEndReason` reasons, and the execution-enclosure and standalone-event rules are on **[session.md](session.md)**. How the log is made durable — the `SessionPersistence` seam, JSONL/SQLite backends, the `session/flush` checkpoint, crash recovery, and `SessionHeader` — is on **[persistence.md](persistence.md)**.
|
||||
The session event variants, `deriveMessages()` projection rules, `TurnEndReason` vocabulary, and execution-enclosure and standalone-event rules are on **[session.md](session.md)**. How the log is made durable — the `SessionPersistence` seam, JSONL/SQLite backends, the `session/flush` checkpoint, crash recovery, and `SessionHeader` — is on **[persistence.md](persistence.md)**.
|
||||
|
||||
## The agent handle
|
||||
|
||||
@@ -415,45 +553,11 @@ The twelve event variants (`turn/start`, `turn/end`, `step/start`, `step/end`, `
|
||||
Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Which inbox queue a {@link Agent.send} item joins:
|
||||
* - `next-turn` — the item becomes its own turn, claimed at a turn boundary.
|
||||
* - `next-step` — during prompt admission or an open turn, the item stages for
|
||||
* the next safe step boundary; otherwise it is promoted per its `wakeup`
|
||||
* flag.
|
||||
*/
|
||||
type SendTarget = 'next-turn' | 'next-step'
|
||||
/** One of the two ordered pending-message lists owned by an agent. */
|
||||
type InboxTarget = 'next-turn' | 'next-step'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Resolved inbox placement reported when an accepted message is enqueued. */
|
||||
type InboxPlacement = 'queued' | 'steering'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Options for the unified {@link Agent.send} primitive over the
|
||||
* (`target` × `wakeup`) matrix. Named presets: {@link Agent.followup}
|
||||
* (`next-turn`/wakeup), {@link Agent.steer} (`next-step`/wakeup), and
|
||||
* {@link Agent.inject} (`next-step`/no-wakeup).
|
||||
*
|
||||
* The object is complete so routing policy is explicit.
|
||||
*/
|
||||
interface SendOptions {
|
||||
/** Queue the item joins. */
|
||||
target: SendTarget
|
||||
/**
|
||||
* Whether this item makes the model run: wake a parked driver (`next-turn`)
|
||||
* or force a continuation step (`next-step` while running). A `false`
|
||||
* `next-turn` item queues without waking; a `false`
|
||||
* `next-step` item attaches durable context without forcing another step
|
||||
* (the injection preset).
|
||||
*/
|
||||
wakeup: boolean
|
||||
}
|
||||
```
|
||||
|
||||
The fixed-preset aliases own `target` and `wakeup`; their already identified `UserMessage` carries role, content, and provenance. Its `MessageId` remains stable across that message's `agent/inbox/*` events without being returned by the delivery methods. Injection bypasses the FIFOs and never appears on those events.
|
||||
Every pending occurrence is its `UserMessage`; `MessageId` is the sole identity. `Inbox.append`, `prepend`, `replace`, `remove`, `clear`, `splice`, and `claim` record normalized durable `agent/inbox/spliced` mutations and reject duplicate pending ids. `replace(messageId, newMessage)` and `remove(messageId)` locate the pending message across both lists; replacement may change identity and emits the old message as discarded followed by the new message as inserted. Ordinary removals and `clear()` are cancellations. `claim(target)` removes the proposed step batch — all `next-step` input plus, at a turn boundary, one `next-turn` message — through pure deletion splices without emitting discarded notifications, and the loop separately emits per-message claimed notifications. Whole-queue consumers such as UI projections reconstruct `nextTurn` and `nextStep` from the durable splices, while consumers following one message use the exact `agent/inbox/inserted`, `claimed`, and `discarded` notifications.
|
||||
|
||||
```ts type-equiv
|
||||
/** Options for {@link Agent.cancel}. */
|
||||
@@ -461,23 +565,25 @@ interface CancelOptions {
|
||||
/**
|
||||
* Preserve queued and steering inbox items instead of discarding them. The
|
||||
* active turn is still aborted, but un-started and pending work survives for a
|
||||
* later turn and no `agent/inbox/discard` fires.
|
||||
* later turn and no canceled inbox splice is logged.
|
||||
*/
|
||||
keepInbox?: boolean
|
||||
keepInbox?: boolean | undefined
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable runtime cause accepted by {@link Agent.cancel}. */
|
||||
/** Why an active agent driver was cancelled. */
|
||||
type AgentCancelCause =
|
||||
| { readonly kind: 'user' }
|
||||
| { readonly kind: 'parent' }
|
||||
| { readonly kind: 'hook'; readonly reason: string }
|
||||
| { readonly kind: 'disposed' }
|
||||
```
|
||||
|
||||
`Agent` is an interface over the public live-agent contract. Concrete drivers own the `followup`/`steer`/`inject` aliases and route them through `send`'s (`target` × `wakeup`) matrix.
|
||||
`Agent` is an interface over the public live-agent contract. Its unified `send` method exposes target and wakeup routing directly; `followup`, `steer`, and `inject` are fixed-preset aliases.
|
||||
|
||||
```ts type-equiv
|
||||
/** Public live-agent handle with aliases over the unified delivery primitive. */
|
||||
/** Public live-agent handle. */
|
||||
interface Agent {
|
||||
/** The single identity shared with {@link session}. */
|
||||
readonly id: SessionId
|
||||
@@ -485,89 +591,81 @@ interface Agent {
|
||||
readonly options: AgentOptions
|
||||
/** The live session this agent drives; its log is the durable source of truth. */
|
||||
readonly session: Session
|
||||
/** The agent-owned projection of durable pending work. */
|
||||
readonly inbox: Inbox
|
||||
/** The current lifecycle state, mirrored on every `agent/status` transition. */
|
||||
readonly status: AgentStatus
|
||||
/**
|
||||
* Whether a `next-step` send currently stages for prompt admission or the
|
||||
* open turn. Unlike {@link status}, this excludes admission exit and turn
|
||||
* settlement, when a waking `next-step` send becomes a queued follow-up.
|
||||
*/
|
||||
readonly acceptsNextStep: boolean
|
||||
/** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */
|
||||
readonly ctx: Context
|
||||
|
||||
/**
|
||||
* The unified delivery primitive over the (`target` × `wakeup`) matrix.
|
||||
* It routes the caller's typed content and source as follows:
|
||||
*
|
||||
* - `next-turn` queues an item that becomes the sole ordinary message of its
|
||||
* own FIFO-ordered turn; `wakeup:true` wakes a
|
||||
* parked driver, while `wakeup:false` queues without waking.
|
||||
* - `next-step` with `wakeup:true` stages steering during prompt admission
|
||||
* or an open turn; outside that window it falls back to a woken
|
||||
* `next-turn`.
|
||||
* - `next-step` with `wakeup:false` injects durable model-facing context
|
||||
* without running the model: admission or an open turn stages it for the
|
||||
* next safe log position, while an injection outside that window appends
|
||||
* immediately without opening a turn. If admission closes without a turn,
|
||||
* a context-only boundary appends immediately; context staged beside
|
||||
* steering remains pending with it.
|
||||
* The agent publishes or queues the identified frozen message as-is.
|
||||
* @param message - identified model-facing content and its producer provenance.
|
||||
* @param options - target queue and wakeup decision.
|
||||
*/
|
||||
send(message: UserMessage, options: SendOptions): void
|
||||
|
||||
/**
|
||||
* Clear queued and steering work — unless `keepInbox` — and abort the active
|
||||
* turn. An effective call first emits `agent/cancel-requested` with the
|
||||
* resolved typed cause. The first cause wins for the active turn, and
|
||||
* `whenIdle()` resolves after cancellation reaches quiescence. Idle
|
||||
* cancellation is a no-op and does not arm later work.
|
||||
* @param cause - the stable caller intent carried by the current turn signal.
|
||||
* turn or between-turn task. The first cause wins for that activity. With no
|
||||
* active activity, cancellation is a no-op and does not arm later work.
|
||||
* @param cause - the stable caller intent carried by the active operation signal.
|
||||
* @param options - cancellation options; `keepInbox` preserves pending work.
|
||||
*/
|
||||
cancel(cause: AgentCancelCause, options?: CancelOptions): void
|
||||
|
||||
/** Resolve at idle quiescence; disposal waits for driver exit rather than only the status transition. */
|
||||
/**
|
||||
* Resolve after the current whole-agent activity reaches quiescence. This
|
||||
* follows replacement work started before the observed driver retires,
|
||||
* but does not identify the settlement of any particular message.
|
||||
* @returns fulfillment after no active driver or maintenance task remains.
|
||||
*/
|
||||
whenIdle(): Promise<void>
|
||||
|
||||
/**
|
||||
* Queue an ordinary follow-up turn and wake the driver — the
|
||||
* `next-turn`/wakeup preset of {@link send}. The item becomes the sole
|
||||
* ordinary message of its own turn.
|
||||
* Run one non-turn maintenance task from the true idle phase. The task starts
|
||||
* synchronously after claiming that phase; later waking input remains in the
|
||||
* inbox until the task settles, while public status stays `idle`.
|
||||
* `whenIdle()` follows both the task and any waking work released behind it.
|
||||
* @param task - operation whose fulfillment or rejection is preserved, with a signal aborted by {@link cancel}.
|
||||
* @throws synchronously when turn-driving or another maintenance task already owns the agent.
|
||||
* @returns the task promise.
|
||||
*/
|
||||
runMaintenance<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T>
|
||||
|
||||
/**
|
||||
* Route identified input to an inbox boundary and optionally wake the driver.
|
||||
* Waking input submitted after active cancellation is queued for the next turn.
|
||||
* @param message - identified content and its producer provenance.
|
||||
* @param target - the preferred next-turn or next-step inbox boundary.
|
||||
* @param wakeup - whether delivery may wake the driver.
|
||||
*/
|
||||
send(message: UserMessage, target: InboxTarget, wakeup: boolean): void
|
||||
|
||||
/**
|
||||
* Queue an ordinary follow-up turn and wake the driver. The item becomes the
|
||||
* sole ordinary message of its own turn.
|
||||
* @param message - identified prompt content and its producer provenance.
|
||||
*/
|
||||
followup(message: UserMessage): void
|
||||
|
||||
/**
|
||||
* Submit steering during prompt admission or an open turn — the
|
||||
* `next-step`/wakeup preset of {@link send}. It stages for the next steering
|
||||
* checkpoint before a request or stop decision. If the activity fails before
|
||||
* that boundary, the remainder stays staged without waking the agent; retry
|
||||
* or a later prompt takes it. Outside that window steering falls back to a
|
||||
* woken follow-up turn, while cancellation or disposal may discard pending
|
||||
* steering.
|
||||
* Submit steering for the nearest step. An idle driver starts a turn;
|
||||
* a running driver consumes it at its next step boundary.
|
||||
* A rejected step leaves steering parked in the inbox until the next
|
||||
* wake; cancellation or disposal may discard pending steering.
|
||||
* @param message - identified steering content and its producer provenance.
|
||||
*/
|
||||
steer(message: UserMessage): void
|
||||
|
||||
/**
|
||||
* Append model-facing context without running the model — the
|
||||
* `next-step`/no-wakeup preset of {@link send}. Admission or an open turn
|
||||
* stages it at the next safe log position; outside that window it appends
|
||||
* immediately without opening a turn. If admission closes without a turn,
|
||||
* a context-only boundary appends immediately; context staged beside
|
||||
* steering remains pending with it.
|
||||
* Queue model-facing context for the next pre-step without waking the
|
||||
* driver. A running driver claims it at the nearest later step boundary;
|
||||
* idle drivers leave it pending until follow-up or steering
|
||||
* wakes them. It may miss a request whose pre-step already claimed its
|
||||
* batch. Cancellation or disposal may discard pending context.
|
||||
* @param message - identified injected context and its producer provenance.
|
||||
*/
|
||||
inject(message: UserMessage): void
|
||||
}
|
||||
```
|
||||
|
||||
`AgentStatus` is `'idle' | 'running'`, and `SessionId` is branded. Disposal removes the agent from the registry and emits `agent/disposed`; it is not a terminal status value. `running` describes the driver-wide drain interval and may span consecutive queued turns; it does not prove a turn is still open. `acceptsNextStep` is the narrower routing predicate for callers that must choose between steering the current admission/turn and submitting a fresh admitted prompt. `AgentOptions` is merge-extensible: core declares `provider?`, `model?`, and `maxTokens?` (dispatch requires provider and model after `agent/request`). When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission leaves the provider default in control. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
|
||||
`AgentStatus` is `'idle' | 'running'`, and `SessionId` is branded. Disposal removes the agent from the registry and emits `agent/disposed`; it is not a terminal status value. `running` describes the driver-wide drain interval and may span consecutive queued turns; it does not prove a turn is still open. `followup()` returns no handle: its `MessageId` identifies durable inbox insertion, claim, and discard facts, not a later assistant output or turn ending. `whenIdle()` observes the whole agent, so callers may call a receipt-to-idle interval a run only when they explicitly own that interval ([decision](../../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md)). `AgentOptions` is merge-extensible: core declares `provider?`, `model?`, and `maxTokens?` (dispatch requires provider and model after `agent/request`). When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
|
||||
|
||||
The cause is a TypeScript-enforced same-process input. An active `TurnCancellation` holder copies its discriminant into the runtime-only `AbortSignal.reason` and is retired before `turn/end` publication; the frozen `AbortSignal.reason` remains readable after that retirement. Only the loop reads the cause (`user`, `parent`, or lifecycle-only `disposed`) back off its own machine-private signal at settlement — there is no public reader, and a signal grants cooperating listeners no classification authority. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result.
|
||||
The cause is a TypeScript-enforced same-process input. An active cancellation holder copies it into the runtime-only `AbortSignal.reason`; a signal grants cooperating listeners no classification authority. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result.
|
||||
|
||||
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.
|
||||
|
||||
@@ -577,22 +675,31 @@ The process-local initiator carried by `ctx.agents` is the exact `Agent` above,
|
||||
|
||||
## Interception decisions
|
||||
|
||||
Prompt and post-tool decisions use the same identified `UserMessage` shape as durable user-role input. Each `additionalContexts` entry becomes a separate `user/message`, preserving its identity and provenance. Hook bridges map their native decision fields onto these typed results.
|
||||
Pre-step decisions use the same identified `UserMessage` shape as durable user-role input. The entered batch is authoritative and preserves every message's identity and provenance. Hook bridges map their native decision fields onto this typed result.
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
`agent/prompt-submit` returns a `PromptDecision` before a turn opens. Allow may rewrite the claimed prompt or attach `additionalContexts`; block rejects admission without creating turn events:
|
||||
`agent/pre-step` receives the exclusive claimed batch and the proposed step's coordinates and cancellation signal. The initial proposal runs inside an open turn before any step; a tool continuation may submit an empty claimed batch between steps:
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Prompt interception result. `allow.content` replaces the prompt, while
|
||||
* `additionalContexts` appends model-facing context before the turn starts.
|
||||
* An `allow` returned by a listener is authoritative: a listener wrapping
|
||||
* `next()` preserves both fields unless it intentionally replaces them.
|
||||
*/
|
||||
type PromptDecision =
|
||||
| { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: UserMessage[] }
|
||||
| { kind: 'block'; reason: string }
|
||||
/** Coordinates and cancellation for a proposed step. */
|
||||
interface PreStepContext {
|
||||
/** Turn that will own the step. */
|
||||
readonly turn: number
|
||||
/** Step proposed by the loop. */
|
||||
readonly step: number
|
||||
/** Current turn cancellation signal. */
|
||||
readonly signal: AbortSignal
|
||||
}
|
||||
```
|
||||
|
||||
It returns a `PreStepDecision`. Reject opens no step. Enter supplies the complete message batch appended after `step/start`; claimed messages omitted by the final decision remain removed, while input inserted after the claim stays pending:
|
||||
|
||||
```ts type-equiv
|
||||
/** Whether and with which messages the loop enters a proposed step. */
|
||||
type PreStepDecision =
|
||||
| { kind: 'reject' }
|
||||
| { kind: 'enter'; messages: UserMessage[] }
|
||||
```
|
||||
|
||||
`agent/request-error` runs after a failed model step closes and before its turn closes. Listeners can repair durable state or await policy work while the failed turn's signal is still live. A handling listener returns `{ kind: 'retry' }` without calling `next()`; the default `undefined` leaves the failure terminal.
|
||||
@@ -602,12 +709,7 @@ type PromptDecision =
|
||||
type RequestErrorAction = { kind: 'retry' } | undefined
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Model-request failure with an optional machine-routable provider code. */
|
||||
type RequestError = Error & { code?: string }
|
||||
```
|
||||
|
||||
`agent/step` is the single serial boundary before request derivation. `agent/turn-stopping` runs when a turn has no tool or steering continuation, before one final steering drain.
|
||||
`agent/pre-step` is the single serial boundary before request derivation. `agent/turn-stopping` runs when a turn has no tool or steering continuation, before one final steering drain.
|
||||
|
||||
`agent/session-start` carries a `SessionStartSource` (why the session lifecycle began; a bridge keys its SessionStart matcher on it):
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ harness 是一个微内核:一个极小的核心加上众多插件。大多数
|
||||
| [commands.md](commands.md) | 人类命令 seam:定义、适配器发现、直接调用、结果与解析视图 |
|
||||
| [session.md](session.md) | 完整的 `SessionEventMap` 变体目录、`TurnTrigger`/`TurnEndReason`、`deriveMessages()`、执行封闭与独立事件 |
|
||||
| [persistence.md](persistence.md) | 持久性 seam:`SessionPersistence`、JSONL + SQLite 后端、`session/flush`、崩溃恢复、`SessionHeader` |
|
||||
| [settings.md](settings.md) | 用户设置 seam:`SettingsNamespace` 注册、分层解析(默认值 → 组合 `base` → 用户文档)、owner scope、热提交 |
|
||||
| [credentials.md](credentials.md) | 凭据 seam:配置中的 `CredentialRef` 引用(绝不含值)、按操作解析、对 UI 安全的 `CredentialInfo`、provider 来源层 |
|
||||
| [session-query.md](session-query.md) | 逻辑记录、有界精确事件读取、关系追踪、语义筛选器/文档与全文检索结果页 |
|
||||
| [session-title.md](session-title.md) | 持久标题快照、来源 provenance 与异步提供方契约 |
|
||||
| [system-prompt.md](system-prompt.md) | 逐次组装的上下文、工具提供方结果、提示词段落与协作式组装 |
|
||||
@@ -69,14 +71,13 @@ declare module '@deepseek-ai/dsh-llm' {
|
||||
}
|
||||
```
|
||||
|
||||
六个规范 map 使用此模式;插件作者扩展它们:
|
||||
五个规范 map 使用此模式;插件作者扩展它们:
|
||||
|
||||
| Map | 包 | 派生 | 目录 |
|
||||
|---|---|---|---|
|
||||
| `ContentBlockMap` | dsh-llm | `ContentBlock` | [下文](#content-blocks-and-messages) |
|
||||
| `MessageSourceMap` | dsh-llm | `MessageSource` | [下文](#content-blocks-and-messages) |
|
||||
| `FinishReasonMap` | dsh-llm | `FinishReason` | [下文](#the-model-request-and-result) |
|
||||
| `TurnTriggerMap` | dsh-session | `TurnTrigger` | [session.md](session.md) |
|
||||
| `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.md) |
|
||||
| `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.md) |
|
||||
|
||||
@@ -165,12 +166,84 @@ interface Message {
|
||||
*/
|
||||
interface MessageSourceMap {
|
||||
user: { kind: 'user' }
|
||||
plugin: { kind: 'plugin'; plugin: string }
|
||||
plugin: { kind: 'plugin'; plugin: string } & ContextFormed
|
||||
model: ModelMessageSource
|
||||
tool: ToolMessageSource
|
||||
}
|
||||
```
|
||||
|
||||
溯源与形态是相互独立的两根轴。`kind` 回答「由谁产生」;生产方可选混入的 `form` 回答「这是何种形态的信息」,因此多个生产方可以共用一种呈现,一个生产方在一次会话中也可以发出多种形态。该词汇表是语义的,逐个取值增长;未声明或无法识别的取值是有文档的默认,按不透明内容呈现:
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* What SHAPE of information a producer-supplied context carries, declared by
|
||||
* the producer beside its provenance.
|
||||
*
|
||||
* `MessageSource.kind` answers *who produced this*; `form` answers *what kind
|
||||
* of thing it is*, and the two axes are deliberately independent — several
|
||||
* producers share one form (three snapshot producers today), and one producer
|
||||
* may emit more than one form over a session.
|
||||
*
|
||||
* The vocabulary is SEMANTIC, never visual: a value states that the content is
|
||||
* a file's instructions or a catalog of available items, and a consumer decides
|
||||
* what that looks like. Colors, icons, ordering, and collapse defaults are the
|
||||
* consumer's business and must not enter this union. It grows one value at a
|
||||
* time as producers gain the structured fields their form needs; an absent or
|
||||
* unknown value is the documented default, presented as opaque content.
|
||||
*/
|
||||
type ContextForm =
|
||||
/** Instructions read out of workspace files the model is expected to follow. */
|
||||
| 'instructions'
|
||||
/** A catalog of items available in this session, republished as it changes. */
|
||||
| 'catalog'
|
||||
/** Current state, where a later snapshot from the same producer supersedes an earlier one. */
|
||||
| 'snapshot'
|
||||
/** A one-off account of something that just happened; it supersedes nothing. */
|
||||
| 'notice'
|
||||
/** A message another agent addressed to this one. */
|
||||
| 'relay'
|
||||
/** Material lifted out of another session's log, possibly reduced on the way in. */
|
||||
| 'recall'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One named contribution to a `snapshot`-form context, in assembly order. */
|
||||
interface ContextSnapshotSection {
|
||||
/** The contributing subsystem's name. */
|
||||
readonly name: string
|
||||
/** That contribution's model-facing text, exactly as assembled. */
|
||||
readonly text: string
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Producer-declared {@link ContextForm} and the fields that form requires,
|
||||
* mixed into the source shapes that carry one.
|
||||
*
|
||||
* Discriminated by `form` so a producer cannot declare a shape without the
|
||||
* facts that shape is presented from: a `notice` must record its one-line
|
||||
* account, a `snapshot` its sections. Omitting `form` stays valid — an
|
||||
* undeclared context is the documented default.
|
||||
*/
|
||||
type ContextFormed =
|
||||
| { readonly form?: never }
|
||||
| { readonly form: 'instructions' }
|
||||
| { readonly form: 'catalog' }
|
||||
| {
|
||||
readonly form: 'snapshot'
|
||||
/** The named contributions this snapshot assembled, in order. */
|
||||
readonly sections: readonly ContextSnapshotSection[]
|
||||
}
|
||||
| {
|
||||
readonly form: 'notice'
|
||||
/** One-line account of what happened, shown without expanding the row. */
|
||||
readonly summary: string
|
||||
}
|
||||
| { readonly form: 'relay' }
|
||||
| { readonly form: 'recall' }
|
||||
```
|
||||
|
||||
## 流式输出
|
||||
|
||||
适配器发出原始**分片**协议;循环记录分片(回放保真度),同时将同一批分片送入 `BlockAssembler` 以重建块和消息。`StreamChunk` 是基于 `type` 的封闭判别联合——`block-start`、`text-delta`、`reasoning-delta`、`tool-call-delta`、`block-end`、`usage`、`finish`。
|
||||
@@ -187,6 +260,34 @@ interface MessageSourceMap {
|
||||
|
||||
提供方与模型发现使用小型、提供方无关的描述符。模型目录仅供参考:路由仍以已注册提供方为键,适配器也可以接受未列出的模型 id。
|
||||
|
||||
注册适配器会返回一个句柄:既是释放器,也带有原子的路由替换——路由集合由用户配置决定的插件正需要它。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* What {@link LlmService.registerAdapter} returns: the disposer, plus an
|
||||
* atomic route replacement for the same adapter instance.
|
||||
*/
|
||||
interface AdapterRegistrationHandle {
|
||||
/** Release every route this registration currently holds. */
|
||||
(): void
|
||||
/**
|
||||
* Replace this registration's routes with `providers`, keeping the same
|
||||
* adapter instance. The candidate set is validated in full first — a
|
||||
* conflict with another adapter, an invalid name, or bad provider metadata
|
||||
* throws and leaves the current routes untouched — and the swap itself is
|
||||
* one synchronous section, so no request can observe a gap. An empty array
|
||||
* is legal here (a settings section that emptied holds zero routes while
|
||||
* staying registered), unlike an empty initial registration.
|
||||
*
|
||||
* Throws `LlmError` with code `REGISTRATION_DISPOSED` once the registration
|
||||
* has been released: its routes are gone and its disposer has already run,
|
||||
* so anything registered afterwards would have no owner left to release it.
|
||||
* @param providers - the complete next route set for this registration.
|
||||
*/
|
||||
replace(providers: string[]): void
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Display metadata for one registered provider route. */
|
||||
interface LlmProviderInfo {
|
||||
@@ -197,6 +298,30 @@ interface LlmProviderInfo {
|
||||
}
|
||||
```
|
||||
|
||||
适配器插件还会通过 `registerConfigurableProviders()` 声明哪些路由*可以*运行,并指明每条路由的用户设置分节,使配置界面能在任何路由注册之前就呈现休眠的提供方。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One provider route an adapter plugin can activate through configuration,
|
||||
* whether or not the route is currently registered. Configuration surfaces
|
||||
* merge this directory with `listProviders()` to offer every configurable
|
||||
* provider alongside its live/dormant state.
|
||||
*/
|
||||
interface LlmConfigurableProvider {
|
||||
/** Provider route key this entry activates when configured. */
|
||||
provider: string
|
||||
/** Human-readable provider name for configuration surfaces. */
|
||||
displayName: string
|
||||
/** User-settings namespace whose section configures this provider. */
|
||||
settingsNs: string
|
||||
/**
|
||||
* Path from that namespace's section root to this provider's profile
|
||||
* object; empty when the whole section is the profile.
|
||||
*/
|
||||
settingsPath: readonly string[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** One adapter-discovered model; catalog membership is advisory, not request validation. */
|
||||
interface LlmModelInfo {
|
||||
@@ -211,7 +336,7 @@ interface LlmModelInfo {
|
||||
}
|
||||
```
|
||||
|
||||
对正确性敏感的元数据与参考目录分开解析,并归服务该确切路由的适配器所有。上下文容量和推理选项共用同一个确切模型结果,消费方因而无需重复执行权威模型解析。
|
||||
对正确性敏感的元数据与参考目录分开解析,并归服务该确切路由的适配器所有。上下文容量、适配器调用默认值和推理选项共用同一个确切模型结果,消费方因而无需重复执行权威模型解析。
|
||||
|
||||
```ts type-equiv
|
||||
/** Provider-owned context capacity for one exact provider/model route. */
|
||||
@@ -258,6 +383,8 @@ interface LlmModelReasoningInfo {
|
||||
interface LlmResolvedModelInfo extends LlmModelInfo {
|
||||
/** Provider-owned context capacity when known. */
|
||||
context?: LlmModelContext
|
||||
/** Adapter-configured per-request output cap materialized when callers omit one. */
|
||||
defaultMaxTokens?: number
|
||||
/** Adapter-owned selectable reasoning levels when exposed. */
|
||||
reasoning?: LlmModelReasoningInfo
|
||||
}
|
||||
@@ -344,9 +471,9 @@ interface ToolSchema {
|
||||
|
||||
### 请求信封:`LlmCallConfig` 与记录的 header
|
||||
|
||||
循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、渲染后的提示词以及权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
|
||||
循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、适配器默认值来源、渲染后的提示词以及权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
|
||||
|
||||
`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall 结束后,循环会在轮次信号控制下完成确切模型的能力准备,拒绝显式指定但不受支持的推理强度 ID(不自动调整),填入适配器配置的默认值,并记录最终生效值。准备完成的调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
|
||||
`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall 开始前,循环会移除标记为适配器默认值的值,使确切模型准备过程填入所选路由的当前值;未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID(不自动调整),并记录生效配置及其来源。准备完成的调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
|
||||
|
||||
在协议格式上,循环构建的请求先读取 `system` 槽位(渲染后的提示词组装),再读取派生历史——边界快照,其尾部在轮次首步是最新的 `user/message`,在后续步骤是上一步的工具结果。开发不变式针对每个循环构建的请求精确重算此等式。
|
||||
|
||||
@@ -369,6 +496,17 @@ interface LlmCallConfig {
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Effective config fields supplied by exact-model adapter resolution rather
|
||||
* than by the caller's request proposal.
|
||||
*/
|
||||
interface LlmCallConfigAdapterDefaults {
|
||||
reasoningEffort?: true
|
||||
maxTokens?: true
|
||||
}
|
||||
```
|
||||
|
||||
## 会话
|
||||
|
||||
`Session` 是一份类型化 `SessionEvent` 的**仅追加日志**——唯一的真源。LLM(大语言模型)消息历史从日志*派生*(`deriveMessages()`),而非单独存储。事件词汇从 `SessionEventMap` 派生:
|
||||
@@ -384,7 +522,7 @@ interface LlmCallConfig {
|
||||
*
|
||||
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
|
||||
* they only exist on {@link SurfaceEventType} variants (`user/message`,
|
||||
* `assistant/message`, `tool/result`, `steering/message`).
|
||||
* `assistant/message`, `tool/result`).
|
||||
* Non-surface events (boundary markers, chunks, usage, errors) never carry
|
||||
* surface metadata — the compiler enforces this at `Session.append()`
|
||||
* call sites.
|
||||
@@ -412,7 +550,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}[T]
|
||||
```
|
||||
|
||||
十二种事件变体(`turn/start`、`turn/end`、`step/start`、`step/end`、`user/message`、`assistant/chunk`、`assistant/message`、`tool/call`、`tool/result`、`steering/message`、`todo/write`、`request/header`)、`deriveMessages()` 投影规则、`TurnTrigger`/`TurnEndReason` 原因以及执行封闭和独立事件规则都在 **[session.md](session.md)** 中。日志如何持久化——`SessionPersistence` seam、JSONL/SQLite 后端、`session/flush` 检查点、崩溃恢复与 `SessionHeader`——则在 **[persistence.md](persistence.md)** 中。
|
||||
会话事件变体、`deriveMessages()` 投影规则、`TurnEndReason` 词汇以及执行封闭和独立事件规则都在 **[session.md](session.md)** 中。日志如何持久化——`SessionPersistence` seam、JSONL/SQLite 后端、`session/flush` 检查点、崩溃恢复与 `SessionHeader`——则在 **[persistence.md](persistence.md)** 中。
|
||||
|
||||
<a id="the-agent-handle"></a>
|
||||
|
||||
@@ -423,45 +561,11 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Which inbox queue a {@link Agent.send} item joins:
|
||||
* - `next-turn` — the item becomes its own turn, claimed at a turn boundary.
|
||||
* - `next-step` — during prompt admission or an open turn, the item stages for
|
||||
* the next safe step boundary; otherwise it is promoted per its `wakeup`
|
||||
* flag.
|
||||
*/
|
||||
type SendTarget = 'next-turn' | 'next-step'
|
||||
/** One of the two ordered pending-message lists owned by an agent. */
|
||||
type InboxTarget = 'next-turn' | 'next-step'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Resolved inbox placement reported when an accepted message is enqueued. */
|
||||
type InboxPlacement = 'queued' | 'steering'
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Options for the unified {@link Agent.send} primitive over the
|
||||
* (`target` × `wakeup`) matrix. Named presets: {@link Agent.followup}
|
||||
* (`next-turn`/wakeup), {@link Agent.steer} (`next-step`/wakeup), and
|
||||
* {@link Agent.inject} (`next-step`/no-wakeup).
|
||||
*
|
||||
* The object is complete so routing policy is explicit.
|
||||
*/
|
||||
interface SendOptions {
|
||||
/** Queue the item joins. */
|
||||
target: SendTarget
|
||||
/**
|
||||
* Whether this item makes the model run: wake a parked driver (`next-turn`)
|
||||
* or force a continuation step (`next-step` while running). A `false`
|
||||
* `next-turn` item queues without waking; a `false`
|
||||
* `next-step` item attaches durable context without forcing another step
|
||||
* (the injection preset).
|
||||
*/
|
||||
wakeup: boolean
|
||||
}
|
||||
```
|
||||
|
||||
固定预设的别名方法自带 `target` 与 `wakeup`;其已有标识的 `UserMessage` 会携带角色、内容与 provenance。投递方法不会返回其 `MessageId`,但该 id 在这条消息的各个 `agent/inbox/*` 事件中保持稳定。注入绕过两个 FIFO,从不出现在这些事件中。
|
||||
每个待处理入队项就是其 `UserMessage`;`MessageId` 是唯一标识。`Inbox.append`、`prepend`、`replace`、`remove`、`clear`、`splice` 与 `claim` 会记录规范化的持久 `agent/inbox/spliced` 变更,并拒绝重复的待处理 id。`replace(messageId, newMessage)` 与 `remove(messageId)` 通过 `MessageId` 跨两份列表定位待处理消息;替换可以改变标识,并先将旧消息作为 discarded 发布,再将新消息作为 inserted 发布。普通删除和 `clear()` 都表示取消。`claim(target)` 通过无 outcome 的纯删除 splice 移除拟进入步骤的批次——全部 `next-step` 输入,外加轮次边界上的一条 `next-turn` 消息——且不发出 discarded 通知;循环另行逐条发出 claimed 通知。UI 投影等整体队列消费方通过持久 splice 重建 `nextTurn` 与 `nextStep`,而跟踪单条消息的消费方使用精确的 `agent/inbox/inserted`、`claimed` 与 `discarded` 通知。
|
||||
|
||||
```ts type-equiv
|
||||
/** Options for {@link Agent.cancel}. */
|
||||
@@ -469,23 +573,25 @@ interface CancelOptions {
|
||||
/**
|
||||
* Preserve queued and steering inbox items instead of discarding them. The
|
||||
* active turn is still aborted, but un-started and pending work survives for a
|
||||
* later turn and no `agent/inbox/discard` fires.
|
||||
* later turn and no canceled inbox splice is logged.
|
||||
*/
|
||||
keepInbox?: boolean
|
||||
keepInbox?: boolean | undefined
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable runtime cause accepted by {@link Agent.cancel}. */
|
||||
/** Why an active agent driver was cancelled. */
|
||||
type AgentCancelCause =
|
||||
| { readonly kind: 'user' }
|
||||
| { readonly kind: 'parent' }
|
||||
| { readonly kind: 'hook'; readonly reason: string }
|
||||
| { readonly kind: 'disposed' }
|
||||
```
|
||||
|
||||
`Agent` 是覆盖公开活跃 agent 契约的接口。具体驱动器拥有 `followup`/`steer`/`inject` 别名方法,并将它们经由 `send` 的(`target` × `wakeup`)矩阵路由。
|
||||
`Agent` 是覆盖公开活跃 agent 契约的接口。它的统一 `send` 方法直接公开目标与唤醒路由;`followup`、`steer` 和 `inject` 是固定预设别名。
|
||||
|
||||
```ts type-equiv
|
||||
/** Public live-agent handle with aliases over the unified delivery primitive. */
|
||||
/** Public live-agent handle. */
|
||||
interface Agent {
|
||||
/** The single identity shared with {@link session}. */
|
||||
readonly id: SessionId
|
||||
@@ -493,89 +599,81 @@ interface Agent {
|
||||
readonly options: AgentOptions
|
||||
/** The live session this agent drives; its log is the durable source of truth. */
|
||||
readonly session: Session
|
||||
/** The agent-owned projection of durable pending work. */
|
||||
readonly inbox: Inbox
|
||||
/** The current lifecycle state, mirrored on every `agent/status` transition. */
|
||||
readonly status: AgentStatus
|
||||
/**
|
||||
* Whether a `next-step` send currently stages for prompt admission or the
|
||||
* open turn. Unlike {@link status}, this excludes admission exit and turn
|
||||
* settlement, when a waking `next-step` send becomes a queued follow-up.
|
||||
*/
|
||||
readonly acceptsNextStep: boolean
|
||||
/** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */
|
||||
readonly ctx: Context
|
||||
|
||||
/**
|
||||
* The unified delivery primitive over the (`target` × `wakeup`) matrix.
|
||||
* It routes the caller's typed content and source as follows:
|
||||
*
|
||||
* - `next-turn` queues an item that becomes the sole ordinary message of its
|
||||
* own FIFO-ordered turn; `wakeup:true` wakes a
|
||||
* parked driver, while `wakeup:false` queues without waking.
|
||||
* - `next-step` with `wakeup:true` stages steering during prompt admission
|
||||
* or an open turn; outside that window it falls back to a woken
|
||||
* `next-turn`.
|
||||
* - `next-step` with `wakeup:false` injects durable model-facing context
|
||||
* without running the model: admission or an open turn stages it for the
|
||||
* next safe log position, while an injection outside that window appends
|
||||
* immediately without opening a turn. If admission closes without a turn,
|
||||
* a context-only boundary appends immediately; context staged beside
|
||||
* steering remains pending with it.
|
||||
* The agent publishes or queues the identified frozen message as-is.
|
||||
* @param message - identified model-facing content and its producer provenance.
|
||||
* @param options - target queue and wakeup decision.
|
||||
*/
|
||||
send(message: UserMessage, options: SendOptions): void
|
||||
|
||||
/**
|
||||
* Clear queued and steering work — unless `keepInbox` — and abort the active
|
||||
* turn. An effective call first emits `agent/cancel-requested` with the
|
||||
* resolved typed cause. The first cause wins for the active turn, and
|
||||
* `whenIdle()` resolves after cancellation reaches quiescence. Idle
|
||||
* cancellation is a no-op and does not arm later work.
|
||||
* @param cause - the stable caller intent carried by the current turn signal.
|
||||
* turn or between-turn task. The first cause wins for that activity. With no
|
||||
* active activity, cancellation is a no-op and does not arm later work.
|
||||
* @param cause - the stable caller intent carried by the active operation signal.
|
||||
* @param options - cancellation options; `keepInbox` preserves pending work.
|
||||
*/
|
||||
cancel(cause: AgentCancelCause, options?: CancelOptions): void
|
||||
|
||||
/** Resolve at idle quiescence; disposal waits for driver exit rather than only the status transition. */
|
||||
/**
|
||||
* Resolve after the current whole-agent activity reaches quiescence. This
|
||||
* follows replacement work started before the observed driver retires,
|
||||
* but does not identify the settlement of any particular message.
|
||||
* @returns fulfillment after no active driver or maintenance task remains.
|
||||
*/
|
||||
whenIdle(): Promise<void>
|
||||
|
||||
/**
|
||||
* Queue an ordinary follow-up turn and wake the driver — the
|
||||
* `next-turn`/wakeup preset of {@link send}. The item becomes the sole
|
||||
* ordinary message of its own turn.
|
||||
* Run one non-turn maintenance task from the true idle phase. The task starts
|
||||
* synchronously after claiming that phase; later waking input remains in the
|
||||
* inbox until the task settles, while public status stays `idle`.
|
||||
* `whenIdle()` follows both the task and any waking work released behind it.
|
||||
* @param task - operation whose fulfillment or rejection is preserved, with a signal aborted by {@link cancel}.
|
||||
* @throws synchronously when turn-driving or another maintenance task already owns the agent.
|
||||
* @returns the task promise.
|
||||
*/
|
||||
runMaintenance<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T>
|
||||
|
||||
/**
|
||||
* Route identified input to an inbox boundary and optionally wake the driver.
|
||||
* Waking input submitted after active cancellation is queued for the next turn.
|
||||
* @param message - identified content and its producer provenance.
|
||||
* @param target - the preferred next-turn or next-step inbox boundary.
|
||||
* @param wakeup - whether delivery may wake the driver.
|
||||
*/
|
||||
send(message: UserMessage, target: InboxTarget, wakeup: boolean): void
|
||||
|
||||
/**
|
||||
* Queue an ordinary follow-up turn and wake the driver. The item becomes the
|
||||
* sole ordinary message of its own turn.
|
||||
* @param message - identified prompt content and its producer provenance.
|
||||
*/
|
||||
followup(message: UserMessage): void
|
||||
|
||||
/**
|
||||
* Submit steering during prompt admission or an open turn — the
|
||||
* `next-step`/wakeup preset of {@link send}. It stages for the next steering
|
||||
* checkpoint before a request or stop decision. If the activity fails before
|
||||
* that boundary, the remainder stays staged without waking the agent; retry
|
||||
* or a later prompt takes it. Outside that window steering falls back to a
|
||||
* woken follow-up turn, while cancellation or disposal may discard pending
|
||||
* steering.
|
||||
* Submit steering for the nearest step. An idle driver starts a turn;
|
||||
* a running driver consumes it at its next step boundary.
|
||||
* A rejected step leaves steering parked in the inbox until the next
|
||||
* wake; cancellation or disposal may discard pending steering.
|
||||
* @param message - identified steering content and its producer provenance.
|
||||
*/
|
||||
steer(message: UserMessage): void
|
||||
|
||||
/**
|
||||
* Append model-facing context without running the model — the
|
||||
* `next-step`/no-wakeup preset of {@link send}. Admission or an open turn
|
||||
* stages it at the next safe log position; outside that window it appends
|
||||
* immediately without opening a turn. If admission closes without a turn,
|
||||
* a context-only boundary appends immediately; context staged beside
|
||||
* steering remains pending with it.
|
||||
* Queue model-facing context for the next pre-step without waking the
|
||||
* driver. A running driver claims it at the nearest later step boundary;
|
||||
* idle drivers leave it pending until follow-up or steering
|
||||
* wakes them. It may miss a request whose pre-step already claimed its
|
||||
* batch. Cancellation or disposal may discard pending context.
|
||||
* @param message - identified injected context and its producer provenance.
|
||||
*/
|
||||
inject(message: UserMessage): void
|
||||
}
|
||||
```
|
||||
|
||||
`AgentStatus` 为 `'idle' | 'running'`,`SessionId` 是品牌类型。dispose(资源释放)会把 agent 从注册表移除并发出 `agent/disposed`;它不是一个终态 status 值。`running` 描述整个驱动器的排空区间,可能跨越连续的排队轮次;它不能证明某个轮次仍然打开。对于需要在把输入作为 steering 加入当前提示词准入/轮次,还是提交为一个新的待准入提示词之间做选择的调用方,`acceptsNextStep` 才是更窄且准确的路由判断条件。`AgentOptions` 可合并扩展:core 声明 `provider?`、`model?` 与 `maxTokens?`(在 `agent/request` 后,分发要求 provider 与 model 都存在)。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时由提供方默认值控制。Persona 归 `dsh-system-prompt` 所有:agent 作用域的 `deployment:persona` 可以遮蔽全局默认值。
|
||||
`AgentStatus` 为 `'idle' | 'running'`,`SessionId` 是品牌类型。dispose(资源释放)会把 agent 从注册表移除并发出 `agent/disposed`;它不是一个终态 status 值。`running` 描述整个驱动器的排空区间,可能跨越连续的排队轮次;它不能证明某个轮次仍然打开。`followup()` 不返回 handle:其 `MessageId` 标识持久 inbox 的插入、领取与丢弃事实,而不标识之后的助手输出或轮次结束。`whenIdle()` 观察整个 agent,因此只有显式拥有从回执到 idle 这一完整区间的调用方才能将其称为一次运行([决策](../../.agents/notes/implemented/architecture/2026-07-30-followup-enqueue-and-owned-runs.md))。`AgentOptions` 可合并扩展:core 声明 `provider?`、`model?` 与 `maxTokens?`(在 `agent/request` 后,分发要求 provider 与 model 都存在)。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。Persona 归 `dsh-system-prompt` 所有:agent 作用域的 `deployment:persona` 可以遮蔽全局默认值。
|
||||
|
||||
cause 是由 TypeScript 强制约束的同进程输入。活跃的 `TurnCancellation` 持有者会把其判别字段复制到仅运行时的 `AbortSignal.reason`,并在发布 `turn/end` 前退役;冻结后的 `AbortSignal.reason` 仍可读取。只有 loop 会在结算时从自己机器私有的 signal 上读回 cause(`user`、`parent` 或仅用于生命周期的 `disposed`)——不存在公开的读取器,signal 也不授予协作监听器任何分类权限。持久 `turn/end` 保留粗粒度 `{ kind: 'aborted' }` 结果;若需记录请求 provenance,应使用单独的持久事件,而不是让终态结果承担额外含义。
|
||||
cause 是由 TypeScript 强制约束的同进程输入。活跃的取消持有者会将它复制到仅运行时的 `AbortSignal.reason`;signal 不授予协作监听器任何分类权限。持久 `turn/end` 保留粗粒度 `{ kind: 'aborted' }` 结果;若需记录请求 provenance,应使用单独的持久事件,而不是让终态结果承担额外含义。
|
||||
|
||||
[事件分类](../architecture.md#event)拥有 `agent/*` 生命周期、检查点与 waterfall(瀑布式事件)契约。轮次和步骤边界是持久会话事件,而不是 agent emit。
|
||||
|
||||
@@ -585,22 +683,31 @@ cause 是由 TypeScript 强制约束的同进程输入。活跃的 `TurnCancella
|
||||
|
||||
## 拦截决策
|
||||
|
||||
提示词决策与工具后决策使用与持久 user-role 输入相同、带标识的 `UserMessage` 形状。每个 `additionalContexts` 条目都会成为一条独立的 `user/message`,保留各自的标识与 provenance。钩子桥接层把其原生决策字段映射到这些类型化结果上。
|
||||
pre-step 决策使用与持久 user-role 输入相同、带标识的 `UserMessage` 形状。进入步骤的批次具有权威性,并保留每条消息的标识与 provenance。钩子桥接层把其原生决策字段映射到这一类型化结果上。
|
||||
|
||||
源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
`agent/prompt-submit` 在轮次打开前返回 `PromptDecision`。allow 可以改写已领取的提示词或附加 `additionalContexts`;block 拒绝准入且不产生任何轮次事件:
|
||||
`agent/pre-step` 接收独占的已领取批次,以及拟进入步骤的坐标与取消 signal。首次提案在已打开的轮次内、任何步骤开始前运行;工具 continuation 可以在步骤之间提交空的已领取批次:
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Prompt interception result. `allow.content` replaces the prompt, while
|
||||
* `additionalContexts` appends model-facing context before the turn starts.
|
||||
* An `allow` returned by a listener is authoritative: a listener wrapping
|
||||
* `next()` preserves both fields unless it intentionally replaces them.
|
||||
*/
|
||||
type PromptDecision =
|
||||
| { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: UserMessage[] }
|
||||
| { kind: 'block'; reason: string }
|
||||
/** Coordinates and cancellation for a proposed step. */
|
||||
interface PreStepContext {
|
||||
/** Turn that will own the step. */
|
||||
readonly turn: number
|
||||
/** Step proposed by the loop. */
|
||||
readonly step: number
|
||||
/** Current turn cancellation signal. */
|
||||
readonly signal: AbortSignal
|
||||
}
|
||||
```
|
||||
|
||||
它返回 `PreStepDecision`。reject 不会打开步骤。enter 提供在 `step/start` 后追加的完整消息批次;最终决策省略的已领取消息保持已删除,而领取后插入的输入仍留待后续处理:
|
||||
|
||||
```ts type-equiv
|
||||
/** Whether and with which messages the loop enters a proposed step. */
|
||||
type PreStepDecision =
|
||||
| { kind: 'reject' }
|
||||
| { kind: 'enter'; messages: UserMessage[] }
|
||||
```
|
||||
|
||||
`agent/request-error` 在失败的模型步骤关闭之后、其轮次关闭之前运行。listener 可以在失败轮次的 signal 仍然存活时修复持久状态或 await 策略工作。处理该错误的 listener 返回 `{ kind: 'retry' }` 且不调用 `next()`;默认的 `undefined` 会让失败保持终态。
|
||||
@@ -610,12 +717,7 @@ type PromptDecision =
|
||||
type RequestErrorAction = { kind: 'retry' } | undefined
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Model-request failure with an optional machine-routable provider code. */
|
||||
type RequestError = Error & { code?: string }
|
||||
```
|
||||
|
||||
`agent/step` 是请求推导前唯一的串行边界。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。
|
||||
`agent/pre-step` 是请求推导前唯一的串行边界。`agent/turn-stopping` 在轮次没有工具或 steering(中途引导)后续时运行,先于最后一次 steering 排空。
|
||||
|
||||
`agent/session-start` 携带 `SessionStartSource`(会话生命周期为何开始;桥接层据此匹配其 SessionStart):
|
||||
|
||||
|
||||
6
docs/core-data-structures/credentials.i18n.yaml
Normal file
6
docs/core-data-structures/credentials.i18n.yaml
Normal file
@@ -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 docs/core-data-structures/credentials.md
|
||||
credentials.md: 3f6fcd127d01e2c49e17c70c002bebe9f363e951
|
||||
credentials.zh.md: b5d2d9e164a85ce090790635c438b768cae4c9ca
|
||||
50
docs/core-data-structures/credentials.md
Normal file
50
docs/core-data-structures/credentials.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# User Credentials
|
||||
|
||||
English | [中文](credentials.zh.md)
|
||||
|
||||
The credential seam of [dsh-credentials](../../packages/credentials/credentials) keeps secrets out of configuration: settings sections and `cordis.yml` entries carry *references* (environment-variable names), providers such as [dsh-credentials-local](../../packages/credentials/credentials-local) own the values, and consumers resolve a reference once per operation — the LLM adapters resolve once per model request, so a rotated credential reaches the very next request without any restart. One seam-wide rule binds every provider: an empty stored value is absent everywhere.
|
||||
|
||||
Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts)
|
||||
|
||||
## Identity
|
||||
|
||||
A reference names one credential as a POSIX-style environment-variable name. The brand keeps references from mixing with other cross-boundary strings; construction validates the shell-identifier shape.
|
||||
|
||||
```ts type-equiv
|
||||
/** Nominal reference to one credential: a POSIX-style environment-variable name. */
|
||||
type CredentialRef = Branded<'CredentialRef'>
|
||||
```
|
||||
|
||||
## Resolution
|
||||
|
||||
`resolve(ref)` returns the value with the provider-defined source layer that supplied it, or `undefined` while unconfigured. Consumers re-resolve at each operation and never cache across operations — that per-operation read is the hot-update mechanism.
|
||||
|
||||
```ts type-equiv
|
||||
/** One resolved credential value and the source layer that supplied it. */
|
||||
interface ResolvedCredential {
|
||||
/** The non-empty secret value. */
|
||||
value: string
|
||||
/** Provider-defined source layer id (the local provider uses `env` and `file`). */
|
||||
source: string
|
||||
}
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
`describe(ref)` answers configuration surfaces without ever exposing a value: whether the reference resolves, from which layer, and whether `set` would currently succeed. The local provider reports a reference supplied by the live process environment as `writable: false` — a write would appear to succeed while resolution kept returning the shadowing value, so the seam rejects it and the UI can render the reference read-only up front.
|
||||
|
||||
```ts type-equiv
|
||||
/** Source and writability facts for one reference, safe for configuration UIs — never the value. */
|
||||
interface CredentialInfo {
|
||||
/** Whether {@link Credentials.resolve} would currently return a value. */
|
||||
configured: boolean
|
||||
/** Source layer currently supplying the value; absent while unconfigured. */
|
||||
source?: string
|
||||
/** Whether {@link Credentials.set} would currently succeed for this reference. */
|
||||
writable: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Change commits
|
||||
|
||||
`credentials/updated (ref)` fires after a committed change to a provider-managed source — a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Consumers do not need the event (they re-resolve per operation); it exists for configuration surfaces refreshing a "configured" badge.
|
||||
50
docs/core-data-structures/credentials.zh.md
Normal file
50
docs/core-data-structures/credentials.zh.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 用户凭据
|
||||
|
||||
[English](credentials.md) | 中文
|
||||
|
||||
[dsh-credentials](../../packages/credentials/credentials) 的凭据 seam 把机密挡在配置之外:settings 分节与 `cordis.yml` 条目携带的是*引用*(环境变量名),值归 [dsh-credentials-local](../../packages/credentials/credentials-local) 这类 provider 所有,消费方每个操作解析一次引用——LLM 适配器每次模型请求解析一次,因此轮换后的凭据无需任何重启即可作用于紧随其后的下一次请求。一条 seam 级规则约束每个 provider:空的存储值在任何地方都视为不存在。
|
||||
|
||||
Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credentials/credentials/src/index.ts)
|
||||
|
||||
## 标识
|
||||
|
||||
引用以 POSIX 风格环境变量名命名一条凭据。brand 使引用不与其他跨边界字符串混用;构造时校验 shell 标识符形态。
|
||||
|
||||
```ts type-equiv
|
||||
/** Nominal reference to one credential: a POSIX-style environment-variable name. */
|
||||
type CredentialRef = Branded<'CredentialRef'>
|
||||
```
|
||||
|
||||
## 解析
|
||||
|
||||
`resolve(ref)` 返回值,连同供出该值、由 provider 定义的来源层;未配置期间返回 `undefined`。消费方在每个操作中重新解析,绝不跨操作缓存——这次按操作进行的读取正是热更新机制。
|
||||
|
||||
```ts type-equiv
|
||||
/** One resolved credential value and the source layer that supplied it. */
|
||||
interface ResolvedCredential {
|
||||
/** The non-empty secret value. */
|
||||
value: string
|
||||
/** Provider-defined source layer id (the local provider uses `env` and `file`). */
|
||||
source: string
|
||||
}
|
||||
```
|
||||
|
||||
## 描述
|
||||
|
||||
`describe(ref)` 在绝不暴露值的前提下回应配置界面:引用当前是否可解析、来自哪一层、`set` 当前能否成功。本地 provider 把由活跃进程环境供值的引用报告为 `writable: false`——那样的写入会表面成功而解析持续返回遮蔽值,因此 seam 直接拒绝,界面也得以提前把该引用渲染为只读。
|
||||
|
||||
```ts type-equiv
|
||||
/** Source and writability facts for one reference, safe for configuration UIs — never the value. */
|
||||
interface CredentialInfo {
|
||||
/** Whether {@link Credentials.resolve} would currently return a value. */
|
||||
configured: boolean
|
||||
/** Source layer currently supplying the value; absent while unconfigured. */
|
||||
source?: string
|
||||
/** Whether {@link Credentials.set} would currently succeed for this reference. */
|
||||
writable: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## 变更提交
|
||||
|
||||
`credentials/updated (ref)` 在 provider 管理的来源发生已提交变更后触发——`set`、`unset` 或在存储中观察到的外部编辑。进程环境自身的变化不可观测,永不发出事件。消费方不需要该事件(它们按操作重新解析);它服务于配置界面刷新「已配置」徽标。
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/filesystem.md
|
||||
filesystem.md: 110c1fd428b15c5094f9dcc94050cad61c324373
|
||||
filesystem.zh.md: aca450364c05c6f756c36fccc11be7246767f3a4
|
||||
filesystem.zh.md: 010ec22a5d4a29555425c3deede08b317cba7004
|
||||
|
||||
@@ -69,7 +69,7 @@ interface FsInfo {
|
||||
}
|
||||
```
|
||||
|
||||
`lstat` 是路径层级、不跟随链接的元数据原语。它接收路径而不是 `FsTarget`,因为 `resolve` 会有意跟随 symlink 以产生稳定标识;需要检查信任边界的消费方可以先调用 `lstat`,在解析前拒绝 `symlink`。
|
||||
`lstat` 是路径级、不跟随链接的元数据原语。它接收路径而不是 `FsTarget`,因为 `resolve` 会有意跟随 symlink 以产生稳定标识;需要检查信任边界的消费方可以先调用 `lstat`,在解析前拒绝 `symlink`。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -111,7 +111,7 @@ interface FsDirEntry {
|
||||
|
||||
## 写入与编辑守卫(提供方 seam)
|
||||
|
||||
`writeText` 和 `editText` 的版本守卫都是可选的:省略它执行无条件(裸提供方)变更,提供它则启用守卫。`writeText` 的守卫是 `FsWriteIntent`:`createIfAbsent` 在目标缺失时创建,目标已存在时以 `FS_NOT_OBSERVED` 拒绝;`replaceIfVersion` 仅在目标存在且版本匹配时替换,否则报 `FS_STALE_VERSION`。省略 `expected` 则无条件创建或覆盖。联合类型本身只包含两种有守卫的意图;「无守卫」通过省略表达,因此 write 和 edit 共享同一个对称的 `expected?` 形状。
|
||||
`writeText` 和 `editText` 的版本守卫都是可选的:省略守卫时执行无条件的裸提供方变更,提供守卫时则执行相应的条件检查。`writeText` 的守卫是 `FsWriteIntent`:`createIfAbsent` 在目标缺失时创建,目标已存在时以 `FS_NOT_OBSERVED` 拒绝;`replaceIfVersion` 仅在目标存在且版本匹配时替换,否则报 `FS_STALE_VERSION`。省略 `expected` 则无条件创建或覆盖。联合类型本身只包含两种有守卫的意图;「无守卫」通过省略表达,因此 write 和 edit 共享同一个对称的 `expected?` 形状。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -179,11 +179,11 @@ interface FsEditOutcome {
|
||||
|
||||
`dsh-fs` 拥有三个事件,由工具分发、策略插件监听,使发射方(`dsh-tool-fs`)与监听方(`dsh-fs-policy`)共享词汇,而发射方无需依赖策略插件。它们只携带 `dsh-fs` 词汇加一个不透明的 `object` actor,不含面向模型的概念,也不含 agent/会话所有者结构。
|
||||
|
||||
`fs/write-intent` 与 `fs/edit-intent` 是**单槽决策 waterfall**:工具分发时附带一个默认 thunk(返回 `undefined`,即裸提供方),监听方完全决策而不调用 `next()`。该槽按注册顺序先到先得——由策略插件占据是部署约定,而非强制不变式。`fs/observed` 是一个即发即弃的记录事件,通过普通 `ctx.emit` 分发;其监听方必须是同步的、仅产生副作用,因为工具不守卫该 emit——抛异常的监听方会在一次已成功的变更上表现为工具的 `isError` 结果。生成的目录在 [events.md](../cordis-catalog/events.md) 中展示确切签名。
|
||||
`fs/write-intent` 与 `fs/edit-intent` 是**单槽决策 waterfall**:工具分发时附带一个默认 thunk(返回 `undefined`,即裸提供方),监听方完全决策而不调用 `next()`。该槽按注册顺序先到先得——由策略插件占据是部署约定,而非强制不变式。`fs/observed` 是一个即发即弃的记录事件,通过普通 `ctx.emit` 分发;其监听方必须是同步的、仅产生副作用,因为工具不会捕获该 emit 抛出的异常——抛出异常的监听方会导致工具为一次已经成功的变更返回 `isError` 结果。生成的目录在 [events.md](../cordis-catalog/events.md) 中展示确切签名。
|
||||
|
||||
## 执行上下文(策略插件)
|
||||
|
||||
策略插件只需要足够的执行上下文,通过收窄 `fs/*` 事件携带的不透明 `object` actor 来推导观测状态的所有者。`ToolExecution` 满足此形状,因此 `dsh-tool-fs` 将其执行对象作为 actor 直接传递,而无需让 `dsh-fs-policy` 导入工具、agent 或会话包(package)。
|
||||
策略插件只需要足够的执行上下文,通过收窄 `fs/*` 事件携带的不透明 `object` actor 来推导观测状态的所有者。`ToolExecution` 满足此形状,因此 `dsh-tool-fs` 将其执行对象作为 actor 直接传递,而无需让 `dsh-fs-policy` 导入工具、agent 或会话包。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
|
||||
@@ -1,6 +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
|
||||
goal.md: 704a93320cc38d1b9400edc2d9ad2342bc11dccd
|
||||
goal.zh.md: b2e083843a70823bf6a6b43e046b1f38f4e11e22
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/goal.md
|
||||
goal.md: fc6a7e63e58fc7cd4bc524be1e66515593680d95
|
||||
goal.zh.md: c584cb375bd55c13c158e7ee22d7721040dffa05
|
||||
|
||||
@@ -71,10 +71,10 @@ interface GoalView extends GoalSnapshot {
|
||||
|
||||
## Durable changes
|
||||
|
||||
Every mutation is a round-zero goal-sourced `user/message` whose metadata is either a complete snapshot or a clear tombstone. The version, metadata, goal source, and verbatim rendered content form one replay invariant.
|
||||
Every mutation is a durable `goal/change` session event whose payload is either a complete post-mutation snapshot or a clear tombstone. The strict fold and persisted projection derive lifecycle state only from these events; inbox mutations do not affect goal state.
|
||||
|
||||
```ts type-equiv
|
||||
/** Full-snapshot goal mutation retained in a model-visible context event. */
|
||||
/** Full-snapshot goal mutation committed by a durable `goal/change` event. */
|
||||
interface GoalSnapshotChangeMeta {
|
||||
readonly kind: 'goal/change'
|
||||
readonly version: 1
|
||||
@@ -97,18 +97,16 @@ interface GoalClearChangeMeta {
|
||||
}
|
||||
```
|
||||
|
||||
Goal state changes use round `0`. A continuation consumer attributes each admitted user-message turn with a positive, sequential round number and the current revision; replay rejects gaps, stale revisions, stopped phases, and cap overflow.
|
||||
A continuation consumer attributes each admitted user-message turn with a positive, sequential round number and the current revision; only these admitted `user/message` events advance `roundsStarted`. Replay rejects non-positive rounds, gaps, stale revisions, stopped phases, and cap overflow.
|
||||
|
||||
```ts type-equiv
|
||||
/** Message attribution for durable goal state and continuation rounds. */
|
||||
/** Message attribution for admitted continuation rounds. */
|
||||
interface GoalMessageSource {
|
||||
readonly kind: 'goal'
|
||||
readonly goalId: GoalId
|
||||
readonly revision: number
|
||||
/** Zero for state changes; positive for admitted continuation rounds. */
|
||||
/** Positive admitted continuation round. */
|
||||
readonly round: number
|
||||
/** Complete durable mutation carried only by round-zero state-change messages. */
|
||||
readonly change?: GoalChangeMeta
|
||||
}
|
||||
```
|
||||
|
||||
@@ -133,7 +131,7 @@ interface EditGoalRequest {
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Live notification after one goal mutation has been accepted for logging. */
|
||||
/** Live notification after one durable goal mutation commits. */
|
||||
interface GoalChanged {
|
||||
readonly operation: GoalOperation
|
||||
readonly ref: GoalRef
|
||||
@@ -144,4 +142,4 @@ interface GoalChanged {
|
||||
|
||||
## Service behavior
|
||||
|
||||
[`GoalService`](../../packages/goal/goal/src/index.ts) resolves creation defaults, folds strict replay, enforces exact-live-agent identity and compare-and-set mutations, overlays deferred injections, and emits contained `goal/changed` notifications. The package [README](../../packages/goal/goal/README.md) owns the callable and model-visible contract.
|
||||
[`GoalService`](../../packages/goal/goal/src/index.ts) resolves creation defaults, folds strict replay from durable `goal/change` events, enforces exact-live-agent identity and compare-and-set mutations, and emits contained `goal/changed` notifications. The package [README](../../packages/goal/goal/README.md) owns the callable and model-visible contract.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](goal.md) | 中文
|
||||
|
||||
事件溯源目标领域及其策略消费方共享的类型。[目标领域 Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md)负责记录持久化与激活决策;本页记录 [`packages/goal/goal/src/types.ts`](../../packages/goal/goal/src/types.ts) 中的字面形态。
|
||||
事件溯源目标领域及其策略消费方共享的类型。[目标领域 Agent Note](../../.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md)负责记录持久化与激活决策;本页记录 [`packages/goal/goal/src/types.ts`](../../packages/goal/goal/src/types.ts) 中的字面形态。
|
||||
|
||||
## 标识与生命周期
|
||||
|
||||
@@ -71,10 +71,10 @@ interface GoalView extends GoalSnapshot {
|
||||
|
||||
## 持久变更
|
||||
|
||||
每次变更都是 Round 编号为 0、来源为目标的 `user/message`,其元数据要么是完整快照,要么是清除墓碑。版本、元数据、目标来源和逐字渲染内容共同构成一项回放不变量。
|
||||
每次变更都是持久的 `goal/change` 会话事件,其载荷要么是变更后的完整快照,要么是清除墓碑。严格折叠与持久投影只从这些事件派生生命周期状态;inbox 变更不会影响 goal 状态。
|
||||
|
||||
```ts type-equiv
|
||||
/** Full-snapshot goal mutation retained in a model-visible context event. */
|
||||
/** Full-snapshot goal mutation committed by a durable `goal/change` event. */
|
||||
interface GoalSnapshotChangeMeta {
|
||||
readonly kind: 'goal/change'
|
||||
readonly version: 1
|
||||
@@ -97,24 +97,22 @@ interface GoalClearChangeMeta {
|
||||
}
|
||||
```
|
||||
|
||||
目标状态变更使用 Round `0`。续跑消费方会为每个获准的用户消息轮次标注正数且连续的 Round 编号和当前修订号;回放会拒绝编号缺口、陈旧修订号、已停止阶段和超出上限。
|
||||
续跑消费方会为每个获准的用户消息轮次标注正数且连续的 Round 编号和当前修订号;只有这些获准的 `user/message` 事件会推进 `roundsStarted`。回放会拒绝非正数 Round、编号缺口、陈旧修订号、已停止阶段和超出上限。
|
||||
|
||||
```ts type-equiv
|
||||
/** Message attribution for durable goal state and continuation rounds. */
|
||||
/** Message attribution for admitted continuation rounds. */
|
||||
interface GoalMessageSource {
|
||||
readonly kind: 'goal'
|
||||
readonly goalId: GoalId
|
||||
readonly revision: number
|
||||
/** Zero for state changes; positive for admitted continuation rounds. */
|
||||
/** Positive admitted continuation round. */
|
||||
readonly round: number
|
||||
/** Complete durable mutation carried only by round-zero state-change messages. */
|
||||
readonly change?: GoalChangeMeta
|
||||
}
|
||||
```
|
||||
|
||||
## 请求与通知
|
||||
|
||||
创建操作会区分调用方省略的值与部署选择,`create()` 会在内部解析后者。编辑是局部替换,其运行时校验器要求至少提供一个字段。每条变更通知都会携带获准的操作和确切修订号;清除操作不带 `goal`。
|
||||
创建操作会区分调用方省略字段与采用部署配置值这两种情况,`create()` 会在内部解析后者。编辑是局部替换,其运行时校验器要求至少提供一个字段。每条变更通知都会携带获准的操作和确切修订号;清除操作不带 `goal`。
|
||||
|
||||
```ts type-equiv
|
||||
/** Input whose omitted round cap is resolved by the service configuration. */
|
||||
@@ -133,7 +131,7 @@ interface EditGoalRequest {
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Live notification after one goal mutation has been accepted for logging. */
|
||||
/** Live notification after one durable goal mutation commits. */
|
||||
interface GoalChanged {
|
||||
readonly operation: GoalOperation
|
||||
readonly ref: GoalRef
|
||||
@@ -144,4 +142,4 @@ interface GoalChanged {
|
||||
|
||||
## 服务行为
|
||||
|
||||
[`GoalService`](../../packages/goal/goal/src/index.ts) 解析创建默认值、执行严格回放折叠、校验确切的活跃 agent 身份、以比较并设置方式执行变更、叠加延迟注入,并发出 `goal/changed` 通知;监听器故障会被隔离。包 [README](../../packages/goal/goal/README.md) 负责记录可调用契约和面向模型的契约。
|
||||
[`GoalService`](../../packages/goal/goal/src/index.ts) 解析创建默认值、从持久 `goal/change` 事件执行严格回放折叠、校验确切的活跃 agent 身份、以比较并设置方式执行变更,并发出 `goal/changed` 通知;监听器故障会被隔离。包 [README](../../packages/goal/goal/README.md) 负责记录可调用契约和面向模型的契约。
|
||||
|
||||
@@ -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 docs/core-data-structures/llm-streaming.md
|
||||
llm-streaming.md: 6811611768a0ec577360a8ff82792899cf3b8fec
|
||||
llm-streaming.zh.md: 35374af6a20086f15384840689dba5aa24351750
|
||||
llm-streaming.md: 5c90b3ce4ac65a99997f6ba7ad5deac494b7f772
|
||||
llm-streaming.zh.md: 7fd0043234cb40d6b21cec6ff101993164785a6e
|
||||
|
||||
@@ -15,8 +15,9 @@ A streaming response interleaves several typed blocks (text, reasoning, multiple
|
||||
* Raw streaming protocol emitted by adapters.
|
||||
* Block indexes correlate interleaved deltas, and `block-end` carries the
|
||||
* assembled block. Adapters emit usage before the terminal finish and nothing
|
||||
* afterward; tool arguments remain raw JSON strings. Failures either throw or
|
||||
* end with `error`/`aborted`, and consumers must handle both paths.
|
||||
* afterward; tool arguments remain raw JSON strings. An adapter implementation
|
||||
* may throw, but `LlmService.stream()` normalizes that failure to a terminal
|
||||
* `error` or `aborted` finish before exposing it to consumers.
|
||||
*/
|
||||
type StreamChunk =
|
||||
| { type: 'block-start'; index: number; blockType: ContentBlockType }
|
||||
@@ -64,10 +65,10 @@ Every adapter MUST obey these, and every consumer may rely on them:
|
||||
- **Provider stalls are bounded at the transport.** Both shipping remote adapters expose positive finite `streamIdleTimeoutMs` with a five-minute default. The watchdog arms only while iterator `next()` is outstanding, uses one stable signal for the whole request, maps its own expiry to `TIMEOUT`, and keeps an earlier caller abort as `ABORTED`.
|
||||
- **Context overflow has one canonical code.** Both DeepSeek adapters classify explicit provider detail through `isContextWindowExceededError()` and surface `CONTEXT_WINDOW_EXCEEDED`, whether the failure arrives as a thrown HTTP `LlmError` or an in-band finish error. Consumers route on the code, never provider text.
|
||||
- **An empty completion is a retryable error, not a silent success.** Both adapters map a terminal `stop` finish that carried no content blocks to `finish {kind:'error'}` with the canonical `EMPTY_RESPONSE` code, and `dsh-llm-retry` retries it by default; see [empty model responses are retryable](../../.agents/notes/implemented/bug-fix/2026-07-24-empty-model-response-is-retryable.md).
|
||||
- **Every provider HTTP request carries the app-attribution header.** Adapters send `attributionHeaders()` (below) - the `User-Agent` baseline - and prove it with a wire-level test (mock server asserting the received header, or the library's header hook for a library-backed adapter).
|
||||
- **Every provider HTTP request carries the app-attribution header.** Adapters send `attributionHeaders()` (below), the `User-Agent` baseline.
|
||||
- **Replay state is adapter-owned.** A successful `finish` may carry lossless-JSON state needed to reconstruct a native provider response. The loop stores it with the assembled assistant message. On a later request, `LlmService` passes the state only when the historical provider and target provider are currently registered to the exact same adapter instance. That adapter validates the state and owns any cross-model or cross-provider conversion; other adapters receive the provider-neutral content and provenance without the private state.
|
||||
|
||||
This contract is pinned down by two deliberately independent implementations: `dsh-llm-deepseek` (direct fetch, SSE framing via `eventsource-parser`) and `dsh-llm-pi-ai` (a generic multi-provider adapter through `@earendil-works/pi-ai`). The library-backed adapter exercises the finish-chunk error path, while transport-boundary tests prove each idle watchdog stops its actual request.
|
||||
Two independent implementations obey this contract: `dsh-llm-deepseek` uses direct fetch with SSE framing through `eventsource-parser`, while `dsh-llm-pi-ai` provides a generic multi-provider adapter through `@earendil-works/pi-ai`. Both carry cancellation and the idle watchdog to the provider request.
|
||||
|
||||
## `ResolvedRetryPolicy`
|
||||
|
||||
@@ -141,8 +142,9 @@ declare class BlockAssembler {
|
||||
push(chunk: StreamChunk): void;
|
||||
/**
|
||||
* Assemble all blocks seen so far, in stream order.
|
||||
* @returns one block per seen index; an open block assembles from its
|
||||
* accumulated deltas (an unknown block type never closed by `block-end` throws).
|
||||
* @returns one block per seen index, except that max-token truncation drops
|
||||
* tool calls that cannot be executed safely; an open block assembles from
|
||||
* its accumulated deltas (an unknown block type never closed by `block-end` throws).
|
||||
*/
|
||||
blocks(): ContentBlock[];
|
||||
/** Usage from the `usage` chunk; undefined until one arrives. */
|
||||
@@ -162,13 +164,19 @@ declare class BlockAssembler {
|
||||
|
||||
## The seam
|
||||
|
||||
`LlmAdapter` is the provider seam: subclass, implement `stream()`, and register one adapter instance with `ctx.llm.registerAdapter(providers, adapter)`. `GenerateOptions.provider` selects the registered adapter; `GenerateOptions.model` is passed to that adapter and need not be registered at lifecycle start. Duplicate provider routes fail atomically. Optional `providerRetryPolicy()` is captured per route with normal defaults, while `providerInfo()` and asynchronous `listModels()` feed `LlmService.listProviders()` / `listModels()` with detached selector metadata. That catalog is advisory rather than a request whitelist: the adapter remains authoritative and may accept unlisted model ids. One asynchronous `resolveModel()` query returns exact model identity plus optional correctness-sensitive context capacity and ordered model-owned reasoning ids with an optional deployment default; absent fields mean unavailable metadata or capability, not invalid catalog membership. The resolver receives optional cancellation and must settle promptly after abort. `LlmService.resolveModelInfo()` validates and detaches the aggregate. The service validates and materializes reasoning through `resolveCallConfig()` at the final adapter boundary, so direct calls cannot bypass unsupported-effort rejection; direct dispatch captures one registration before awaiting that resolution. The agent loop instead uses `prepareCall()` to keep the same registration across model resolution, durable header logging, and dispatch. Adapter lookup happens at the terminal continuation of the `llm/stream` waterfall, so a listener may short-circuit the call or route a mutable one-shot request before lookup. The `block-start` / `block-end` `index` correlation and the assembler together mean an adapter only has to emit well-formed chunks — block reassembly is not each adapter's problem. The consumer surface (`ctx.llm.stream()`) and the `llm/stream` waterfall are described in [architecture.md § Content blocks and streaming](../architecture.md#content-blocks-and-streaming-dsh-llm).
|
||||
`LlmAdapter` is the provider seam: subclass, implement `stream()`, and register one adapter instance with `ctx.llm.registerAdapter(providers, adapter)`. `GenerateOptions.provider` selects the registered adapter; `GenerateOptions.model` is passed to that adapter and need not be registered at lifecycle start. Duplicate provider routes fail atomically. Optional `providerRetryPolicy()` is captured per route with normal defaults, while `providerInfo()` and asynchronous `listModels()` feed `LlmService.listProviders()` / `listModels()` with detached selector metadata. That catalog is advisory rather than a request whitelist: the adapter remains authoritative and may accept unlisted model ids. One asynchronous `resolveModel()` query returns exact model identity plus optional correctness-sensitive context capacity, an adapter-configured `defaultMaxTokens`, and ordered model-owned reasoning ids with an optional deployment default; absent fields mean unavailable metadata or provider-owned behavior, not invalid catalog membership. The resolver receives optional cancellation and must settle promptly after abort. `LlmService.resolveModelInfo()` validates and detaches the aggregate. At the final adapter boundary, `resolveCallConfig()` materializes the output default only when `maxTokens` is absent and validates and materializes reasoning, so direct calls cannot bypass either configured behavior; direct dispatch captures one registration before awaiting that resolution. The agent loop instead uses `prepareCall()` to keep the same registration across model resolution, durable header logging, and dispatch, retain detached context metadata from that exact lookup, and report which config fields the adapter defaulted. Adapter lookup happens at the terminal continuation of the `llm/stream` waterfall, so a listener may short-circuit the call or route a mutable one-shot request before lookup. AgentLoop observes a request attempt once the outer waterfall returns a stream handle; that limited boundary does not prove a lazy terminal adapter was constructed or began provider I/O. The `block-start` / `block-end` `index` correlation and the assembler together mean an adapter only has to emit well-formed chunks — block reassembly is not each adapter's problem. The consumer surface (`ctx.llm.stream()`) and the `llm/stream` waterfall are described in [architecture.md § Content blocks and streaming](../architecture.md#content-blocks-and-streaming-dsh-llm).
|
||||
|
||||
```ts type-equiv
|
||||
/** One model call whose config and adapter registration were resolved together. */
|
||||
interface PreparedLlmCall {
|
||||
/** Detached, deep-frozen config with any adapter-owned default materialized. */
|
||||
readonly config: LlmCallConfig
|
||||
/** Immutable retry policy captured with the adapter registration. */
|
||||
readonly retryPolicy: ResolvedRetryPolicy
|
||||
/** Detached context metadata resolved with the registration-bound call. */
|
||||
readonly context?: LlmModelContext
|
||||
/** Config fields materialized by the captured adapter rather than proposed by the caller. */
|
||||
readonly adapterDefaults: LlmCallConfigAdapterDefaults
|
||||
/**
|
||||
* Dispatch this call once through the registration captured during
|
||||
* preparation. The request's call-config fields must match {@link config};
|
||||
@@ -215,7 +223,7 @@ declare abstract class LlmAdapter {
|
||||
* @param model - exact model id passed to {@link GenerateOptions.model}.
|
||||
* @param _signal - cancellation for this exact-model lookup; asynchronous
|
||||
* implementations must settle promptly after it aborts.
|
||||
* @returns provider/model identity plus any context and reasoning metadata.
|
||||
* @returns provider/model identity plus any context, call-default, and reasoning metadata.
|
||||
*/
|
||||
resolveModel(
|
||||
provider: string,
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
* Raw streaming protocol emitted by adapters.
|
||||
* Block indexes correlate interleaved deltas, and `block-end` carries the
|
||||
* assembled block. Adapters emit usage before the terminal finish and nothing
|
||||
* afterward; tool arguments remain raw JSON strings. Failures either throw or
|
||||
* end with `error`/`aborted`, and consumers must handle both paths.
|
||||
* afterward; tool arguments remain raw JSON strings. An adapter implementation
|
||||
* may throw, but `LlmService.stream()` normalizes that failure to a terminal
|
||||
* `error` or `aborted` finish before exposing it to consumers.
|
||||
*/
|
||||
type StreamChunk =
|
||||
| { type: 'block-start'; index: number; blockType: ContentBlockType }
|
||||
@@ -64,10 +65,10 @@ interface LlmFailure {
|
||||
- **提供方停顿在传输层受到时限约束。** 两个已交付的远程适配器都暴露正数且有限的 `streamIdleTimeoutMs`,默认五分钟。watchdog 只在 iterator `next()` 尚未完成时启动,整个请求使用同一个稳定 signal,把自身到期映射为 `TIMEOUT`,并把更早发生的调用方中止保留为 `ABORTED`。
|
||||
- **上下文溢出只有一个规范 code。** 两个 DeepSeek 适配器都通过 `isContextWindowExceededError()` 对提供方的显式细节分类并暴露 `CONTEXT_WINDOW_EXCEEDED`,无论失败以抛出的 HTTP `LlmError` 还是带内 finish error 到达。消费方按 code 路由,绝不依赖提供方文本。
|
||||
- **空 completion 是可重试错误,而不是静默的成功结果。** 两个适配器都把没有携带任何内容块的终止性 `stop` 结束映射为携带规范 `EMPTY_RESPONSE` code 的 `finish {kind:'error'}`,`dsh-llm-retry` 默认会重试它;详见[空模型响应可重试](../../.agents/notes/implemented/bug-fix/2026-07-24-empty-model-response-is-retryable.md)。
|
||||
- **每个提供方 HTTP 请求都携带应用归属头。** 适配器发送 `attributionHeaders()`(见下文)作为 `User-Agent` 基线,并通过协议级测试加以证明(mock 服务器断言收到的 header,或对基于库的适配器使用库的 header 钩子)。
|
||||
- **每个提供方 HTTP 请求都携带应用归属头。** 适配器发送下文的 `attributionHeaders()`,即 `User-Agent` 基线。
|
||||
- **回放状态归适配器所有。** 成功的 `finish` 可以携带重建提供方原生响应所需的无损 JSON 状态。循环会将其与组装后的 assistant 消息一起存储。后续请求中,仅当历史提供方与目标提供方当前注册到完全相同的适配器实例时,`LlmService` 才会传递该状态。该适配器负责校验状态并拥有所有跨模型或跨提供方转换;其他适配器只会收到提供方无关的内容与 provenance,不会收到私有状态。
|
||||
|
||||
该契约由两个有意保持独立的实现锁定:`dsh-llm-deepseek`(直接 fetch,SSE(Server-Sent Events)分帧经由 `eventsource-parser`)和 `dsh-llm-pi-ai`(通过 `@earendil-works/pi-ai` 实现的通用多提供方适配器)。基于库的适配器覆盖 finish 分片错误路径,而传输边界测试证明每个空闲 watchdog 都会停止其实际请求。
|
||||
两个彼此独立的实现遵循该契约:`dsh-llm-deepseek` 使用直接 fetch,并通过 `eventsource-parser` 进行 SSE(Server-Sent Events)分帧;`dsh-llm-pi-ai` 则通过 `@earendil-works/pi-ai` 提供通用多提供方适配器。两者都会把取消与空闲 watchdog 传递至提供方请求。
|
||||
|
||||
## `ResolvedRetryPolicy`
|
||||
|
||||
@@ -75,7 +76,7 @@ interface LlmFailure {
|
||||
|
||||
## `AppIdentity`:应用归属
|
||||
|
||||
每个适配器都会向提供方发送的静态公开应用标识([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts))。`attributionHeaders(identity?)` 只把它映射到标准 `User-Agent` header;该契约有意不支持 OpenRouter 特有的应用归属 header。默认 `APP_IDENTITY` 从包(package) manifest(元数据清单)获取版本;每个字段都是公开产品事实——不含 secret、路径、会话 id 或逐用户标识,且任何逐请求信息都不得影响这些值。设计理由见[强制 `User-Agent` 归属](../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。
|
||||
每个适配器都会向提供方发送的静态公开应用标识([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts))。`attributionHeaders(identity?)` 只把它映射到标准 `User-Agent` header;该契约有意不支持 OpenRouter 特有的应用归属 header。默认 `APP_IDENTITY` 从包 manifest(元数据清单)获取版本;每个字段都是公开产品事实——不含 secret、路径、会话 id 或逐用户标识,且任何逐请求信息都不得影响这些值。设计理由见[强制 `User-Agent` 归属](../../.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -141,8 +142,9 @@ declare class BlockAssembler {
|
||||
push(chunk: StreamChunk): void;
|
||||
/**
|
||||
* Assemble all blocks seen so far, in stream order.
|
||||
* @returns one block per seen index; an open block assembles from its
|
||||
* accumulated deltas (an unknown block type never closed by `block-end` throws).
|
||||
* @returns one block per seen index, except that max-token truncation drops
|
||||
* tool calls that cannot be executed safely; an open block assembles from
|
||||
* its accumulated deltas (an unknown block type never closed by `block-end` throws).
|
||||
*/
|
||||
blocks(): ContentBlock[];
|
||||
/** Usage from the `usage` chunk; undefined until one arrives. */
|
||||
@@ -162,13 +164,19 @@ declare class BlockAssembler {
|
||||
|
||||
## seam
|
||||
|
||||
`LlmAdapter` 是提供方 seam:创建子类、实现 `stream()`,再用 `ctx.llm.registerAdapter(providers, adapter)` 注册一个适配器实例。`GenerateOptions.provider` 选择已注册适配器;`GenerateOptions.model` 会传给该适配器,无需在生命周期启动时注册。重复提供方路由会原子失败。可选的 `providerRetryPolicy()` 会按路由捕获并填入 normal 默认值,`providerInfo()` 与异步 `listModels()` 方法则为 `LlmService.listProviders()` / `listModels()` 提供分离的 selector 元数据。该目录仅供参考,不是请求白名单:适配器仍是权威,并可接受未列出的模型 id。单次异步 `resolveModel()` 查询返回确切模型身份,以及可选的对正确性敏感的上下文容量、由模型持有的有序推理强度 ID 和部署默认值;字段缺失表示元数据或能力不可用,而不表示目录成员关系无效。解析器会接收可选的取消信号,并且必须在信号中止后迅速完成结算。`LlmService.resolveModelInfo()` 会校验聚合结果并返回分离值。服务通过最终适配器边界的 `resolveCallConfig()` 校验推理强度并填入默认值,因此直接调用也无法绕过对不支持推理强度的拒绝;直接分派会在等待解析前捕获一项适配器注册。agent loop 则使用 `prepareCall()`,使模型解析、请求头持久记录和分派全程使用同一项注册。适配器查找发生在 `llm/stream` waterfall(瀑布式事件)的终端 continuation,因此 listener 可以在查找前短路调用,或路由一个可变的一次性请求。`block-start` / `block-end` 的 `index` 关联与 assembler 共同意味着适配器只需 emit 格式正确的分片——块重组不是每个适配器各自的问题。消费方 surface(`ctx.llm.stream()`)与 `llm/stream` waterfall 见 [architecture.md § 内容块与流式传输](../architecture.md#content-blocks-and-streaming-dsh-llm)。
|
||||
`LlmAdapter` 是提供方 seam:创建子类、实现 `stream()`,再用 `ctx.llm.registerAdapter(providers, adapter)` 注册一个适配器实例。`GenerateOptions.provider` 选择已注册适配器;`GenerateOptions.model` 会传给该适配器,无需在生命周期启动时注册。重复提供方路由会原子失败。可选的 `providerRetryPolicy()` 会按路由捕获并填入 normal 默认值,`providerInfo()` 与异步 `listModels()` 方法则为 `LlmService.listProviders()` / `listModels()` 提供分离的 selector 元数据。该目录仅供参考,不是请求白名单:适配器仍是权威,并可接受未列出的模型 id。单次异步 `resolveModel()` 查询返回确切模型身份,以及可选的对正确性敏感的上下文容量、适配器配置的 `defaultMaxTokens`、由模型持有的有序推理强度 ID 和部署默认值;字段缺失表示元数据不可用或保留提供方持有的行为,而不表示目录成员关系无效。解析器会接收可选的取消信号,并且必须在信号中止后迅速完成结算。`LlmService.resolveModelInfo()` 会校验聚合结果并返回分离值。在最终适配器边界,`resolveCallConfig()` 仅在 `maxTokens` 缺失时填入输出默认值,并校验和填入推理强度,因此直接调用也无法绕过任何一项已配置行为;直接分派会在等待解析前捕获一项适配器注册。agent loop 则使用 `prepareCall()`,使模型解析、请求头持久记录和分派全程使用同一项注册,保留来自同一次查询的分离上下文元数据,并报告适配器填入的配置字段。适配器查找发生在 `llm/stream` waterfall(瀑布式事件)的终端 continuation,因此 listener 可以在查找前短路调用,或路由一个可变的一次性请求。AgentLoop 在外层 waterfall 返回流句柄时观察到一次请求尝试;这个有限边界不能证明惰性终端适配器已构造完成或开始提供方 I/O。`block-start` / `block-end` 的 `index` 关联与 assembler 共同意味着适配器只需 emit 格式正确的分片——块重组不是每个适配器各自的问题。消费方 surface(`ctx.llm.stream()`)与 `llm/stream` waterfall 见 [architecture.md § 内容块与流式传输](../architecture.md#content-blocks-and-streaming-dsh-llm)。
|
||||
|
||||
```ts type-equiv
|
||||
/** One model call whose config and adapter registration were resolved together. */
|
||||
interface PreparedLlmCall {
|
||||
/** Detached, deep-frozen config with any adapter-owned default materialized. */
|
||||
readonly config: LlmCallConfig
|
||||
/** Immutable retry policy captured with the adapter registration. */
|
||||
readonly retryPolicy: ResolvedRetryPolicy
|
||||
/** Detached context metadata resolved with the registration-bound call. */
|
||||
readonly context?: LlmModelContext
|
||||
/** Config fields materialized by the captured adapter rather than proposed by the caller. */
|
||||
readonly adapterDefaults: LlmCallConfigAdapterDefaults
|
||||
/**
|
||||
* Dispatch this call once through the registration captured during
|
||||
* preparation. The request's call-config fields must match {@link config};
|
||||
@@ -215,7 +223,7 @@ declare abstract class LlmAdapter {
|
||||
* @param model - exact model id passed to {@link GenerateOptions.model}.
|
||||
* @param _signal - cancellation for this exact-model lookup; asynchronous
|
||||
* implementations must settle promptly after it aborts.
|
||||
* @returns provider/model identity plus any context and reasoning metadata.
|
||||
* @returns provider/model identity plus any context, call-default, and reasoning metadata.
|
||||
*/
|
||||
resolveModel(
|
||||
provider: string,
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/lsp.md
|
||||
lsp.md: 62b133cbfdf521e067c56355664d7514a613397f
|
||||
lsp.zh.md: d7000970ec9114bcdad40a39d2712d48b9865529
|
||||
lsp.zh.md: 51a19a51a8ad92e744cb920a51f3214f68ae0036
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](lsp.md) | 中文
|
||||
|
||||
LSP seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md):它在单一 `ctx.lsp` 服务上公开语义代码导航,并拆分到多个包(package):接口([dsh-lsp](../../packages/lsp/lsp),`ctx.lsp` + 提供方注册表)、通用实现([dsh-lsp-local](../../packages/lsp/lsp-local),经过配置的 stdio 语言服务器宿主)和消费方([dsh-tool-lsp](../../packages/lsp/tool-lsp),即 `lsp` 工具 schema)。LSP 是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.md) 中。更换提供方不会改变模型请求导航的方式。
|
||||
LSP seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md):它在单一 `ctx.lsp` 服务上公开语义代码导航,并拆分到多个包:接口([dsh-lsp](../../packages/lsp/lsp),`ctx.lsp` + 提供方注册表)、通用实现([dsh-lsp-local](../../packages/lsp/lsp-local),经过配置的 stdio 语言服务器宿主)和消费方([dsh-tool-lsp](../../packages/lsp/tool-lsp),即 `lsp` 工具 schema)。LSP 是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.md) 中。更换提供方不会改变模型请求导航的方式。
|
||||
|
||||
源文件:[`packages/lsp/lsp/src/types.ts`](../../packages/lsp/lsp/src/types.ts)
|
||||
|
||||
@@ -113,7 +113,7 @@ type LspQueryResult =
|
||||
|
||||
## 提供方与服务
|
||||
|
||||
每个提供方拥有一个稳定的品牌化 `id`,以及一份互斥的、小写且以点开头的扩展名映射。`registerProvider` 会原子保留 id 和每个扩展名:注册无效或冲突时不发布任何内容;其 disposer 会释放所有保留项。每次查询独立选择提供方,且选择与顺序无关;没有匹配项时抛出 `LspError` `LSP_UNAVAILABLE`。该 seam 不公开协议类型、进程或文档控制,也不提供通用 JSON-RPC 逃生口。
|
||||
每个提供方拥有一个稳定的品牌化 `id`,以及一份互斥的、小写且以点开头的扩展名映射。`registerProvider` 会原子预留 id 和每个扩展名:注册无效或冲突时不发布任何内容;其 disposer 会释放所有保留项。每次查询独立选择提供方,且选择与顺序无关;没有匹配项时抛出 `LspError` `LSP_UNAVAILABLE`。该 seam 不公开协议类型、进程或文档控制,也不提供通用 JSON-RPC 逃生口。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
|
||||
@@ -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 docs/core-data-structures/persistence.md
|
||||
persistence.md: 0280093883b9fbd4c4bbc788bce7193d59bb7182
|
||||
persistence.zh.md: d32274bbc2e96e3754a8061ba37427e34b6205f6
|
||||
persistence.md: 0968496201defa869d94925e8e5ae3c5da1bbd37
|
||||
persistence.zh.md: efb01427b4355e531fb9b86922223cf27d3b3db0
|
||||
|
||||
@@ -4,7 +4,7 @@ English | [中文](persistence.zh.md)
|
||||
|
||||
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](../../.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, crash-repairing load, non-mutating inspect, and lightweight list/snapshot observation 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 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, reusable Session preparation, logical load/inspect, physical suffix reads, and lightweight list/snapshot observation over the existing `SessionEvent` — **no parallel persisted event type** — and two interchangeable backends implementing the same contract. See the [session-persistence Agent Note](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md).
|
||||
|
||||
## The flush checkpoint
|
||||
|
||||
@@ -14,9 +14,9 @@ The seam is a textbook [capability seam](../../.agents/notes/implemented/archite
|
||||
|
||||
A backend that reloads a log crashed mid-turn finds an open `turn/start` with no `turn/end`. It does **not** truncate — a single turn can be huge in a long-horizon task (many steps, large tool output), and those events were durably appended before the crash. Instead it closes the orphaned turn with a synthetic `turn/end { reason: { kind: 'interrupted' } }`, keeping the interrupted execution balanced without changing any standalone events before or after it. `interrupted` is the one `TurnEndReason` no loop emits (see [session.md](session.md#why-a-turn-ended-turnendreasonmap)).
|
||||
|
||||
Repair applies only to cold sessions. For a live id, `SessionPersistence.load(id)` snapshots the in-memory log, waits until that snapshot is durable, and returns it with the stored header only when balanced; an open live turn rejects rather than receiving synthetic interruption boundaries. A coordinator-backed cold load reserves the id across backend reads and repair writes, so concurrent publication of a same-id live session rejects and rolls back. HMR also adopts a live prefix without closing its active turn.
|
||||
Repair applies only to cold sessions. For a live id, `SessionPersistence.load(id)` waits until the authoritative in-memory snapshot is durable and returns it only when balanced; an open live turn rejects rather than receiving synthetic interruption boundaries. HMR adopts a live prefix without closing its active turn.
|
||||
|
||||
`SessionPersistence.inspect(id)` is the observer counterpart to recovery: it returns a detached valid stored prefix without truncating a torn record, adding interruption closers, or publishing write state. Same-id serialization keeps it coherent with backend writes. Derived read models use `inspect`, never `load`, so observing a checkpointed open turn cannot mutate the log if live ownership begins concurrently.
|
||||
`SessionPersistence.inspect(id)` constructs an immutable logical Session without publishing it or writing recovery. Cold inspection balances an interrupted turn in memory while leaving torn physical tails untouched; inspection of an already-live Session borrows its current immutable snapshot and may therefore contain an open turn. Coordinator-backed implementations retain the exact cold unpublished Session in a bounded LRU, so repeated history reads and a later `prepare(id)` share one read, decompression, validation, freeze, and Session construction. `prepare(id)` reserves the Session, commits pending repair, and returns a disposable publication handle; `load(id)` uses the same machinery to commit repair without publication. The [Session preparation decision](../../.agents/notes/implemented/architecture/2026-08-05-session-preparation.md) owns this lifecycle.
|
||||
|
||||
## `SessionLocation` — optional per-session artifact target
|
||||
|
||||
@@ -66,6 +66,11 @@ interface SessionHeader {
|
||||
* boundary lets resume and replay distinguish parent history from child work.
|
||||
*/
|
||||
readonly seedLength?: number
|
||||
/**
|
||||
* Coarse product classification for a session created as a subagent child.
|
||||
* This is presentation metadata, not proof that the child is continuable.
|
||||
*/
|
||||
readonly origin?: 'subagent'
|
||||
/**
|
||||
* Delegation depth: absent (zero) for a top-level session, parent depth + 1
|
||||
* for a subagent child. Persisted so a recursion budget survives restart and
|
||||
@@ -77,7 +82,7 @@ interface SessionHeader {
|
||||
|
||||
## `CreateSessionOptions` — seeding and metadata
|
||||
|
||||
Creating a `Session` through the store takes a `seed` (initial replay or fork history) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller supplies the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the `delegationDepth`, and — only when reconstructing a persisted session — the original `createdAt` to preserve it.
|
||||
Creating a `Session` through the store takes a `seed` (initial replay or fork history) and `meta` (the storage-level fields the store folds into a `SessionHeader`). The store fills in `version`/`id` and defaults `createdAt`; the caller may supply the validated absolute `cwd`, the `parentSession` lineage, the `seedLength` seed boundary, the optional coarse `origin`, the `delegationDepth`, and an existing `createdAt`. `origin: 'subagent'` lets product navigation hide duplicate child rows; it does not prove that a descriptor is valid or that the child can resume.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -97,6 +102,7 @@ interface CreateSessionOptions {
|
||||
readonly parentSession?: SessionId
|
||||
readonly createdAt?: number
|
||||
readonly seedLength?: number
|
||||
readonly origin?: 'subagent'
|
||||
readonly delegationDepth?: number
|
||||
}
|
||||
}
|
||||
@@ -104,6 +110,72 @@ interface CreateSessionOptions {
|
||||
|
||||
Replay/fork is therefore `ctx.sessions.create(id, { seed: seedEvents })`; resuming a *persisted* session into a live agent is `ctx.agents.resume({ resumeSessionId })`.
|
||||
|
||||
## Preparation and restoration ownership
|
||||
|
||||
`SessionStore.prepare()` accepts ordinary creation options or fresh persistence graphs transferred through `RestoredSessionOptions`. The restoration branch validates and freezes the transferred header and events in place, so callers must retain no mutable aliases. `SessionPreparation` then owns the exact unpublished Session until publication or rollback; disposal is synchronous and idempotent. Persistence inspection exposes only `SessionInspection`, an immutable logical view borrowed from the same prepared Session.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Fresh storage values transferred to {@link SessionStore.prepare} without a
|
||||
* second serialization copy. Callers retain no mutable aliases.
|
||||
*/
|
||||
interface RestoredSessionOptions {
|
||||
/** Fresh detached storage events to validate and freeze in place. */
|
||||
readonly seed: SessionEvent[]
|
||||
/** Fresh detached storage metadata to validate and freeze in place. */
|
||||
readonly meta: SessionHeader
|
||||
/** Select the persistence ownership-transfer path. */
|
||||
readonly seedSource: 'persistence'
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Inputs accepted while constructing an unpublished Session. */
|
||||
type PrepareSessionOptions =
|
||||
| (CreateSessionOptions & { readonly seedSource?: undefined })
|
||||
| RestoredSessionOptions
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Options for a preparation whose provider retains unpublished state. */
|
||||
interface SessionPreparationOptions {
|
||||
/** Release provider-owned state when the Session was not published. */
|
||||
readonly release?: () => void
|
||||
}
|
||||
```
|
||||
|
||||
```ts public-api
|
||||
/**
|
||||
* One exact unpublished Session and the provider state that keeps it usable.
|
||||
* Disposal is synchronous and idempotent. Providers decide whether release
|
||||
* returns the Session to a cache or discards it; publication may consume that
|
||||
* state before disposal, making the callback a no-op.
|
||||
*/
|
||||
declare class SessionPreparation implements Disposable {
|
||||
/** The exact Session to use for setup and publication. */
|
||||
readonly session: Session;
|
||||
/**
|
||||
* Wrap an unpublished Session in one preparation lifetime.
|
||||
* @param session - exact unpublished Session.
|
||||
* @param options - optional provider release behavior.
|
||||
* @returns a preparation disposed after publication or rollback.
|
||||
*/
|
||||
static create(session: Session, options?: SessionPreparationOptions): SessionPreparation;
|
||||
/** Release provider state once when this preparation leaves its caller. */
|
||||
[Symbol.dispose](): void;
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Immutable logical session prepared from persistence or a live owner. */
|
||||
interface SessionInspection {
|
||||
/** Validated immutable session metadata. */
|
||||
readonly meta: SessionHeader
|
||||
/** Validated contiguous logical event log. */
|
||||
readonly events: readonly SessionEvent[]
|
||||
}
|
||||
```
|
||||
|
||||
## Lightweight source revisions
|
||||
|
||||
Consumers of derived state compare a cheap opaque revision before loading a full event log. The persistence backend owns its representation and changes it transactionally with append or mutating load repair; callers compare it only for equality.
|
||||
@@ -128,7 +200,7 @@ interface SessionPersistenceSnapshot {
|
||||
|
||||
## The backends
|
||||
|
||||
Both implement the same abstract `SessionPersistence` (locate/create/append/load/inspect/list/listSnapshots over `SessionEvent`, with optional cancellation on observation methods) and pass `runPersistenceContract`, proving the seam is genuinely backend-agnostic:
|
||||
Both implement the same abstract `SessionPersistence` (locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots over `SessionEvent`, with optional cancellation on observation methods) and pass `runPersistenceContract`, proving the seam is genuinely backend-agnostic:
|
||||
|
||||
- **[dsh-session-persistence-jsonl](../../packages/session-persistence/session-persistence-jsonl)** — an append-only logical JSONL log per session, stored as checksummed concatenated Zstandard frames by default or raw lines by configuration, with crash-safe atomic writes, interrupted-turn recovery, 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.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
事件日志的**持久性 seam**。[session.md](session.md) 描述了内存中的 `Session`:仅追加的 `SessionEvent` 日志即为真源。本页描述如何使该日志持久化:抽象的 `SessionPersistence` 服务、它的后端、flush 检查点、崩溃恢复,以及随日志一同存储的元数据头。日志承载的事件词汇在生成的[持久化日志事件目录](../persistence-catalog.md)中逐项列举。
|
||||
|
||||
该 seam 是典型的[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md):一个抽象服务([dsh-session-persistence](../../packages/session-persistence/session-persistence),`ctx.sessionPersistence`)在现有 `SessionEvent` 上定义 locate/create/append、会执行崩溃修复的 load、不会修改数据的 inspect,以及轻量的 list/snapshot 观察——**没有平行的持久化类型**——以及两个可互换、通过同一套 `runPersistenceContract` 的后端。见 [session-persistence Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)。
|
||||
该 seam 是典型的[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md):一个抽象服务([dsh-session-persistence](../../packages/session-persistence/session-persistence),`ctx.sessionPersistence`)在现有 `SessionEvent` 上定义 locate/create/append、可复用的 Session 准备流程、逻辑 load/inspect、物理后缀读取,以及轻量的 list/snapshot 观察——**没有平行的持久化事件类型**——以及两个实现同一契约的可互换后端。见 [session-persistence Agent Note](../../.agents/notes/implemented/architecture/2026-06-14-session-persistence.md)。
|
||||
|
||||
## flush 检查点
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
后端重新加载一个在轮次中途崩溃的日志时,会发现一个已打开的 `turn/start` 却没有 `turn/end`。它**不会**截断日志:在长周期任务中,单个轮次可能非常庞大(许多步骤、大量工具输出),而这些事件在崩溃前已被持久追加。后端改为用一个合成的 `turn/end { reason: { kind: 'interrupted' } }` 关闭这个遗留轮次,在不改变其前后任何独立事件的情况下配平被中断的执行。`interrupted` 是唯一一个不由循环发出的 `TurnEndReason`(见 [session.md](session.md#why-a-turn-ended-turnendreasonmap))。
|
||||
|
||||
修复仅适用于冷会话。对于活跃 id,`SessionPersistence.load(id)` 会对内存日志拍摄快照,等待该快照完成持久化,并且只在日志平衡时连同已存储的 header 返回;若活跃轮次仍未闭合,则拒绝操作,而不是添加合成的中断边界。由协调器管理的冷加载会在后端读取和修复写入期间占用该 id,因此并发发布同 id 的活跃会话会被拒绝并回滚。HMR 也会接管活跃前缀,而不会关闭其中正在进行的轮次。
|
||||
修复仅适用于冷会话。对于活跃 id,`SessionPersistence.load(id)` 会等待权威内存快照完成持久化,并且只在日志平衡时返回;若活跃轮次仍未闭合,则拒绝操作,而不是添加合成的中断边界。HMR 会接管活跃前缀,而不会关闭其中正在进行的轮次。
|
||||
|
||||
`SessionPersistence.inspect(id)` 是恢复机制面向观察方的对等操作:它返回已存储有效前缀的独立副本,不截断不完整记录、不添加中断结束事件,也不发布写入状态。同 id 串行化确保它与后端写入保持一致。派生读取模型使用 `inspect`,绝不使用 `load`,因此即使活跃所有权并发建立,观察已落检查点但仍未闭合的轮次也不会修改日志。
|
||||
`SessionPersistence.inspect(id)` 会构造一个不可变的逻辑 Session,但不发布它,也不写入恢复内容。冷检查会在内存中配平中断的 turn,同时保持撕裂的物理尾部不变;检查已经实时存在的 Session 则借用其当前不可变快照,因此可能包含打开的 turn。使用协调器的实现会在有界 LRU 中保留这个精确的冷未发布 Session,因此重复历史读取与后续 `prepare(id)` 可复用同一次读取、解压、验证、冻结及 Session 构造。`prepare(id)` 会预留该 Session、提交待处理修复并返回可 dispose 的发布句柄;`load(id)` 使用相同机制提交修复,但不会发布 Session。该生命周期由 [Session 准备阶段决策](../../.agents/notes/implemented/architecture/2026-08-05-session-preparation.md)定义。
|
||||
|
||||
## `SessionLocation`——可选的逐会话产物目标
|
||||
|
||||
@@ -66,6 +66,11 @@ interface SessionHeader {
|
||||
* boundary lets resume and replay distinguish parent history from child work.
|
||||
*/
|
||||
readonly seedLength?: number
|
||||
/**
|
||||
* Coarse product classification for a session created as a subagent child.
|
||||
* This is presentation metadata, not proof that the child is continuable.
|
||||
*/
|
||||
readonly origin?: 'subagent'
|
||||
/**
|
||||
* Delegation depth: absent (zero) for a top-level session, parent depth + 1
|
||||
* for a subagent child. Persisted so a recursion budget survives restart and
|
||||
@@ -77,7 +82,7 @@ interface SessionHeader {
|
||||
|
||||
## `CreateSessionOptions`:seed 与元数据
|
||||
|
||||
通过 store 创建 `Session` 时会接收 `seed`(初始回放或 fork 历史)与 `meta`(store 折叠进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、`delegationDepth`,以及——仅在重建已持久化会话时——需要保留的原始 `createdAt`。
|
||||
通过 store 创建 `Session` 时会接收 `seed`(初始回放或 fork 历史)与 `meta`(store 折叠进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并为 `createdAt` 提供默认值;调用方可以提供已校验的绝对 `cwd`、`parentSession` 谱系、`seedLength` 种子边界、可选的粗粒度 `origin`、`delegationDepth` 以及已有的 `createdAt`。`origin: 'subagent'` 让产品导航能够隐藏重复的 child 行;它不证明描述符有效,也不证明 child 可以恢复。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -97,6 +102,7 @@ interface CreateSessionOptions {
|
||||
readonly parentSession?: SessionId
|
||||
readonly createdAt?: number
|
||||
readonly seedLength?: number
|
||||
readonly origin?: 'subagent'
|
||||
readonly delegationDepth?: number
|
||||
}
|
||||
}
|
||||
@@ -104,6 +110,72 @@ interface CreateSessionOptions {
|
||||
|
||||
因此,回放/fork 的调用方式为 `ctx.sessions.create(id, { seed: seedEvents })`;将一个*持久化*会话恢复为活跃 agent 的调用方式为 `ctx.agents.resume({ resumeSessionId })`。
|
||||
|
||||
## 准备与恢复所有权
|
||||
|
||||
`SessionStore.prepare()` 接收普通创建选项,或通过 `RestoredSessionOptions` 转移所有权的新鲜持久化对象图。恢复分支会直接验证并冻结转移来的 header 与事件,因此调用方不得保留可变别名。`SessionPreparation` 随后持有该精确的未发布 Session,直至发布或回滚;dispose 是同步且幂等的。持久化检查只暴露 `SessionInspection`,即从同一个已准备 Session 借用的不可变逻辑视图。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Fresh storage values transferred to {@link SessionStore.prepare} without a
|
||||
* second serialization copy. Callers retain no mutable aliases.
|
||||
*/
|
||||
interface RestoredSessionOptions {
|
||||
/** Fresh detached storage events to validate and freeze in place. */
|
||||
readonly seed: SessionEvent[]
|
||||
/** Fresh detached storage metadata to validate and freeze in place. */
|
||||
readonly meta: SessionHeader
|
||||
/** Select the persistence ownership-transfer path. */
|
||||
readonly seedSource: 'persistence'
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Inputs accepted while constructing an unpublished Session. */
|
||||
type PrepareSessionOptions =
|
||||
| (CreateSessionOptions & { readonly seedSource?: undefined })
|
||||
| RestoredSessionOptions
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Options for a preparation whose provider retains unpublished state. */
|
||||
interface SessionPreparationOptions {
|
||||
/** Release provider-owned state when the Session was not published. */
|
||||
readonly release?: () => void
|
||||
}
|
||||
```
|
||||
|
||||
```ts public-api
|
||||
/**
|
||||
* One exact unpublished Session and the provider state that keeps it usable.
|
||||
* Disposal is synchronous and idempotent. Providers decide whether release
|
||||
* returns the Session to a cache or discards it; publication may consume that
|
||||
* state before disposal, making the callback a no-op.
|
||||
*/
|
||||
declare class SessionPreparation implements Disposable {
|
||||
/** The exact Session to use for setup and publication. */
|
||||
readonly session: Session;
|
||||
/**
|
||||
* Wrap an unpublished Session in one preparation lifetime.
|
||||
* @param session - exact unpublished Session.
|
||||
* @param options - optional provider release behavior.
|
||||
* @returns a preparation disposed after publication or rollback.
|
||||
*/
|
||||
static create(session: Session, options?: SessionPreparationOptions): SessionPreparation;
|
||||
/** Release provider state once when this preparation leaves its caller. */
|
||||
[Symbol.dispose](): void;
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/** Immutable logical session prepared from persistence or a live owner. */
|
||||
interface SessionInspection {
|
||||
/** Validated immutable session metadata. */
|
||||
readonly meta: SessionHeader
|
||||
/** Validated contiguous logical event log. */
|
||||
readonly events: readonly SessionEvent[]
|
||||
}
|
||||
```
|
||||
|
||||
## 轻量源修订号
|
||||
|
||||
派生状态的消费方会在加载完整事件日志之前比较一个低开销的不透明修订号。其表示由持久化后端拥有,并随 append 或会修改数据的 load 修复以事务方式改变;调用方仅比较修订号是否相等。
|
||||
@@ -128,7 +200,7 @@ interface SessionPersistenceSnapshot {
|
||||
|
||||
## 后端
|
||||
|
||||
两者都实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 上执行 locate/create/append/load/inspect/list/listSnapshots),并通过 `runPersistenceContract`,证明该 seam 确实与后端无关:
|
||||
两者都实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 上执行 locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots,观察方法可选支持取消),并通过 `runPersistenceContract`,证明该 seam 确实与后端无关:
|
||||
|
||||
- **[dsh-session-persistence-jsonl](../../packages/session-persistence/session-persistence-jsonl)**——每个会话一份仅追加的逻辑 JSONL 日志,默认存储为带 checksum 的连续 Zstandard frame,也可配置为原始行;支持崩溃安全的原子写入、被中断轮次的恢复以及读取/回放路径。
|
||||
- **[dsh-session-persistence-sqlite](../../packages/session-persistence/session-persistence-sqlite)**:基于 `node:sqlite`,每个 `SessionEvent` 一行。行结构 `(session_id, seq, type, time, data, source_event_seqs, surface_op)` 与事件 1:1 映射(包含可选的 surface 元数据),因此没有需要保持同步的并行持久化 schema。
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/pty.md
|
||||
pty.md: 97e1e662d1128ab0555e34f8284cf69d7d9d0d1a
|
||||
pty.zh.md: b17bc0d2c7bdb2a980df36824bd360ea975967f5
|
||||
pty.zh.md: a57f7448274de583dbb110ba9499e15dfb4de5f9
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[English](pty.md) | 中文
|
||||
|
||||
PTY 后端、`ctx.pty` 与面向模型的消费方共享的类型。[持久 PTY Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md) 负责记录决策依据;本页记录来自 [`packages/pty/pty/src/types.ts`](../../packages/pty/pty/src/types.ts) 的跨包(package)词汇。
|
||||
PTY 后端、`ctx.pty` 与面向模型的消费方共享的类型。[持久 PTY Agent Note](../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md) 负责记录决策依据;本页记录来自 [`packages/pty/pty/src/types.ts`](../../packages/pty/pty/src/types.ts) 的跨包词汇。
|
||||
|
||||
## 标识与就绪
|
||||
|
||||
`PtySessionId` 是由服务铸造的品牌化 id。可选名称是拥有者本地的显示元数据;授权比较的是确切的所属 `Agent`,而不是名称或猜测的 id。
|
||||
`PtySessionId` 是由服务铸造的品牌化 id。可选名称是拥有者本地的显示元数据;授权比较的是拥有该会话的确切 `Agent`,而不是名称或猜测的 id。
|
||||
|
||||
`PtyWaitReason` 说明一次发送为何返回。它与 `PtySessionStatus` 无关:一次发送可能因静默或超时而返回,但顶层 shell 仍然存活;`session_exit` 表示该 shell 已退出,而不是某个任意的前台子进程已退出。
|
||||
|
||||
@@ -24,7 +24,7 @@ type PtySessionStatus =
|
||||
|
||||
## 后端与活跃会话
|
||||
|
||||
后端负责某个已注册类型的启动方式和就绪检测。`PtyService` 只在初始化成功后才发布返回的会话,随后负责 id 授权与清理。无法清理部分启动资源的后端会以 `PtyBackendCleanupError` 拒绝,从而让资源释放流程保留该清理失败,同时不替换调用方的取消原因。后端会话拥有终端状态,并负责使已捕获资源完全停稳。
|
||||
后端负责启动某种已注册类型的会话并检测其就绪状态。`PtyService` 只在初始化成功后才发布返回的会话,随后负责 id 授权与清理。无法清理部分启动资源时,后端会以 `PtyBackendCleanupError` 拒绝启动;这样,资源释放流程既能保留清理失败,也不会用它替换调用方的取消原因。后端会话拥有终端状态,并负责让已捕获的资源完全停稳。
|
||||
|
||||
```ts type-equiv
|
||||
/** Replaceable provider for one PTY session type. */
|
||||
@@ -58,7 +58,7 @@ interface PtyBackendSession {
|
||||
|
||||
## 发送与保留输出
|
||||
|
||||
一个活跃会话同时只接受一个活动发送。该操作向通用后台任务公开一个消费式输出游标,并向前台调用方公开一个最终结果。`PtyReadResult` 则为有界的会话 scrollback 单独分页。
|
||||
一个活跃会话同时只接受一个活动发送。该操作向通用后台任务提供读取后即推进的输出游标,并向前台调用方提供最终结果。`PtyReadResult` 则为有界的会话 scrollback 单独分页。
|
||||
|
||||
```ts type-equiv
|
||||
/** Live backend-owned send; exactly one may be active per PTY session. */
|
||||
@@ -88,4 +88,4 @@ interface PtySendResult {
|
||||
|
||||
## 归属与持久性
|
||||
|
||||
`PtyService` 会将一项等待完成的清理附加到确切的拥有者作用域,拒绝其他拥有者的操作,并让会话在后端或工具插件重载期间保持存活。PTY 状态与原始字节仍局限在进程内。模型输入与有界的返回输出通过现有 `tool/call`、`tool/result` 和任务结果路径持久保存,而不是重复记录 PTY 会话事件。
|
||||
`PtyService` 会将一项等待完成的清理附加到确切的拥有者作用域,拒绝其他拥有者的操作,并让会话在后端或工具插件重载期间保持存活。PTY 状态与原始字节仍局限在进程内。模型输入与有界返回输出通过现有 `tool/call`、`tool/result` 和任务结果路径持久保存,而不是重复记录 PTY 会话事件。
|
||||
|
||||
@@ -1,6 +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
|
||||
sandbox.md: 9bc05fa06f22fdc9ac9e8aacd482c1e7c2f2edec
|
||||
sandbox.zh.md: 9a52f126758fe0e7988715c7824e963bd6e6ea84
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/sandbox.md
|
||||
sandbox.md: 9e5feafe046f18dad49aeaf281793f0b8e03c240
|
||||
sandbox.zh.md: a1314d1b78eb0d46ea4c8ca5aa330ee83bf89132
|
||||
|
||||
@@ -87,7 +87,27 @@ interface SandboxPolicy extends SandboxExecutionPolicy {
|
||||
|
||||
## Wrapped argv and classification dialects
|
||||
|
||||
`ConfinedArgv` is what the consumer spawns. Besides the replacement argv, it carries the backend's enforcement fact and two orthogonal stderr dialects. `denialSignatures` identify the confined command being blocked while the sandbox works correctly. `runnerFailureSignatures` identify the sandbox runner refusing or failing before it executes the command; consumers check these first and surface a sandbox infrastructure failure, never an ordinary task failure.
|
||||
`RunnerFailureRule` combines evidence that a runner failed before executing the command. A consumer requires a nonzero exit, the optional allowed-exit-code gate, and a case-insensitive fatal signature within one remaining stderr line. Case-insensitive exact full-line informational exclusions are removed first, so a benign runner notice cannot prove failure by itself. The matched line remains available as error detail; classification does not rewrite stderr.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Evidence that identifies a sandbox runner failing before it executes the
|
||||
* wrapped command. A consumer first applies {@link allowedExitCodes} when
|
||||
* present, removes {@link informationalLines} by case-insensitive exact line
|
||||
* equality, then matches {@link fatalSignatures} case-insensitively within
|
||||
* each remaining stderr line. Exit status alone never proves runner failure.
|
||||
*/
|
||||
interface RunnerFailureRule {
|
||||
/** Nonzero process exit codes on which this rule may match; omitted permits any nonzero exit. */
|
||||
allowedExitCodes?: readonly number[]
|
||||
/** Non-empty substrings identifying a fatal runner diagnostic on one stderr line. */
|
||||
fatalSignatures: readonly string[]
|
||||
/** Benign stderr lines excluded by exact full-line equality before fatal matching. */
|
||||
informationalLines?: readonly string[]
|
||||
}
|
||||
```
|
||||
|
||||
`ConfinedArgv` is what the consumer spawns. Besides the replacement argv, it carries the backend's enforcement fact and two orthogonal stderr classifiers. `denialSignatures` identify the confined command being blocked while the sandbox works correctly. `runnerFailureRules` identify the sandbox runner refusing or failing before it executes the command; consumers check these first and surface a sandbox infrastructure failure, never an ordinary task failure.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -110,18 +130,19 @@ interface ConfinedArgv {
|
||||
*/
|
||||
denialSignatures: readonly string[]
|
||||
/**
|
||||
* Case-insensitive signatures for runner failure before command execution.
|
||||
* Consumers check these before denial signatures: runner failure means the
|
||||
* Structured runner-failure evidence rules. Consumers require a matching
|
||||
* fatal stderr line (after informational exclusions) and any rule-specific
|
||||
* exit-code gate before checking denial signatures: runner failure means the
|
||||
* command never ran, while denial means confinement worked and blocked it.
|
||||
*/
|
||||
runnerFailureSignatures: readonly string[]
|
||||
runnerFailureRules: readonly RunnerFailureRule[]
|
||||
}
|
||||
```
|
||||
|
||||
An operator-configured local runner must supply at least one `runnerFailureSignatures` entry for its own pre-exec refusal dialect; the provider adds outer-shell missing and unexecutable forms automatically. This makes an executable custom runner rejecting its profile distinguishable from the wrapped command exiting with the same status.
|
||||
The [local provider](../../packages/sandbox/sandbox-local/README.md) owns operator configuration and maps its runner dialect into these rules. The [sandboxed bash consumer](../../packages/bash/bash-sandbox/README.md) owns spawn and result attribution.
|
||||
|
||||
## Provider and fail-closed errors
|
||||
|
||||
`ctx.sandbox.confine(argv, policy)` returns a `ConfinedArgv` or throws `SandboxUnavailableError` with code `SANDBOX_UNAVAILABLE` when no usable backend exists. A selected runner can also fail closed at execution time, in which case its failure signature carries the same infrastructure meaning. Silent unconfined passthrough is never legal for a confined policy.
|
||||
`ctx.sandbox.confine(argv, policy)` returns a `ConfinedArgv` or throws `SandboxUnavailableError` with code `SANDBOX_UNAVAILABLE` when no usable backend exists. Consumers may also classify a failure while spawning or observing the returned argv; that attribution belongs to the consumer contract. Silent unconfined passthrough is never legal for a confined policy.
|
||||
|
||||
Provider probing arbitrates between multiple candidates and is cached for the provider lifetime. A platform with one candidate may select it directly; execution-time refusal retains the safety property. The local provider reports bwrap and Seatbelt as full and preserves the Landlock launcher's full/partial kernel verdict.
|
||||
Provider selection, probing, caching, and backend-specific enforcement reports belong to the [local provider](../../packages/sandbox/sandbox-local/README.md).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](sandbox.md) | 中文
|
||||
|
||||
[dsh-sandbox](../../packages/sandbox/sandbox) 的进程沙箱 seam 将与宿主共享文件系统和内核的子进程 argv 包装在文件效果策略中,而不将消费方耦合到特定平台运行器。[dsh-sandbox-local](../../packages/sandbox/sandbox-local) 提供 Linux bwrap/Landlock 与 macOS Seatbelt 后端;[dsh-bash-sandbox](../../packages/bash/bash-sandbox) 是第一个消费方。容器、microVM 和远程执行是完整能力 seam 的兄弟实现,而非 `ctx.sandbox` 的提供方。
|
||||
[dsh-sandbox](../../packages/sandbox/sandbox) 的进程沙箱 seam 将与宿主共享文件系统和内核的子进程 argv 包装在文件效果策略中,而不将消费方耦合到特定平台运行器。[dsh-sandbox-local](../../packages/sandbox/sandbox-local) 提供 Linux bwrap/Landlock 与 macOS Seatbelt 后端;[dsh-bash-sandbox](../../packages/bash/bash-sandbox) 是第一个消费方。容器、microVM 和远程执行是完整能力 seam 的同级实现,而非 `ctx.sandbox` 的提供方。
|
||||
|
||||
源码:[`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox/src/index.ts)
|
||||
|
||||
@@ -27,7 +27,7 @@ type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
|
||||
type ConfinedSandboxMode = Exclude<SandboxMode, 'danger-full-access'>
|
||||
```
|
||||
|
||||
强制执行程度是一个报告事实。`full` 表示后端管控了该模式承诺的所有文件效果;`partial` 表示活跃后端或较旧的内核 ABI 仅管控其中一个子集,因此要求绝对保证的消费方必须拒绝或向上暴露这一区别。
|
||||
强制执行完整性是后端报告的事实。`full` 表示后端管控了该模式承诺的所有文件效果;`partial` 表示活跃后端或较旧的内核 ABI 仅管控其中一个子集,因此要求绝对保证的消费方必须拒绝或向上暴露这一区别。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -68,7 +68,7 @@ interface SandboxPolicyRequest {
|
||||
}
|
||||
```
|
||||
|
||||
只有受约束的执行会到达 `ctx.sandbox`;其提供方策略在保留同一 root 的同时收窄模式。这使并发会话、消费方与一次性提权重试可以向同一提供方请求不同边界,而无需改变提供方状态。
|
||||
只有受约束的执行会到达 `ctx.sandbox`;传给提供方的策略在保留同一 root 的同时收窄模式。这使并发会话、消费方与一次性提权重试可以向同一提供方请求不同边界,而无需改变提供方状态。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -87,7 +87,27 @@ interface SandboxPolicy extends SandboxExecutionPolicy {
|
||||
|
||||
## 包装后的 argv 与分类方言
|
||||
|
||||
`ConfinedArgv` 是消费方实际 spawn 的内容。除了替换后的 argv,它还携带后端的强制执行事实和两种正交的 stderr 方言。`denialSignatures` 用于识别沙箱正常工作时被隔离命令被阻止的情况。`runnerFailureSignatures` 用于识别沙箱运行器在执行命令之前拒绝或失败的情况;消费方应先检查后者,将其作为沙箱基础设施故障上报,而非普通任务失败。
|
||||
`RunnerFailureRule` 汇集用于判定 runner 在执行命令前失败的证据。消费方要求进程以非零状态退出,并同时满足可选的允许退出码门控,以及余下某一 stderr 行中不区分大小写的致命签名。系统会先按不区分大小写的整行精确匹配移除信息性排除项,因此无害的 runner 通知本身不能证明失败。匹配到的行仍可用作错误详情;分类过程不会重写 stderr。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Evidence that identifies a sandbox runner failing before it executes the
|
||||
* wrapped command. A consumer first applies {@link allowedExitCodes} when
|
||||
* present, removes {@link informationalLines} by case-insensitive exact line
|
||||
* equality, then matches {@link fatalSignatures} case-insensitively within
|
||||
* each remaining stderr line. Exit status alone never proves runner failure.
|
||||
*/
|
||||
interface RunnerFailureRule {
|
||||
/** Nonzero process exit codes on which this rule may match; omitted permits any nonzero exit. */
|
||||
allowedExitCodes?: readonly number[]
|
||||
/** Non-empty substrings identifying a fatal runner diagnostic on one stderr line. */
|
||||
fatalSignatures: readonly string[]
|
||||
/** Benign stderr lines excluded by exact full-line equality before fatal matching. */
|
||||
informationalLines?: readonly string[]
|
||||
}
|
||||
```
|
||||
|
||||
`ConfinedArgv` 是消费方实际 spawn 的内容。除了替换后的 argv,它还携带后端的强制执行事实和两种正交的 stderr 分类器。`denialSignatures` 用于识别沙箱正常工作时受限命令被阻止的情况。`runnerFailureRules` 用于识别沙箱 runner 在执行命令之前拒绝或失败的情况;消费方应先检查后者,将其作为沙箱基础设施故障上报,而非普通任务失败。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -110,18 +130,19 @@ interface ConfinedArgv {
|
||||
*/
|
||||
denialSignatures: readonly string[]
|
||||
/**
|
||||
* Case-insensitive signatures for runner failure before command execution.
|
||||
* Consumers check these before denial signatures: runner failure means the
|
||||
* Structured runner-failure evidence rules. Consumers require a matching
|
||||
* fatal stderr line (after informational exclusions) and any rule-specific
|
||||
* exit-code gate before checking denial signatures: runner failure means the
|
||||
* command never ran, while denial means confinement worked and blocked it.
|
||||
*/
|
||||
runnerFailureSignatures: readonly string[]
|
||||
runnerFailureRules: readonly RunnerFailureRule[]
|
||||
}
|
||||
```
|
||||
|
||||
运维人员配置的本地运行器必须为自身的 pre-exec 拒绝方言提供至少一条 `runnerFailureSignatures` 条目;提供方会自动添加外层 shell 的 missing 和 unexecutable 形式。这使得可执行的自定义运行器拒绝其 profile 的情况能够与被包装命令以相同状态码退出的情况区分开来。
|
||||
[本地提供方](../../packages/sandbox/sandbox-local/README.md)拥有运维配置,并将其 runner 方言映射到这些规则。[沙箱化 bash 消费方](../../packages/bash/bash-sandbox/README.md)拥有 spawn 与结果归因。
|
||||
|
||||
## 提供方与 fail-closed 错误
|
||||
|
||||
`ctx.sandbox.confine(argv, policy)` 返回一个 `ConfinedArgv`,或在没有可用后端时抛出 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)。已选定的运行器也可能在执行时 fail-closed,此时其失败签名承载相同的基础设施含义。对于受限策略,静默的无隔离透传永远不合法。
|
||||
`ctx.sandbox.confine(argv, policy)` 返回一个 `ConfinedArgv`,或在没有可用后端时抛出 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)。消费方也可以在 spawn 或观察所返回的 argv 时对失败进行分类;该归因属于消费方契约。对于受限策略,静默的无隔离透传永远不合法。
|
||||
|
||||
提供方探测在多个候选后端之间仲裁,结果在提供方生命周期内缓存。只有一个候选后端的平台可以直接选定它;执行时拒绝仍保留安全属性。本地提供方将 bwrap 和 Seatbelt 报告为 full,并保留 Landlock 启动器的 full/partial 内核裁定。
|
||||
提供方选择、探测、缓存和后端专有的强制执行报告归[本地提供方](../../packages/sandbox/sandbox-local/README.md)所有。
|
||||
|
||||
@@ -1,6 +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
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/scope.md
|
||||
scope.md: 73a697f2843293daffff85dabf4656346f7dcd04
|
||||
scope.zh.md: f3c591da2befdcff69d89ad0667653392111fb8e
|
||||
scope.zh.md: 06e2528eca958102110caa3a7d370e0778c0f7b2
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](scope.md) | 中文
|
||||
|
||||
[scope 包(package)](../../packages/core/scope)提供 identity、carrier 与 scoped-layer 词汇,使同一个注册上下文同时代表逐 agent(智能体)可见性和共享生命周期所有权。它是库原语,而不是 Cordis 服务;生命周期设计理由由 [agent-scope 运行时设计 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#scope-routing-one-opaque-key-selects-one-layer)规定,注册表层决策由[共享存储 Agent Note](../../.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md)规定,可调用 API 与过滤语义则由包 [README](../../packages/core/scope/README.md)规定。
|
||||
[scope 包](../../packages/core/scope)提供身份、载体与作用域层词汇,使同一注册上下文同时表达每个 agent(智能体)的可见性和共享生命周期所有权。它是库原语,而不是 Cordis 服务;生命周期设计理由由 [agent-scope 运行时设计 Agent Note](../../.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#scope-routing-one-opaque-key-selects-one-layer)规定,注册表层决策由[共享存储 Agent Note](../../.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md)规定,可调用 API 与过滤语义则由包 [README](../../packages/core/scope/README.md)规定。
|
||||
|
||||
源码:[`packages/core/scope/src/index.ts`](../../packages/core/scope/src/index.ts) 与 [`packages/core/scope/src/store.ts`](../../packages/core/scope/src/store.ts)。
|
||||
|
||||
@@ -28,7 +28,7 @@ type Scoped<T extends object> = object & { readonly [ScopedBrand]: T }
|
||||
|
||||
## 拥有所有权的注册上下文
|
||||
|
||||
`Scope` 将带标签的注册上下文与两个拆卸接口配对。`rawDispose` 保留有序复合 effect 所需的精确 Cordis disposer 身份;`dispose()` 是面向直接调用方和竞态调用方的公共停稳边界。
|
||||
`Scope` 将带标签的注册上下文与两个拆卸接口配对。`rawDispose` 保留有序复合 effect 所需的 Cordis disposer 本身;`dispose()` 是面向直接调用方和竞态调用方的公共完全停稳边界。
|
||||
|
||||
```ts type-equiv
|
||||
/** A minted registration scope and its quiescent disposal boundaries. */
|
||||
@@ -54,6 +54,6 @@ interface ScopeLayer {
|
||||
}
|
||||
```
|
||||
|
||||
`ScopedLayers<L>` 拥有立即创建的全局 layer,以及惰性创建的确切作用域 layer。读取不会创建 layer:`peek(undefined)` 表示没有 overlay,而 `merge()` 会物化按插入顺序排列的全局具名 entry,随后是带作用域的 shadow。注册使用同一个上下文表示可见性与 Cordis effect 所有权,在可选通知前收集一个同步 undo,返回 Cordis 的确切 disposer,并且只在带作用域 layer 的完整 `ScopeLayer` 为空时回收它。
|
||||
`ScopedLayers<L>` 拥有立即创建的全局 layer,以及惰性创建的确切作用域 layer。读取不会创建 layer:`peek(undefined)` 表示不存在作用域覆盖层,而 `merge()` 会依次物化按插入顺序排列的全局具名条目和带作用域的遮蔽项。注册使用同一个上下文表示可见性与 Cordis effect 所有权,在可选通知前取得一个同步撤销函数,返回 Cordis 的原始 disposer,并且只在带作用域 layer 的完整 `ScopeLayer` 为空时回收它。
|
||||
|
||||
`NamedEntries<V>` 提供按插入顺序的查找与 live iteration,重复错误由调用方所有。`AnonymousEntries<V>` 为每次 append 分配唯一标识,使相等的值仍相互独立。迭代在同一非空 table generation 内保持 live;排空 table 会让现有 iterator 与后续插入脱离。两者都返回幂等的确切 entry undo;共享的 `EntryValues` 实现接口不公开。
|
||||
`NamedEntries<V>` 提供按插入顺序的查找和动态迭代,重复项错误由调用方处理。`AnonymousEntries<V>` 为每次 append 分配唯一标识,因此值相等的条目仍彼此独立。在同一轮非空 table 生命周期内,迭代器可以观察后续变化;table 被清空后,现有迭代器不会再观察后续插入。两者都返回幂等、精确对应相应条目的撤销函数;共享实现接口 `EntryValues` 不对外公开。
|
||||
|
||||
@@ -1,6 +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
|
||||
session-query.md: d92af4bac34f7d41457e9e193111c3a53fe8022e
|
||||
session-query.zh.md: ecf330b0a361ffae352a91c0d35524444936606d
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/session-query.md
|
||||
session-query.md: e7514dd6c3bc20a07395663bff40ce65e1363b78
|
||||
session-query.zh.md: 4c3dd4d435dbd8a20fbd4db5da1a7d649c2e6d0b
|
||||
|
||||
@@ -338,6 +338,7 @@ The closed code union distinguishes request validation, missing targets, malform
|
||||
/** Stable machine-routable failure taxonomy for session reads, traces, and search. */
|
||||
type SessionQueryErrorCode =
|
||||
| 'SESSION_QUERY_ABORTED'
|
||||
| 'SESSION_QUERY_CORRUPT_SESSION'
|
||||
| 'SESSION_QUERY_EVENT_NOT_FOUND'
|
||||
| 'SESSION_QUERY_INDEX_FAILED'
|
||||
| 'SESSION_QUERY_INVALID_CONFIG'
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](session-query.md) | 中文
|
||||
|
||||
本文定义面向优先使用 live 数据的逻辑会话语料库的查询词汇。[接口包(package)](../../packages/session-query/session-query)负责精确读取、来源优先级、关系追踪、语义提取,以及与提供方无关的过滤器;[SQLite 包](../../packages/session-query/session-query-sqlite)负责具体全文索引的生命周期。
|
||||
本文定义逻辑会话语料库的查询词汇;当 live 数据存在时,该语料库优先使用 live 数据。[接口包](../../packages/session-query/session-query)负责精确读取、来源优先级、关系追踪、语义提取,以及与提供方无关的过滤器;[SQLite 包](../../packages/session-query/session-query-sqlite)负责具体全文索引的生命周期。
|
||||
|
||||
源码:[`packages/session-query/session-query/src/types.ts`](../../packages/session-query/session-query/src/types.ts)
|
||||
|
||||
## 逻辑记录
|
||||
|
||||
`SessionRecord` 由跨语料库列表返回。它独立于克隆后的实时优先 header 暴露源可用性。`SessionEventRecord` 是轻量的原始日志投影;分类使用与 model-history 推导相同的 `foldSurface()` 状态转换。
|
||||
`SessionRecord` 由全语料库列表返回。它除了克隆的、优先取自 live 源的 header 外,还单独公开各源的可用性。`SessionEventRecord` 是轻量的原始日志投影;分类使用与模型历史推导相同的 `foldSurface()` 状态转换。
|
||||
|
||||
```ts type-equiv
|
||||
/** Whether an event is current model context, replaced context, or raw-log-only. */
|
||||
@@ -51,7 +51,7 @@ interface SessionSurfaceSnapshot {
|
||||
}
|
||||
```
|
||||
|
||||
`SessionTitleObservation` 将同样的原子观测规则应用于标题折叠,使授权消费者能够验证提供标题的源 header。批量读取会按顺序为每个唯一请求 id 返回一个 `SessionTitleObservationResult`:操作失败只影响对应 id,而取消会拒绝整个操作。
|
||||
`SessionTitleObservation` 将同样的原子观测规则应用于标题折叠,使执行授权检查的消费方能够验证提供标题的源 header。批量读取会按顺序为每个唯一请求 id 返回一个 `SessionTitleObservationResult`:操作失败只影响对应 id,而取消会拒绝整个操作。
|
||||
|
||||
```ts type-equiv
|
||||
/** Latest folded title bound to the same session-header observation. */
|
||||
@@ -102,7 +102,7 @@ interface SessionEventRecord {
|
||||
|
||||
## 与提供方无关的过滤器和文档
|
||||
|
||||
会话和事件过滤器数组内的各项按逻辑与(AND)组合;单个列表子句中的各值按逻辑或(OR)组合。范围包含两端。事件的 `text` 子句会对提取出的语义文本执行正则表达式扫描:搜索文本按字面量处理,Unicode 字符不区分大小写,空白字符可灵活匹配;该过程与全文搜索提供方无关。
|
||||
会话和事件过滤器数组内的各项按逻辑与(AND)组合;单个列表子句中的各值按逻辑或(OR)组合。范围包含两端。事件的 `text` 子句会对提取出的语义文本执行正则表达式扫描:搜索文本按字面量处理,按 Unicode 规则执行不区分大小写的匹配,并允许灵活匹配空白字符;该过程与全文搜索提供方无关。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -191,7 +191,7 @@ interface SessionSearchPage<T> {
|
||||
}
|
||||
```
|
||||
|
||||
与跨会话分组 hit 不同,会话内搜索即使没有命中项,也必须公开它观测到的目标 header。
|
||||
与跨会话分组 hit 不同,会话内搜索结果即使没有命中项,也必须公开搜索时观测到的目标 header。
|
||||
|
||||
```ts type-equiv
|
||||
/** Event-search results bound to the indexed target-session observation. */
|
||||
@@ -219,7 +219,7 @@ interface SessionSearchHit extends SessionRecord {
|
||||
|
||||
## 会话谱系
|
||||
|
||||
`SessionLineageTrace` 按由近及远的顺序携带已知 parent,并携带一片由直接 descendant 递归嵌套而成的森林。完整性判别字段使已知 root 与缺失 parent 互斥。
|
||||
`SessionLineageTrace` 按由近及远的顺序携带已知 parent,以及由直接 descendant 递归嵌套而成的森林。完整性判别字段使已知 root 与缺失 parent 互斥。
|
||||
|
||||
```ts type-equiv
|
||||
/** Recursive descendant node in a session-lineage trace. */
|
||||
@@ -292,7 +292,7 @@ interface SessionEventWindow {
|
||||
|
||||
## 事件关系
|
||||
|
||||
事件追踪会区分位置性的 surface 替换与已记录 provenance。除 `replacementChain` 外,每个 seq 列表都包含直接链接;该链从目标沿直接 replacer 追踪到最终的位置替换。
|
||||
事件追踪会区分位置替换与日志中记录的来源关系。除 `replacementChain` 外,每个 seq 列表都只包含直接链接;该链从目标沿直接 replacer 追踪到最终的位置替换。
|
||||
|
||||
```ts type-equiv
|
||||
/** Request for direct surface and provenance relationships around one event. */
|
||||
@@ -338,6 +338,7 @@ interface SessionEventTraceObservation extends SessionEventTrace {
|
||||
/** Stable machine-routable failure taxonomy for session reads, traces, and search. */
|
||||
type SessionQueryErrorCode =
|
||||
| 'SESSION_QUERY_ABORTED'
|
||||
| 'SESSION_QUERY_CORRUPT_SESSION'
|
||||
| 'SESSION_QUERY_EVENT_NOT_FOUND'
|
||||
| 'SESSION_QUERY_INDEX_FAILED'
|
||||
| 'SESSION_QUERY_INVALID_CONFIG'
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/session-reference.md
|
||||
session-reference.md: 5375677f6a1748909743ca76d5191cb9e736a40a
|
||||
session-reference.zh.md: 8e9abea7ce87e51061813d282e20db951918a650
|
||||
session-reference.zh.md: 3ff4a1719926bda0a9111482a7778a8c94553370
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](session-reference.md) | 中文
|
||||
|
||||
结构化的跨会话引用请求与预备消息上下文。[包(package)契约](../../packages/context/session-reference) 负责规范 URI、当前表层投影、标签安全的 JSON 与字节保留、稳定错误和不可信的模型提示词。宿主适配器使用这些类型,而不会把各自 UI 的提及语法传入 agent(智能体)核心。
|
||||
结构化的跨会话引用请求与准备后的消息上下文。[包契约](../../packages/context/session-reference) 负责规范 URI、当前表层投影、标签安全的 JSON 与字节保留、稳定错误和不可信的模型提示词。宿主适配器使用这些类型,而不会把各自 UI 的提及语法传入 agent(智能体)核心。
|
||||
|
||||
来源:[`packages/context/session-reference/src/types.ts`](../../packages/context/session-reference/src/types.ts)
|
||||
|
||||
@@ -36,9 +36,9 @@ interface SessionReferenceCandidate {
|
||||
}
|
||||
```
|
||||
|
||||
## 预备消息
|
||||
## 准备后的消息
|
||||
|
||||
预备过程保留可读的当前消息内容,并最多返回一个聚合上下文。
|
||||
准备过程保留可读的当前消息内容,并最多返回一个聚合上下文。
|
||||
|
||||
```ts type-equiv
|
||||
/** Direct message content and optional referenced-session context. */
|
||||
@@ -52,7 +52,7 @@ interface PreparedReferencedMessage {
|
||||
|
||||
## 错误
|
||||
|
||||
`SessionReferenceError.code` 区分无效配置或输入、自引用、数量限制、源读取失败、预算失败和取消。宿主协议会把这些 code 映射到各自的错误信封,无需检查提示词字节。
|
||||
`SessionReferenceError.code` 区分无效配置或输入、自引用、数量限制、源读取失败、预算失败和取消。宿主协议会把这些 code 映射到各自的错误封装,无需检查提示词字节。
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable failure codes exposed to host adapters. */
|
||||
|
||||
@@ -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 docs/core-data-structures/session-title.md
|
||||
session-title.md: 33efc911c0ca1ae94dc4ded74676e5c32a73bdd5
|
||||
session-title.zh.md: a4b95a726d2bc89a13d14f1daa2f825cd5aa91b1
|
||||
session-title.md: fff1aa1f6be45d0cfc4d7f6a9527ccb93561618f
|
||||
session-title.zh.md: 77a0a5e94053c94bf84bbc749cb6e260898b5d00
|
||||
|
||||
@@ -34,6 +34,10 @@ type SessionTitleSource =
|
||||
readonly provider: SessionTitleProviderId
|
||||
readonly model?: SessionTitleModelProvenance
|
||||
}
|
||||
| {
|
||||
/** Explicit user rename: pins the title — automatic generation stops scheduling. */
|
||||
readonly kind: 'user'
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
@@ -41,9 +45,9 @@ type SessionTitleSource =
|
||||
interface SessionTitleEventData {
|
||||
/** Normalized non-empty title text. */
|
||||
readonly title: string
|
||||
/** Exact human `user/message` seqs used to derive this title. */
|
||||
/** Exact human `user/message` seqs used to derive this title; empty for an explicit user rename. */
|
||||
readonly messageSeqs: number[]
|
||||
/** Built-in fallback or registered-provider provenance. */
|
||||
/** Built-in fallback, registered-provider, or explicit-user provenance. */
|
||||
readonly source: SessionTitleSource
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](session-title.md) | 中文
|
||||
|
||||
[`@deepseek-ai/dsh-session-title`](../../packages/session-title/session-title) 所拥有的持久化后写覆盖标题状态与可选异步提供方词汇。共享 LLM(大语言模型)辅助组件负责精确的辅助请求记录。各包(package)README 负责时序、回退、失败与 fork 行为;生成的[持久化日志事件目录](../persistence-catalog.md)负责完整的事件声明。
|
||||
[`@deepseek-ai/dsh-session-title`](../../packages/session-title/session-title) 所拥有的持久、后写覆盖的标题状态与可选异步提供方词汇。共享 LLM(大语言模型)辅助组件负责精确的辅助请求记录。各包 README 负责时序、回退、失败与 fork 行为;生成的[持久化日志事件目录](../persistence-catalog.md)负责完整的事件声明。
|
||||
|
||||
源码:[`packages/session-title/session-title/src/index.ts`](../../packages/session-title/session-title/src/index.ts)、[`packages/session-title/session-title-llm/src/index.ts`](../../packages/session-title/session-title-llm/src/index.ts)
|
||||
|
||||
## 持久标题状态
|
||||
|
||||
提供方生成修订时会记录 `SessionTitleProviderId`。`SessionTitleEventData` 携带精确的人类消息来源信息,`SessionTitleSnapshot` 则加入 `foldSessionTitle()` 选出的持久事件信封事实。
|
||||
提供方生成修订时会记录 `SessionTitleProviderId`。`SessionTitleEventData` 携带精确的人类消息来源信息,`SessionTitleSnapshot` 则加入 `foldSessionTitle()` 选出的持久事件封装信息。
|
||||
|
||||
```ts type-equiv
|
||||
/** Identifies one session-title provider registration. */
|
||||
@@ -34,6 +34,10 @@ type SessionTitleSource =
|
||||
readonly provider: SessionTitleProviderId
|
||||
readonly model?: SessionTitleModelProvenance
|
||||
}
|
||||
| {
|
||||
/** Explicit user rename: pins the title — automatic generation stops scheduling. */
|
||||
readonly kind: 'user'
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
@@ -41,9 +45,9 @@ type SessionTitleSource =
|
||||
interface SessionTitleEventData {
|
||||
/** Normalized non-empty title text. */
|
||||
readonly title: string
|
||||
/** Exact human `user/message` seqs used to derive this title. */
|
||||
/** Exact human `user/message` seqs used to derive this title; empty for an explicit user rename. */
|
||||
readonly messageSeqs: number[]
|
||||
/** Built-in fallback or registered-provider provenance. */
|
||||
/** Built-in fallback, registered-provider, or explicit-user provenance. */
|
||||
readonly source: SessionTitleSource
|
||||
}
|
||||
```
|
||||
@@ -82,7 +86,7 @@ interface SessionTitleLlmRequestEventData {
|
||||
|
||||
## 提供方输入与输出
|
||||
|
||||
服务会对截至某一修订的合格消息创建快照。提供方返回的 seq 仅可来自该请求;由服务负责的接受过程会验证顺序、规范化标题、强制执行字节上限并追加来源信息。
|
||||
服务会对截至某一修订的合格消息创建快照。提供方返回的 seq 仅可来自该请求;由服务负责的接纳流程会验证顺序、规范化标题、强制执行字节上限并追加来源信息。
|
||||
|
||||
```ts type-equiv
|
||||
/** One eligible human text message exposed to title providers. */
|
||||
|
||||
@@ -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 docs/core-data-structures/session.md
|
||||
session.md: 6ae0ab79b5c7bc3bc1859bf819ce25679672a7f0
|
||||
session.zh.md: 79ed40f7eee7a8cae05a366d646f85580c73d5d2
|
||||
session.md: fac201b581e395865fd46d51bca1350cbbc46e8e
|
||||
session.zh.md: 53dc9d11de895deec72aaf5ea81c70ba87c9c6bd
|
||||
|
||||
@@ -26,14 +26,19 @@ interface UserMessage extends Message {
|
||||
*/
|
||||
interface SessionEventMap {
|
||||
/**
|
||||
* Opens turn `turn`. `trigger` records what started the model loop.
|
||||
* Opens turn `turn` before the loop claims queued input or runs pre-step.
|
||||
* Rejection, empty input, cancellation, or failure may close it with no
|
||||
* step; otherwise the following identified `user/message` event or batch
|
||||
* records the messages entering the step.
|
||||
*/
|
||||
'turn/start': { turn: number; trigger: TurnTrigger }
|
||||
'turn/start': { turn: number }
|
||||
/**
|
||||
* Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop
|
||||
* 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.
|
||||
* Closes turn `turn` with the {@link TurnEndReason} that ended it. A turn
|
||||
* with no entered step has no `step/start` or `step/end`. The loop does not await a
|
||||
* flush at turn boundaries: `dsh-session-checkpoint-policy` owns the
|
||||
* per-request durability checkpoint, and consumers that read storage after
|
||||
* `whenIdle()` flush themselves. 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. */
|
||||
@@ -44,9 +49,8 @@ interface SessionEventMap {
|
||||
* A user-role message on the model-visible surface: a direct human prompt
|
||||
* (the queued message claimed for this turn), a synthetic `agent.inject()`
|
||||
* context (file-change notices, subdir AGENTS.md, skill content, cron
|
||||
* notifications, …), or an admitted goal continuation round. All three
|
||||
* project their `content` verbatim; `source` tells them apart. An idle
|
||||
* injection may append this event between turns without running the model.
|
||||
* notifications, …), or an entered goal continuation round. All three
|
||||
* project their `content` verbatim; `source` tells them apart.
|
||||
*/
|
||||
'user/message': UserMessage
|
||||
/** Raw stream chunk — token-level replay fidelity. */
|
||||
@@ -82,8 +86,6 @@ interface SessionEventMap {
|
||||
error?: { name: string; code: string }
|
||||
meta?: JsonValue
|
||||
}
|
||||
/** Steering content injected between steps of a running turn. */
|
||||
'steering/message': { turn: number; message: UserMessage }
|
||||
/** Whole-list snapshot; latest write wins on replay. Log-only UI state; never derived history. */
|
||||
'todo/write': { todos: TodoItem[] }
|
||||
/**
|
||||
@@ -91,6 +93,34 @@ interface SessionEventMap {
|
||||
* It is log-only; the latest snapshot reconstructs the request header.
|
||||
*/
|
||||
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
|
||||
/**
|
||||
* Route metadata for the next request, logged only when the route or capacity
|
||||
* changes. It does not participate in request reconstruction or header equality.
|
||||
*/
|
||||
'request/context': RequestContext
|
||||
/**
|
||||
* Marks the end of a constructor seed. Events before it have smaller seq
|
||||
* values and came from the seed (resume, fork, or replay); this lifecycle
|
||||
* produced none of them. This log-only event is the durable projection of
|
||||
* {@link Session.firstLiveSeq}. Its payload is empty — position and `time`
|
||||
* carry the meaning.
|
||||
*
|
||||
* Locate the LAST one in stored history. A seed already ending in one is not
|
||||
* re-marked, so reopening an untouched session does not grow its log per
|
||||
* pickup and the event need not be at the current `firstLiveSeq`.
|
||||
*
|
||||
* `Session`'s constructor is the only legitimate writer. The invariant
|
||||
* companion deliberately constrains nothing here, so a plugin appending one
|
||||
* would silently classify every live bracket before it as seed history.
|
||||
*
|
||||
* An owner of a standalone open/close bracket (`compact/start` …
|
||||
* `compact/end`) reads it because seed history and live work are otherwise
|
||||
* byte-identical: an unmatched opening marker before this event belongs to
|
||||
* an ended lifecycle, whatever ended it. NOT a liveness signal about other
|
||||
* writers — a concurrently live session holds its own boundary elsewhere,
|
||||
* so tolerating concurrent writers needs a signal beyond the log.
|
||||
*/
|
||||
'session/end-seed': Record<string, never>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -121,7 +151,7 @@ interface TodoItem {
|
||||
|
||||
### The request header event: `request/header`
|
||||
|
||||
The request envelope — the `EpochHeader` (call config + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message.
|
||||
The request envelope — the `EpochHeader` (call config + adapter-default provenance + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -132,6 +162,8 @@ The request envelope — the `EpochHeader` (call config + rendered system prompt
|
||||
interface EpochHeader {
|
||||
/** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
|
||||
config: LlmCallConfig
|
||||
/** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
|
||||
adapterDefaults?: LlmCallConfigAdapterDefaults
|
||||
/** Rendered system prompt text; absent for a system-less request. */
|
||||
system?: string
|
||||
/** Assembled tool schemas; absent for a tool-less request. */
|
||||
@@ -141,6 +173,22 @@ interface EpochHeader {
|
||||
|
||||
Canonical form represents an empty system prompt or tool list as an absent field, matching how requests are built. Legacy v0 logs containing the removed `request/header-delta` event or its full-snapshot `fallback` reason are rejected at seed, append, and persistence-load boundaries rather than replayed incompletely.
|
||||
|
||||
### The route capacity event: `request/context`
|
||||
|
||||
The context metadata of the route a request resolved to is separate logged state, appended beside `request/header` inside the same step and only when the provider, model, or capacity differs from the previous record. It stays outside `EpochHeader` because that type is the reconstruction contract compared field-wise by `headerEquals`: capacity describes a route, not a request input, so folding it in would let a capacity change register as a request-envelope `change` and would pull adapter metadata into the loop's reconstruction invariant. Like `request/header`, it is not a `SurfaceEventType` and produces no LLM message. `session.requestContext()` folds the latest record incrementally. A route whose adapter advertises no capacity is recorded with `contextWindow` absent, so the new record clears an older route's capacity.
|
||||
|
||||
```ts type-equiv
|
||||
/** Registration-bound metadata for one resolved model route. */
|
||||
interface RequestContext {
|
||||
/** Registered provider route the metadata belongs to. */
|
||||
provider: string
|
||||
/** Provider-owned model id the metadata belongs to. */
|
||||
model: string
|
||||
/** Maximum combined request and response context in tokens, when advertised. */
|
||||
contextWindow?: number
|
||||
}
|
||||
```
|
||||
|
||||
## `SessionEvent<T>` — one log entry
|
||||
|
||||
A proper discriminated union over `type` (not independent `type`/`data` unions), so `switch (event.type)` narrows `event.data` without casts. `seq` is the monotonic position in the log (`seq = log.length`); `time` is epoch ms.
|
||||
@@ -154,7 +202,7 @@ A proper discriminated union over `type` (not independent `type`/`data` unions),
|
||||
*
|
||||
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
|
||||
* they only exist on {@link SurfaceEventType} variants (`user/message`,
|
||||
* `assistant/message`, `tool/result`, `steering/message`).
|
||||
* `assistant/message`, `tool/result`).
|
||||
* Non-surface events (boundary markers, chunks, usage, errors) never carry
|
||||
* surface metadata — the compiler enforces this at `Session.append()`
|
||||
* call sites.
|
||||
@@ -188,7 +236,7 @@ For `assistant/message`, a present `sourceEventSeqs: []` is a complete known-emp
|
||||
|
||||
## Surface types
|
||||
|
||||
The four message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`, `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).
|
||||
The three message-producing types (`SurfaceEventType` — `user/message`, `assistant/message`, `tool/result`) 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
|
||||
|
||||
@@ -202,7 +250,6 @@ type SurfaceEventType =
|
||||
| 'user/message'
|
||||
| 'assistant/message'
|
||||
| 'tool/result'
|
||||
| 'steering/message'
|
||||
```
|
||||
|
||||
### `SurfaceOp` — how an event entered the surface
|
||||
@@ -212,7 +259,7 @@ type SurfaceEventType =
|
||||
* How a session event entered the ordered surface. Only valid on
|
||||
* {@link SurfaceEventType} events.
|
||||
*
|
||||
* - `'append'`: added to the tail — normal path for user/assistant/tool/steering
|
||||
* - `'append'`: added to the tail — normal path for user/assistant/tool
|
||||
* messages.
|
||||
* - `{ op: 'replace', start, end }`: replaces surface nodes from `start`
|
||||
* (inclusive) through `end` (inclusive) with this node. Both must exist as
|
||||
@@ -246,7 +293,7 @@ interface SurfaceIntent {
|
||||
}
|
||||
```
|
||||
|
||||
Required for `SurfaceEventType` events — every message-producing event must declare how it joins the surface, the sole source of derived history. Non-surface types reject it at compile time.
|
||||
Required for `SurfaceEventType` events — every message-producing event must declare how it joins the surface, the sole source of derived model history. A human-facing transcript is the other projection and reads the log's append-origin events instead, because the surface deliberately shadows the ranges a replacement summarizes (`isAppendSurfaceEvent` in [dsh-session](../../packages/core/session/README.md)). Non-surface types reject it at compile time.
|
||||
|
||||
The same provenance distinction applies here: only `assistant/message` may carry a present empty `sourceEventSeqs`; omission does not assert that its source stream was empty.
|
||||
|
||||
@@ -254,6 +301,8 @@ The same provenance distinction applies here: only `assistant/message` may carry
|
||||
|
||||
`Session.surface` returns the session's stable `SessionSurface` view. The same incremental manager validates append candidates before commit and advances this projection from committed events; callers can observe membership and replacement generation but cannot invoke validation.
|
||||
|
||||
`SurfaceManager(log, baseSeq?)` can instead fold a contiguous loaded window whose first event has the absolute sequence `baseSeq`. Every event remains contiguous in that absolute sequence space, and a replacement that crosses the window head fails because its declared range is absent.
|
||||
|
||||
```ts type-equiv
|
||||
/** Readonly live projection of the message-producing session events. */
|
||||
interface SessionSurface {
|
||||
@@ -294,14 +343,16 @@ interface SurfaceFoldResult {
|
||||
|
||||
## `Session` public API
|
||||
|
||||
The body-stripped declaration keeps the plain class's public constructor, state accessors, append boundary, and history projections synchronized with source. Store operations remain in the generated [`ctx.sessions` service catalog](../cordis-catalog/services.md#ctxsessions--sessionstore).
|
||||
The body-stripped declaration keeps the plain class's detached factory, state accessors, append boundary, and history projections synchronized with source. Store operations remain in the generated [`ctx.sessions` service catalog](../cordis-catalog/services.md#ctxsessions--sessionstore).
|
||||
|
||||
```ts public-api
|
||||
/**
|
||||
* An event-sourced session: an append-only log of {@link SessionEvent}s.
|
||||
*
|
||||
* Plain class (not a Service) — create instances via `ctx.sessions.create()`.
|
||||
* Plain class (not a Service) — create live instances via
|
||||
* `ctx.sessions.create()` and detached instances via {@link create}.
|
||||
* Seeding with an existing event log replays/forks a session.
|
||||
* @typert object
|
||||
*/
|
||||
declare class Session {
|
||||
/** The ordered surface over this session's event log. */
|
||||
@@ -309,7 +360,7 @@ declare class Session {
|
||||
/**
|
||||
* Detached, deep-frozen creation metadata (format version, cwd, lineage,
|
||||
* seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
|
||||
* `Session` is constructed bare (tests, ad-hoc replay), a minimal header is
|
||||
* `Session` is created without a store-owned header, a minimal header is
|
||||
* synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
|
||||
* `session.header` is always present. Kept out of the event log — it is a
|
||||
* storage concern, not replayable conversation state.
|
||||
@@ -319,17 +370,46 @@ declare class Session {
|
||||
get id(): SessionId;
|
||||
/**
|
||||
* The first seq appended IN THIS PROCESS: the length of the constructor
|
||||
* seed (0 without one). Events below it entered through construction —
|
||||
* replay, fork, or resume — and were never published on the `session/event`
|
||||
* firehose (constructor seeds do not emit), so consumers that replay the
|
||||
* log as a publication substitute (telemetry adoption) start here. Distinct
|
||||
* from `header.seedLength`, the DURABLE fork-lineage boundary: a resumed
|
||||
* session's constructor seed is its full stored log, while its header keeps
|
||||
* the original fork value — this field is the in-process construction fact
|
||||
* and is deliberately not persisted.
|
||||
* seed (0 without one). Events with smaller seq values entered through
|
||||
* construction — replay, fork, or resume — and were never published on the
|
||||
* `session/event` firehose (constructor seeds do not emit), so consumers
|
||||
* that replay the log as a publication substitute (telemetry adoption)
|
||||
* start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
|
||||
* boundary: a resumed session's constructor seed is its full stored log,
|
||||
* while its header keeps the original fork value — this field is the
|
||||
* in-process construction fact.
|
||||
*
|
||||
* Not persisted itself: a seeded session projects it into the log as the
|
||||
* `session/end-seed` event, which is what a consumer reading STORED history
|
||||
* reads. Locate the LAST such event, not necessarily one at this seq — a
|
||||
* seed already ending in one is not re-marked, so reopening an untouched
|
||||
* session leaves that event at a smaller seq than `firstLiveSeq`. Prefer
|
||||
* this field in-process: it is exact before the marker reaches storage.
|
||||
*
|
||||
* When this lifecycle appends the marker, it occupies this seq before the
|
||||
* store attaches and therefore does not publish either. Otherwise this seq
|
||||
* holds an ordinary published write.
|
||||
*/
|
||||
readonly firstLiveSeq: number;
|
||||
constructor(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader);
|
||||
/**
|
||||
* Create a detached session by validating and snapshotting borrowed seed
|
||||
* events and storage metadata.
|
||||
* @param id - session identity.
|
||||
* @param seed - optional borrowed replay or fork events.
|
||||
* @param header - optional borrowed storage metadata.
|
||||
* @returns a detached session.
|
||||
*/
|
||||
static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
|
||||
/**
|
||||
* Restore a detached session by taking ownership of fresh persistence values.
|
||||
* Storage shape, event envelopes, sequence continuity, surface transitions,
|
||||
* and header fields are validated before the graphs are frozen in place.
|
||||
* @param id - restored session identity.
|
||||
* @param seed - fresh detached events whose ownership is transferred.
|
||||
* @param header - fresh detached metadata whose ownership is transferred.
|
||||
* @returns a restored detached session.
|
||||
*/
|
||||
static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
|
||||
/**
|
||||
* An immutable snapshot of the append-only event log. The snapshot is reused
|
||||
* until the next append; a previously returned array does not grow later.
|
||||
@@ -353,7 +433,8 @@ declare class Session {
|
||||
* the ordered surface; `sourceEventSeqs` records provenance (the seq
|
||||
* numbers of events this one derives from). REQUIRED for
|
||||
* {@link SurfaceEventType} events (every message-producing event must
|
||||
* declare how it joins the surface, the sole source of derived history) and
|
||||
* declare how it joins the surface, the sole source of derived model
|
||||
* history) and
|
||||
* rejected by the compiler for non-surface types like `turn/start` or
|
||||
* `assistant/chunk`.
|
||||
* @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
|
||||
@@ -387,6 +468,12 @@ declare class Session {
|
||||
* @returns the folded header, or undefined when no header event exists yet.
|
||||
*/
|
||||
requestHeader(): EpochHeader | undefined;
|
||||
/**
|
||||
* Return the latest resolved route metadata, or `undefined` before the first
|
||||
* `request/context` event. Each event is folded once.
|
||||
* @returns the latest immutable route metadata.
|
||||
*/
|
||||
requestContext(): RequestContext | undefined;
|
||||
/**
|
||||
* Derive the LLM message history by walking the ordered sequences of
|
||||
* message-producing events maintained by `surfaceOp` markers. The
|
||||
@@ -407,15 +494,8 @@ declare class Session {
|
||||
*/
|
||||
deriveMessages(): Message[];
|
||||
/**
|
||||
* Project a single event into the LLM message it derives to, or null when
|
||||
* it produces none — a non-surface event (chunk, boundary, log-only record)
|
||||
* or an empty-content assistant/message (which exists only to host usage).
|
||||
* 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 Agent Note). The returned message is
|
||||
* the already frozen message nested in the event wrapper and shared by
|
||||
* delivery, durable history, and model requests.
|
||||
* Instance face of the pure per-node `deriveEventMessage` export from
|
||||
* `surface.ts`.
|
||||
* @param event - the event to project.
|
||||
* @returns the derived message, or null when the event produces none.
|
||||
*/
|
||||
@@ -431,9 +511,8 @@ declare class Session {
|
||||
- `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.
|
||||
- `user/message` (injected context, i.e. non-`user` source) → a user-role message carrying its `content` verbatim at its chronological position; provenance and domain data live in its typed source.
|
||||
- `steering/message` → a user-role message carrying exact `content` at its chronological position; an optional envelope remains log-only display metadata.
|
||||
|
||||
Everything else (`turn/*`, `step/*`, plugin-owned `llm/retry`) is structural and does not project into a message. Token accounting reads per-step `assistant/chunk { type: 'usage' }` records and treats `assistant/message.usage` as the committed-step fallback when no usage chunk exists; failed model-request attempts have no assistant message, so their usage chunk is the durable accounting record. An operational error's step number is on `turn/end.reason` for `kind: 'error'`, with normalized `LlmFailure` facts for a final model-request failure and message/code for other live errors. 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.
|
||||
Everything else (`turn/*`, `step/*`, plugin-owned `llm/retry`) is structural and does not project into a message. Token accounting reads per-step `assistant/chunk { type: 'usage' }` records and treats `assistant/message.usage` as the committed-step fallback when no usage chunk exists; failed model-request attempts have no assistant message, so their usage chunk is the durable accounting record. 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.
|
||||
|
||||
## Live-session fork API
|
||||
|
||||
@@ -443,29 +522,14 @@ Everything else (`turn/*`, `step/*`, plugin-owned `llm/retry`) is structural and
|
||||
|
||||
An explicit `boundary` lets callers fork from any stable between-turn position, including a previous `turn/end` or a later standalone log-only event, even if the source has newer events or an open current turn. The API rejects a prefix that ends inside an open turn instead of clipping silently. Broader execution-relation sanity stays in the existing `dsh-invariants` plugin and persistence repair path rather than being duplicated in `fork()`. `dsh-subagent-fork` keeps its completed-prefix clipping because tool-time delegation usually starts while the parent turn is open; ordinary session branching should make the requested boundary explicit.
|
||||
|
||||
## What started a turn: `TurnTriggerMap`
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* What started a turn.
|
||||
* Merge-extensible sum type (same pattern as MessageSourceMap).
|
||||
*/
|
||||
interface TurnTriggerMap {
|
||||
message: { kind: 'message'; source: MessageSource }
|
||||
/** Recovery turn reopened over the repaired current session log. */
|
||||
retry: { kind: 'retry' }
|
||||
/**
|
||||
* An out-of-band producer explicitly enclosed injected context in a one-shot
|
||||
* turn. `Agent.inject()` appends idle context directly and does not use this
|
||||
* trigger; the source mirrors the producer of the enclosed `user/message`.
|
||||
*/
|
||||
injection: { kind: 'injection'; source: MessageSource }
|
||||
}
|
||||
```
|
||||
|
||||
## Why a turn ended: `TurnEndReasonMap`
|
||||
|
||||
`aborted` is intentionally a coarse durable outcome: it records that cancellation interrupted the live turn, not which runtime caller requested it. The runtime-only caller vocabulary belongs to [`AgentCancelCause`](core.md#the-agent-handle); a future audit requirement would use a separate control-request event rather than overloading the terminal result.
|
||||
`turn/start` has no trigger field. The entered `user/message` batch records what entered each step, `llm/retry` records request recovery, and idle injection remains pending until a waking delivery reaches a later pre-step. Live turns retain the typed [`AgentCancelCause`](core.md#the-agent-handle) that stopped the driver; persistence uses the additional `{ kind: 'legacy' }` cause only when importing a supported coarse cancellation record that did not store its caller.
|
||||
|
||||
```ts type-equiv
|
||||
/** Durable cancellation cause, including imports whose original coarse record carried no cause. */
|
||||
type TurnEndCancelCause = AgentCancelCause | { readonly kind: 'legacy' }
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -474,20 +538,15 @@ interface TurnTriggerMap {
|
||||
interface TurnEndReasonMap {
|
||||
completed: { kind: 'completed' }
|
||||
/** A cancellation request interrupted the live turn. */
|
||||
aborted: { kind: 'aborted' }
|
||||
aborted: { kind: 'aborted'; reason: TurnEndCancelCause }
|
||||
|
||||
blocked: { kind: 'blocked' }
|
||||
/**
|
||||
* The turn failed: a step threw or the model reported a failure. `step` is the
|
||||
* step number the failure occurred on (the operational error's location — the
|
||||
* single durable record of an in-turn failure; live diagnostics also fire via
|
||||
* `agent/error`). Final model-request failures retain their normalized facts
|
||||
* as one `failure`; other thrown values retain their rendered message and a
|
||||
* real `HarnessError` code when present.
|
||||
* The turn failed. `error` is always a structured failure: the `LlmError`
|
||||
* facts verbatim, or `{ message: errorChain(error), code: 'UNKNOWN' }`
|
||||
* flattened from any other error.
|
||||
*/
|
||||
error: { kind: 'error'; step: number } & (
|
||||
| { failure: LlmFailure; message?: never; code?: never }
|
||||
| { message: string; code?: string; failure?: never }
|
||||
)
|
||||
disposed: { kind: 'disposed' }
|
||||
error: { kind: 'error'; error: LlmFailure }
|
||||
/** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
|
||||
'max-tokens': { kind: 'max-tokens' }
|
||||
/**
|
||||
@@ -498,19 +557,29 @@ 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. `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. Cancellation and errors remain distinct outcomes. `interrupted` is the one reason no loop emits—it is synthesized by crash recovery (see [persistence.md](persistence.md)). The map is merge-extensible.
|
||||
|
||||
## Execution enclosure and standalone events
|
||||
|
||||
A turn encloses one model-loop execution, not the whole session log. Idle injected `user/message` events and plugin-owned log-only events may appear between `turn/end` and the next `turn/start`; they consume event seqs without incrementing turn numbers. Persistence eagerly records every contiguous accepted event, while crash repair closes only a genuinely open trailing turn. A producer that needs a durability barrier explicitly awaits `ctx.sessions.flush(session)`.
|
||||
A turn encloses one model-loop execution, not the whole session log. AgentLoop records injected `user/message` events only from entering pre-step batches inside a turn; plugin-owned log-only events may still appear between `turn/end` and the next `turn/start`, consuming event seqs without incrementing turn numbers. Persistence eagerly records every contiguous accepted event, while crash repair closes only a genuinely open trailing turn. A producer that needs a durability barrier explicitly awaits `ctx.sessions.flush(session)`.
|
||||
|
||||
The optional `dsh-session/invariant` companion enforces the relations owned by core: turn and step numbering, execution-event enclosure, and same-step tool call/result pairing. Merge-extensible event relations belong to the plugin that declares them, so core does not reject an unknown event merely because no turn is open. See [the standalone-event decision](../../.agents/notes/implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.md).
|
||||
|
||||
## The end-seed boundary: `session/end-seed`
|
||||
|
||||
A seeded session — resume, fork, or replay — appends this log-only event immediately after its constructor seed, as its first live write. Events before it have smaller seq values and came from the seed. It is the durable projection of `firstLiveSeq`: that field answers where this lifecycle's writes start for a consumer holding the object, while the event answers the same question for one holding only stored bytes. The payload is empty, so position and `time` carry the whole meaning, and it produces no message. `Session`'s constructor is the only legitimate writer.
|
||||
|
||||
An explicitly supplied empty seed writes `session/end-seed` at seq 0, which distinguishes an empty resumed session from a fresh one. A seed already ending in `session/end-seed` is not re-marked, so reopening an untouched session does not grow its log per pickup. Locate the LAST `session/end-seed` in stored history rather than assuming one exists at `firstLiveSeq`: after a pickup with no work, the event has a smaller seq than the next lifecycle's `firstLiveSeq`.
|
||||
|
||||
It exists because seed history and live work are otherwise byte-identical, which defeats any plugin owning a standalone open/close bracket: an unmatched `compact/start` reads the same whether the writer crashed mid-compaction or is compacting right now. An opening marker before `session/end-seed` came from the constructor seed and belongs to an ended lifecycle, whatever ended it (a crash, a succeeding process, or a fork out of a still-running parent), so its owner may treat it as dead. That covers only brackets *this* session inherited: a concurrently live session holding an open bracket over the same history has its own boundary elsewhere, so tolerating concurrent writers needs a liveness signal beyond the log. Core writes the boundary and reads nothing from it — a bracket's vocabulary stays with its owning plugin, which is why crash repair closes turn/step/tool boundaries and never `compact/*`.
|
||||
|
||||
Activity ordering excludes the boundary through `lastActivityTime(events)`: picking a session up is not work, and lazy resume means browsing writes one, so a resume picker or session list ordering by log tail would float every opened session to the top.
|
||||
|
||||
## 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). Their owner decides whether they belong to an open execution turn or may stand between turns, and enforces any relation in its own invariant companion. 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`/`Stop`) fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` and the pre-turn `UserPromptSubmit` admission seam get no `hook/*` record because neither has an open turn to enclose one; allowed context is instead evidenced by its sourced `user/message` (see [the hook-bridges Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md)).
|
||||
The hook bridges' `hook/invoked` / `hook/result` provenance pairs (from `@deepseek-ai/dsh-hook-protocol`) correlate by `handlerId`. `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, and `Stop` fire inside the loop's open turn, so their `hook/*` records are turn-enclosed by construction. `SessionStart` gets no `hook/*` record because it runs before turn 1; its context remains pending in the inbox until a waking delivery opens a turn (see [the hook-bridges Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md)).
|
||||
|
||||
## Durability contract
|
||||
|
||||
|
||||
@@ -26,14 +26,19 @@ interface UserMessage extends Message {
|
||||
*/
|
||||
interface SessionEventMap {
|
||||
/**
|
||||
* Opens turn `turn`. `trigger` records what started the model loop.
|
||||
* Opens turn `turn` before the loop claims queued input or runs pre-step.
|
||||
* Rejection, empty input, cancellation, or failure may close it with no
|
||||
* step; otherwise the following identified `user/message` event or batch
|
||||
* records the messages entering the step.
|
||||
*/
|
||||
'turn/start': { turn: number; trigger: TurnTrigger }
|
||||
'turn/start': { turn: number }
|
||||
/**
|
||||
* Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop
|
||||
* 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.
|
||||
* Closes turn `turn` with the {@link TurnEndReason} that ended it. A turn
|
||||
* with no entered step has no `step/start` or `step/end`. The loop does not await a
|
||||
* flush at turn boundaries: `dsh-session-checkpoint-policy` owns the
|
||||
* per-request durability checkpoint, and consumers that read storage after
|
||||
* `whenIdle()` flush themselves. 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. */
|
||||
@@ -44,9 +49,8 @@ interface SessionEventMap {
|
||||
* A user-role message on the model-visible surface: a direct human prompt
|
||||
* (the queued message claimed for this turn), a synthetic `agent.inject()`
|
||||
* context (file-change notices, subdir AGENTS.md, skill content, cron
|
||||
* notifications, …), or an admitted goal continuation round. All three
|
||||
* project their `content` verbatim; `source` tells them apart. An idle
|
||||
* injection may append this event between turns without running the model.
|
||||
* notifications, …), or an entered goal continuation round. All three
|
||||
* project their `content` verbatim; `source` tells them apart.
|
||||
*/
|
||||
'user/message': UserMessage
|
||||
/** Raw stream chunk — token-level replay fidelity. */
|
||||
@@ -82,8 +86,6 @@ interface SessionEventMap {
|
||||
error?: { name: string; code: string }
|
||||
meta?: JsonValue
|
||||
}
|
||||
/** Steering content injected between steps of a running turn. */
|
||||
'steering/message': { turn: number; message: UserMessage }
|
||||
/** Whole-list snapshot; latest write wins on replay. Log-only UI state; never derived history. */
|
||||
'todo/write': { todos: TodoItem[] }
|
||||
/**
|
||||
@@ -91,6 +93,34 @@ interface SessionEventMap {
|
||||
* It is log-only; the latest snapshot reconstructs the request header.
|
||||
*/
|
||||
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
|
||||
/**
|
||||
* Route metadata for the next request, logged only when the route or capacity
|
||||
* changes. It does not participate in request reconstruction or header equality.
|
||||
*/
|
||||
'request/context': RequestContext
|
||||
/**
|
||||
* Marks the end of a constructor seed. Events before it have smaller seq
|
||||
* values and came from the seed (resume, fork, or replay); this lifecycle
|
||||
* produced none of them. This log-only event is the durable projection of
|
||||
* {@link Session.firstLiveSeq}. Its payload is empty — position and `time`
|
||||
* carry the meaning.
|
||||
*
|
||||
* Locate the LAST one in stored history. A seed already ending in one is not
|
||||
* re-marked, so reopening an untouched session does not grow its log per
|
||||
* pickup and the event need not be at the current `firstLiveSeq`.
|
||||
*
|
||||
* `Session`'s constructor is the only legitimate writer. The invariant
|
||||
* companion deliberately constrains nothing here, so a plugin appending one
|
||||
* would silently classify every live bracket before it as seed history.
|
||||
*
|
||||
* An owner of a standalone open/close bracket (`compact/start` …
|
||||
* `compact/end`) reads it because seed history and live work are otherwise
|
||||
* byte-identical: an unmatched opening marker before this event belongs to
|
||||
* an ended lifecycle, whatever ended it. NOT a liveness signal about other
|
||||
* writers — a concurrently live session holds its own boundary elsewhere,
|
||||
* so tolerating concurrent writers needs a signal beyond the log.
|
||||
*/
|
||||
'session/end-seed': Record<string, never>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -98,7 +128,7 @@ interface SessionEventMap {
|
||||
|
||||
### `TodoItem`:一条待办项
|
||||
|
||||
这是 `todo/write` 事件全量列表快照中的单元。它有意保持精简:一行 `content` 加一个三态 `status`(没有 id、优先级或 `activeForm`);列表在每次写入时整体替换,因此条目无需稳定标识。见 [todo_write Agent Note(agent 决策记录)](../../.agents/notes/implemented/feature/2026-06-29-todo-write-tool.md)。
|
||||
这是 `todo/write` 事件全量列表快照中的单元。它有意保持精简:一行 `content` 加一个三态 `status`(没有 id、优先级或 `activeForm`);列表在每次写入时整体替换,因此条目无需稳定标识。见 [todo_write Agent Note](../../.agents/notes/implemented/feature/2026-06-29-todo-write-tool.md)。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -123,7 +153,7 @@ interface TodoItem {
|
||||
|
||||
### 请求头事件:`request/header`
|
||||
|
||||
请求信封(即 `EpochHeader`:调用配置 + 渲染后的系统提示词 + 已组装的工具 schema)会作为会话状态写入日志,因此每个对话请求都是日志的纯函数(见可重建性 Agent Note)。带有 reason `'initial'` 或 `'resume'` 的完整 `request/header` 快照记录每个 agent loop 实例的边界;之后请求发生变化时,系统会以 reason `'change'` 记录另一份完整快照。`foldRequestHeader(events)` 通过选择最新快照重建请求头。该事件不是 `SurfaceEventType`,不产生 LLM 消息。
|
||||
请求信封(即 `EpochHeader`:调用配置 + 适配器默认值来源 + 渲染后的系统提示词 + 已组装的工具 schema)会作为会话状态写入日志,因此每个对话请求都是日志的纯函数(见可重建性 Agent Note)。带有 reason `'initial'` 或 `'resume'` 的完整 `request/header` 快照记录每个 agent loop 实例的边界;之后请求发生变化时,系统会以 reason `'change'` 记录另一份完整快照。`foldRequestHeader(events)` 通过选择最新快照重建请求头。该事件不是 `SurfaceEventType`,不产生 LLM 消息。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -134,6 +164,8 @@ interface TodoItem {
|
||||
interface EpochHeader {
|
||||
/** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
|
||||
config: LlmCallConfig
|
||||
/** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
|
||||
adapterDefaults?: LlmCallConfigAdapterDefaults
|
||||
/** Rendered system prompt text; absent for a system-less request. */
|
||||
system?: string
|
||||
/** Assembled tool schemas; absent for a tool-less request. */
|
||||
@@ -143,6 +175,22 @@ interface EpochHeader {
|
||||
|
||||
规范形式:空系统提示词和空工具列表都表示为字段缺失,与请求构建方式一致。包含已移除的 `request/header-delta` 事件或完整快照原因为 `fallback` 的旧版 v0 日志,会在 seed、append 和持久化加载边界被拒绝,而不会以不完整方式回放。
|
||||
|
||||
### 路由容量事件:`request/context`
|
||||
|
||||
请求所解析到的路由的上下文元数据是独立的已记录状态,在同一步骤内紧随 `request/header` 追加,且仅在提供方、模型或容量与上一条记录不同时追加。它保持在 `EpochHeader` 之外,因为该类型是由 `headerEquals` 逐字段比较的重建契约:容量描述的是路由,不是请求输入,把它折叠进去会让一次容量变化被登记为请求信封的 `change`,也会把适配器元数据拉进 loop 的重建不变式。与 `request/header` 一样,它不是 `SurfaceEventType`,也不产生 LLM 消息。`session.requestContext()` 以增量方式归并最新一条记录。适配器不公布容量的路由会以缺失 `contextWindow` 的形式记录,因此新记录可以清除较早路由的容量。
|
||||
|
||||
```ts type-equiv
|
||||
/** Registration-bound metadata for one resolved model route. */
|
||||
interface RequestContext {
|
||||
/** Registered provider route the metadata belongs to. */
|
||||
provider: string
|
||||
/** Provider-owned model id the metadata belongs to. */
|
||||
model: string
|
||||
/** Maximum combined request and response context in tokens, when advertised. */
|
||||
contextWindow?: number
|
||||
}
|
||||
```
|
||||
|
||||
## `SessionEvent<T>`:一条日志条目
|
||||
|
||||
基于 `type` 的真正可辨识联合(而非独立的 `type`/`data` 联合),因此 `switch (event.type)` 能直接收窄 `event.data`,无需类型断言。`seq` 是日志中的单调递增位置(`seq = log.length`);`time` 为 epoch 毫秒。
|
||||
@@ -156,7 +204,7 @@ interface EpochHeader {
|
||||
*
|
||||
* The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
|
||||
* they only exist on {@link SurfaceEventType} variants (`user/message`,
|
||||
* `assistant/message`, `tool/result`, `steering/message`).
|
||||
* `assistant/message`, `tool/result`).
|
||||
* Non-surface events (boundary markers, chunks, usage, errors) never carry
|
||||
* surface metadata — the compiler enforces this at `Session.append()`
|
||||
* call sites.
|
||||
@@ -190,7 +238,7 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
|
||||
## Surface 类型
|
||||
|
||||
四种产生消息的类型(`SurfaceEventType`:`user/message`、`assistant/message`、`tool/result`、`steering/message`)携带 surface 元数据,用来声明它们如何加入有序的派生 surface。见 [session surface Agent Note](../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md)。
|
||||
三种产生消息的类型(`SurfaceEventType`:`user/message`、`assistant/message`、`tool/result`)携带 surface 元数据,用来声明它们如何加入有序的派生 surface。见 [session surface Agent Note](../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md)。
|
||||
|
||||
### `SurfaceEventType`:事件类型中产生消息的子集
|
||||
|
||||
@@ -204,7 +252,6 @@ type SurfaceEventType =
|
||||
| 'user/message'
|
||||
| 'assistant/message'
|
||||
| 'tool/result'
|
||||
| 'steering/message'
|
||||
```
|
||||
|
||||
### `SurfaceOp`:事件如何进入 surface
|
||||
@@ -214,7 +261,7 @@ type SurfaceEventType =
|
||||
* How a session event entered the ordered surface. Only valid on
|
||||
* {@link SurfaceEventType} events.
|
||||
*
|
||||
* - `'append'`: added to the tail — normal path for user/assistant/tool/steering
|
||||
* - `'append'`: added to the tail — normal path for user/assistant/tool
|
||||
* messages.
|
||||
* - `{ op: 'replace', start, end }`: replaces surface nodes from `start`
|
||||
* (inclusive) through `end` (inclusive) with this node. Both must exist as
|
||||
@@ -248,7 +295,7 @@ interface SurfaceIntent {
|
||||
}
|
||||
```
|
||||
|
||||
对 `SurfaceEventType` 事件必填:每个产生消息的事件都必须声明它如何加入 surface(派生历史的唯一来源)。非 surface 类型在编译期拒绝此参数。
|
||||
对 `SurfaceEventType` 事件必填:每个产生消息的事件都必须声明它如何加入 surface(派生模型历史的唯一来源)。面向人类的记录(transcript)是另一个投影,读取的是日志中追加来源的事件,因为 surface 会有意遮蔽替换所概括的范围(见 [dsh-session](../../packages/core/session/README.md) 的 `isAppendSurfaceEvent`)。非 surface 类型在编译期拒绝此参数。
|
||||
|
||||
此处适用相同的溯源区分:只有 `assistant/message` 可以携带存在但为空的 `sourceEventSeqs`;省略该字段并不表示其源流为空。
|
||||
|
||||
@@ -256,6 +303,8 @@ interface SurfaceIntent {
|
||||
|
||||
`Session.surface` 返回会话稳定的 `SessionSurface` 视图。同一个增量管理器在提交前校验追加候选事件,并根据已提交事件推进该投影;调用方可以观察成员关系和替换代次,但不能调用校验。
|
||||
|
||||
`SurfaceManager(log, baseSeq?)` 也可以折叠一个连续的已加载窗口,其第一个事件的绝对序号为 `baseSeq`。每个事件在该绝对序号空间中仍保持连续;如果替换跨过窗口头部,由于其声明的范围并不存在,该替换会失败。
|
||||
|
||||
```ts type-equiv
|
||||
/** Readonly live projection of the message-producing session events. */
|
||||
interface SessionSurface {
|
||||
@@ -294,16 +343,18 @@ interface SurfaceFoldResult {
|
||||
}
|
||||
```
|
||||
|
||||
## `Session` public API
|
||||
## `Session` 公共 API
|
||||
|
||||
去除方法体的声明与源码中的普通类保持同步,覆盖其公共构造函数、状态访问器、追加边界和历史投影。存储操作仍由生成的 [`ctx.sessions` 服务目录](../cordis-catalog/services.md#ctxsessions--sessionstore)记录。
|
||||
去除方法体的声明与源码中的普通类保持同步,覆盖其脱离态工厂、状态访问器、追加边界和历史投影。存储操作仍由生成的 [`ctx.sessions` 服务目录](../cordis-catalog/services.md#ctxsessions--sessionstore)记录。
|
||||
|
||||
```ts public-api
|
||||
/**
|
||||
* An event-sourced session: an append-only log of {@link SessionEvent}s.
|
||||
*
|
||||
* Plain class (not a Service) — create instances via `ctx.sessions.create()`.
|
||||
* Plain class (not a Service) — create live instances via
|
||||
* `ctx.sessions.create()` and detached instances via {@link create}.
|
||||
* Seeding with an existing event log replays/forks a session.
|
||||
* @typert object
|
||||
*/
|
||||
declare class Session {
|
||||
/** The ordered surface over this session's event log. */
|
||||
@@ -311,7 +362,7 @@ declare class Session {
|
||||
/**
|
||||
* Detached, deep-frozen creation metadata (format version, cwd, lineage,
|
||||
* seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
|
||||
* `Session` is constructed bare (tests, ad-hoc replay), a minimal header is
|
||||
* `Session` is created without a store-owned header, a minimal header is
|
||||
* synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
|
||||
* `session.header` is always present. Kept out of the event log — it is a
|
||||
* storage concern, not replayable conversation state.
|
||||
@@ -321,17 +372,46 @@ declare class Session {
|
||||
get id(): SessionId;
|
||||
/**
|
||||
* The first seq appended IN THIS PROCESS: the length of the constructor
|
||||
* seed (0 without one). Events below it entered through construction —
|
||||
* replay, fork, or resume — and were never published on the `session/event`
|
||||
* firehose (constructor seeds do not emit), so consumers that replay the
|
||||
* log as a publication substitute (telemetry adoption) start here. Distinct
|
||||
* from `header.seedLength`, the DURABLE fork-lineage boundary: a resumed
|
||||
* session's constructor seed is its full stored log, while its header keeps
|
||||
* the original fork value — this field is the in-process construction fact
|
||||
* and is deliberately not persisted.
|
||||
* seed (0 without one). Events with smaller seq values entered through
|
||||
* construction — replay, fork, or resume — and were never published on the
|
||||
* `session/event` firehose (constructor seeds do not emit), so consumers
|
||||
* that replay the log as a publication substitute (telemetry adoption)
|
||||
* start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
|
||||
* boundary: a resumed session's constructor seed is its full stored log,
|
||||
* while its header keeps the original fork value — this field is the
|
||||
* in-process construction fact.
|
||||
*
|
||||
* Not persisted itself: a seeded session projects it into the log as the
|
||||
* `session/end-seed` event, which is what a consumer reading STORED history
|
||||
* reads. Locate the LAST such event, not necessarily one at this seq — a
|
||||
* seed already ending in one is not re-marked, so reopening an untouched
|
||||
* session leaves that event at a smaller seq than `firstLiveSeq`. Prefer
|
||||
* this field in-process: it is exact before the marker reaches storage.
|
||||
*
|
||||
* When this lifecycle appends the marker, it occupies this seq before the
|
||||
* store attaches and therefore does not publish either. Otherwise this seq
|
||||
* holds an ordinary published write.
|
||||
*/
|
||||
readonly firstLiveSeq: number;
|
||||
constructor(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader);
|
||||
/**
|
||||
* Create a detached session by validating and snapshotting borrowed seed
|
||||
* events and storage metadata.
|
||||
* @param id - session identity.
|
||||
* @param seed - optional borrowed replay or fork events.
|
||||
* @param header - optional borrowed storage metadata.
|
||||
* @returns a detached session.
|
||||
*/
|
||||
static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
|
||||
/**
|
||||
* Restore a detached session by taking ownership of fresh persistence values.
|
||||
* Storage shape, event envelopes, sequence continuity, surface transitions,
|
||||
* and header fields are validated before the graphs are frozen in place.
|
||||
* @param id - restored session identity.
|
||||
* @param seed - fresh detached events whose ownership is transferred.
|
||||
* @param header - fresh detached metadata whose ownership is transferred.
|
||||
* @returns a restored detached session.
|
||||
*/
|
||||
static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
|
||||
/**
|
||||
* An immutable snapshot of the append-only event log. The snapshot is reused
|
||||
* until the next append; a previously returned array does not grow later.
|
||||
@@ -355,7 +435,8 @@ declare class Session {
|
||||
* the ordered surface; `sourceEventSeqs` records provenance (the seq
|
||||
* numbers of events this one derives from). REQUIRED for
|
||||
* {@link SurfaceEventType} events (every message-producing event must
|
||||
* declare how it joins the surface, the sole source of derived history) and
|
||||
* declare how it joins the surface, the sole source of derived model
|
||||
* history) and
|
||||
* rejected by the compiler for non-surface types like `turn/start` or
|
||||
* `assistant/chunk`.
|
||||
* @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
|
||||
@@ -389,6 +470,12 @@ declare class Session {
|
||||
* @returns the folded header, or undefined when no header event exists yet.
|
||||
*/
|
||||
requestHeader(): EpochHeader | undefined;
|
||||
/**
|
||||
* Return the latest resolved route metadata, or `undefined` before the first
|
||||
* `request/context` event. Each event is folded once.
|
||||
* @returns the latest immutable route metadata.
|
||||
*/
|
||||
requestContext(): RequestContext | undefined;
|
||||
/**
|
||||
* Derive the LLM message history by walking the ordered sequences of
|
||||
* message-producing events maintained by `surfaceOp` markers. The
|
||||
@@ -409,15 +496,8 @@ declare class Session {
|
||||
*/
|
||||
deriveMessages(): Message[];
|
||||
/**
|
||||
* Project a single event into the LLM message it derives to, or null when
|
||||
* it produces none — a non-surface event (chunk, boundary, log-only record)
|
||||
* or an empty-content assistant/message (which exists only to host usage).
|
||||
* 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 Agent Note). The returned message is
|
||||
* the already frozen message nested in the event wrapper and shared by
|
||||
* delivery, durable history, and model requests.
|
||||
* Instance face of the pure per-node `deriveEventMessage` export from
|
||||
* `surface.ts`.
|
||||
* @param event - the event to project.
|
||||
* @returns the derived message, or null when the event produces none.
|
||||
*/
|
||||
@@ -433,9 +513,8 @@ declare class Session {
|
||||
- `assistant/message` → 一条 assistant 消息,包含事件的提供方/模型溯源信息和可选的适配器私有回放状态。原始 `assistant/chunk` 事件属于回放/UI 数据,在派生时会被**跳过**(组装后的消息才是权威)。**内容为空的** `assistant/message` 也会跳过:因 max-tokens 而截断且无内容的步骤仍会记录一条 `assistant/message` 以承载用量和溯源信息,但无内容的 assistant 轮次不得进入提供方 transcript(文本记录)。
|
||||
- `tool/result` → 一条携带 `tool-result` 块的 user 消息。
|
||||
- `user/message`(注入上下文,即非 `user` 来源)→ 按时间顺序在相应位置生成一条 user-role 消息,并原样承载其 `content`;溯源信息与领域数据都在其类型化的 source 中。
|
||||
- `steering/message` → 按时间顺序在相应位置生成一条携带确切 `content` 的 user-role 消息;可选 envelope 仅作为日志中的展示元数据保留。
|
||||
|
||||
其余所有事件(`turn/*`、`step/*`、插件所有的 `llm/retry`)均为结构信息,不会投影为消息。token 记账读取每个步骤的 `assistant/chunk { type: 'usage' }` 记录;如果没有用量分片,则将 `assistant/message.usage` 作为已提交步骤的后备。失败的模型请求尝试没有 assistant 消息,因此其用量分片是持久化的记账记录。操作错误的步骤号记录在 `turn/end.reason`(`kind: 'error'`)中;如果是最终模型请求失败,其中包含规范化的 `LlmFailure` 事实,其他实时错误则包含消息/代码。由于这一尚未发布的格式有意不提供兼容性承诺,seed/load 校验会拒绝缺少提供方和模型的请求头,以及缺少提供方/模型溯源信息的 assistant 消息,而不会猜测历史数据应走的提供方路由。
|
||||
其余所有事件(`turn/*`、`step/*`、插件所有的 `llm/retry`)均为结构信息,不会投影为消息。token 记账读取每个步骤的 `assistant/chunk { type: 'usage' }` 记录;如果没有用量分片,则将 `assistant/message.usage` 作为已提交步骤的后备。失败的模型请求尝试没有 assistant 消息,因此其用量分片是持久化的记账记录。由于这一尚未发布的格式有意不提供兼容性承诺,seed/load 校验会拒绝缺少提供方和模型的请求头,以及缺少提供方/模型溯源信息的 assistant 消息,而不会猜测历史数据应走的提供方路由。
|
||||
|
||||
## 活跃会话 fork API
|
||||
|
||||
@@ -443,33 +522,18 @@ declare class Session {
|
||||
|
||||
- `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `boundary` seq(含)为止的源事件(默认为当前最后一个事件),要求所选前缀结束时没有开放轮次,然后创建一个活跃的子会话,包含深克隆的种子事件和子会话元数据(`parentSession`、`seedLength` 及继承的 `cwd`)。
|
||||
|
||||
显式 `boundary` 允许调用者从任意稳定的轮次间位置 fork,包括之前的 `turn/end` 或更晚的独立纯日志事件,即使源会话有更新的事件或正在进行的轮次。API 拒绝结束于开放轮次内的前缀,而不是静默截断。更广泛的执行关系健全性检查留在既有的 `dsh-invariants` 插件和持久化修复路径中,不在 `fork()` 中重复。`dsh-subagent-fork` 保留其已完成前缀截断逻辑,因为工具时委托通常在父轮次仍然打开时启动;普通的会话分支应显式指定请求的 boundary。
|
||||
|
||||
## 轮次的触发原因:`TurnTriggerMap`
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* What started a turn.
|
||||
* Merge-extensible sum type (same pattern as MessageSourceMap).
|
||||
*/
|
||||
interface TurnTriggerMap {
|
||||
message: { kind: 'message'; source: MessageSource }
|
||||
/** Recovery turn reopened over the repaired current session log. */
|
||||
retry: { kind: 'retry' }
|
||||
/**
|
||||
* An out-of-band producer explicitly enclosed injected context in a one-shot
|
||||
* turn. `Agent.inject()` appends idle context directly and does not use this
|
||||
* trigger; the source mirrors the producer of the enclosed `user/message`.
|
||||
*/
|
||||
injection: { kind: 'injection'; source: MessageSource }
|
||||
}
|
||||
```
|
||||
显式 `boundary` 允许调用者从任意稳定的轮次间位置 fork,包括之前的 `turn/end` 或更晚的独立纯日志事件,即使源会话有更新的事件或正在进行的轮次。API 拒绝结束于开放轮次内的前缀,而不是静默截断。更广泛的执行关系健全性检查留在既有的 `dsh-invariants` 插件和持久化修复路径中,不在 `fork()` 中重复。`dsh-subagent-fork` 保留其已完成前缀截断逻辑,因为工具调用时的委托通常在父轮次仍然打开时启动;普通的会话分支应显式指定请求的 boundary。
|
||||
|
||||
<a id="why-a-turn-ended-turnendreasonmap"></a>
|
||||
|
||||
## 轮次的结束原因:`TurnEndReasonMap`
|
||||
|
||||
`aborted` 有意作为一种粗粒度的持久结果:它只记录取消中断了实时轮次,不记录是哪个运行时调用方发起取消。仅属于运行时的调用方词汇由 [`AgentCancelCause`](core.md#the-agent-handle) 定义;未来若有审计需求,应新增独立的控制请求事件,而非让终止结果承载这一信息。
|
||||
`turn/start` 没有 trigger 字段。返回 enter 的 pre-step 所产生的 `user/message` 批次记录进入轮次的内容,`llm/retry` 记录请求恢复,idle 注入则保持待处理,直到唤醒交付抵达后续 pre-step。实时轮次会保留停止驱动器的类型化 [`AgentCancelCause`](core.md#the-agent-handle);只有在导入受支持的粗粒度取消记录且记录未保存调用方时,持久化才使用额外的 `{ kind: 'legacy' }` 原因。
|
||||
|
||||
```ts type-equiv
|
||||
/** Durable cancellation cause, including imports whose original coarse record carried no cause. */
|
||||
type TurnEndCancelCause = AgentCancelCause | { readonly kind: 'legacy' }
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -478,20 +542,15 @@ interface TurnTriggerMap {
|
||||
interface TurnEndReasonMap {
|
||||
completed: { kind: 'completed' }
|
||||
/** A cancellation request interrupted the live turn. */
|
||||
aborted: { kind: 'aborted' }
|
||||
aborted: { kind: 'aborted'; reason: TurnEndCancelCause }
|
||||
|
||||
blocked: { kind: 'blocked' }
|
||||
/**
|
||||
* The turn failed: a step threw or the model reported a failure. `step` is the
|
||||
* step number the failure occurred on (the operational error's location — the
|
||||
* single durable record of an in-turn failure; live diagnostics also fire via
|
||||
* `agent/error`). Final model-request failures retain their normalized facts
|
||||
* as one `failure`; other thrown values retain their rendered message and a
|
||||
* real `HarnessError` code when present.
|
||||
* The turn failed. `error` is always a structured failure: the `LlmError`
|
||||
* facts verbatim, or `{ message: errorChain(error), code: 'UNKNOWN' }`
|
||||
* flattened from any other error.
|
||||
*/
|
||||
error: { kind: 'error'; step: number } & (
|
||||
| { failure: LlmFailure; message?: never; code?: never }
|
||||
| { message: string; code?: string; failure?: never }
|
||||
)
|
||||
disposed: { kind: 'disposed' }
|
||||
error: { kind: 'error'; error: LlmFailure }
|
||||
/** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
|
||||
'max-tokens': { kind: 'max-tokens' }
|
||||
/**
|
||||
@@ -502,22 +561,32 @@ interface TurnEndReasonMap {
|
||||
}
|
||||
```
|
||||
|
||||
`max-tokens` 与模型调用中同名的 `FinishReason` 对应:只要轮次内有任何步骤以 `max-tokens` 结束,整个轮次就以 `max-tokens` 而不是 `completed` 结束(即使之后继续执行,截断事实仍优先),让消费方能够区分正常停止和截断停止;但它只优先于 `completed`,`disposed`/`aborted`/`error` 结果的优先级更高。`interrupted` 是唯一不会由任何 loop 发出的原因:它由崩溃恢复合成(见 [persistence.md](persistence.md))。两个 map 均可通过合并扩展。
|
||||
`max-tokens` 与模型调用中同名的 `FinishReason` 对应:只要轮次内有任何步骤以 `max-tokens` 结束,整个轮次就以 `max-tokens` 而不是 `completed` 结束(即使之后继续执行,截断事实仍优先),让消费方能够区分正常停止和截断停止。取消和错误仍是不同的结果。`interrupted` 是唯一不会由任何 loop 发出的原因:它由崩溃恢复合成(见 [persistence.md](persistence.md))。该 map 可通过合并扩展。
|
||||
|
||||
## 执行封闭与独立事件
|
||||
|
||||
一个轮次包围一次模型循环执行,而不是整个会话日志。空闲注入的 `user/message` 事件和插件所属的纯日志事件可以出现在 `turn/end` 与下一个 `turn/start` 之间;它们占用事件 seq,但不递增轮次编号。持久化会尽快记录每个连续且已接受的事件,而崩溃修复只关闭确实仍处于开放状态的尾部轮次。需要持久性屏障的生产方会显式等待 `ctx.sessions.flush(session)`。
|
||||
一个轮次包围一次模型循环执行,而不是整个会话日志。AgentLoop 只会从轮次内返回 enter 的 pre-step 批次记录注入的 `user/message` 事件;插件所属的纯日志事件仍可出现在 `turn/end` 与下一个 `turn/start` 之间,占用事件 seq 但不递增轮次编号。持久化会尽快记录每个连续且已接受的事件,而崩溃修复只关闭确实仍处于开放状态的尾部轮次。需要持久性屏障的生产方会显式等待 `ctx.sessions.flush(session)`。
|
||||
|
||||
可选的 `dsh-session/invariant` 配套插件会强制核心拥有的关系:轮次与步骤编号、执行事件封闭,以及同一步骤内的工具调用/结果配对。可合并扩展事件的关系由声明它的插件拥有,因此核心不会仅因没有开放轮次就拒绝未知事件。见[独立事件决策](../../.agents/notes/implemented/simplification/2026-07-28-remove-synthetic-log-only-turns.md)。
|
||||
|
||||
## 种子结束边界:`session/end-seed`
|
||||
|
||||
带种子的会话(恢复、fork 或回放)紧接构造种子之后追加这个仅日志事件,作为自己的第一次实时写入。在它之前的事件具有更小的 seq,且来自种子。它是 `firstLiveSeq` 的持久投影:该字段为持有对象的消费方回答本生命周期的写入从哪里开始,该事件则为只持有存储字节的消费方回答同一问题。payload 为空,因此位置与 `time` 承载全部含义,且不产生任何消息。`Session` 的构造函数是唯一合法的写入方。
|
||||
|
||||
显式传入的空种子会在 seq 0 写入 `session/end-seed`,从而把从空日志恢复的会话与全新会话区分开来。种子本身已以 `session/end-seed` 结尾时不会重复标记,因此重新打开一个未被改动的会话不会每次拾起都增长日志。应定位存储历史中的最后一条 `session/end-seed`,而不是假定 `firstLiveSeq` 处一定有一条:在一次没有产生工作的拾起之后,该事件的 seq 会小于下一个生命周期的 `firstLiveSeq`。
|
||||
|
||||
它之所以必要,是因为种子历史与实时工作在字节层面完全相同,这会让任何拥有独立开/闭括号的插件失效:一个未配对的 `compact/start`,无论写入方是在压缩中途崩溃、还是此刻正在压缩,读起来都一样。在 `session/end-seed` 之前的开启标记来自构造种子,并且属于一个已结束的生命周期,无论结束原因为何(崩溃、进程接替,或从仍在运行的父会话 fork 出来),因此其所有方可以视之为已死。这只覆盖*本*会话继承的括号:另一个并发存活的会话可能在同一段历史上持有开放括号,而它自己的边界在别处,因此容忍并发写入方还需要日志之外的存活信号。核心写入该边界但不从中读取任何内容——括号的词汇表仍归其所属插件,这也正是崩溃修复只关闭轮次/步骤/工具边界而从不处理 `compact/*` 的原因。
|
||||
|
||||
活动排序通过 `lastActivityTime(events)` 排除该边界:接手会话不算工作,而惰性恢复意味着浏览就会写入一个,因此按日志尾部排序的恢复选择器或会话列表会把每个打开过的会话顶到最前。
|
||||
|
||||
## 插件贡献的仅日志事件
|
||||
|
||||
插件可以通过 declaration merging 添加额外的 `SessionEventMap` 类型。这些是**仅日志**事件:不是 `SurfaceEventType`(不携带 `surfaceOp`,不参与派生历史)。事件所有方决定它们属于一个开放的执行轮次,还是可以独立位于轮次之间,并在自己的不变量配套插件中强制所需关系。完整的逐事件枚举(核心与插件贡献的,含 payload 与溯源信息)见生成的[持久化日志事件目录](../persistence-catalog.md);压缩 seam 的 `compact/*` 语义在 [compaction.md](compaction.md) 中讨论。
|
||||
|
||||
钩子桥接层的 `hook/invoked` / `hook/result` 溯源对(来自 `@deepseek-ai/dsh-hook-protocol`)通过 `handlerId` 关联。轮次中间的钩子点(`PreToolUse`/`PostToolUse`/`Stop`)在 loop 已打开的轮次内触发,因此其 `hook/*` 记录天然位于轮次之内。`SessionStart` 与轮次开始前的 `UserPromptSubmit` 准入 seam 都不生成 `hook/*` 记录,因为两者都没有已打开的轮次可容纳该记录;被放行的上下文改由其带来源的 `user/message` 作为持久证据(见[钩子桥接 Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md))。
|
||||
钩子桥接层的 `hook/invoked` / `hook/result` 溯源对(来自 `@deepseek-ai/dsh-hook-protocol`)通过 `handlerId` 关联。`UserPromptSubmit`、`PreToolUse`、`PostToolUse` 与 `Stop` 在 loop 已打开的轮次内触发,因此其 `hook/*` 记录天然位于轮次之内。`SessionStart` 不生成 `hook/*` 记录,因为它在轮次 1 之前运行;其上下文会在 inbox 中保持待处理,直到唤醒交付打开一个轮次(见[钩子桥接 Agent Note](../../.agents/notes/implemented/feature/2026-06-30-hook-bridges.md))。
|
||||
|
||||
## 持久性契约
|
||||
|
||||
持久化后端依赖的契约如下:持久日志无损保存每个事件,**包括** `assistant/chunk`;`seq` 必须连续,因此不能从规范日志中过滤分片。后端可以为事件批次选择自己的存储编码,只要 `load` 返回与追加时完全一致的事件即可(JSONL 后端默认启用的打包分片行就是此类编码;见 [persistence.md](persistence.md))。所有 `event.data` 都必须可序列化为 JSON;`Session.append` 会从源头强制这一要求(遇到不可序列化数据时抛出),因此错误事件绝不会进入日志,`session.events` 始终与后端可持久化的内容一致。新增携带不可序列化数据的事件类型、破坏核心执行嵌套,或违反事件所有方声明的关系,都会构成磁盘格式的破坏性变更。
|
||||
持久化后端依赖的契约如下:持久日志无损保存每个事件,**包括** `assistant/chunk`;`seq` 必须连续,因此不能从规范日志中过滤分片。后端可以为事件批次选择自己的存储编码,只要 `load` 返回与追加时完全一致的事件即可(JSONL 后端默认启用的打包分片行就是此类编码;见 [persistence.md](persistence.md))。所有 `event.data` 都必须可序列化为 JSON;`Session.append` 会从源头强制这一要求(遇到不可序列化数据时抛出),因此错误事件绝不会进入日志,`session.events` 始终与后端可持久化的内容一致。新增会携带不可序列化数据、破坏核心执行嵌套或违反事件所有方声明关系的事件类型,都会构成磁盘格式的破坏性变更。
|
||||
|
||||
消费此契约的后端见 [persistence.md](persistence.md)。
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user