Merge branch 'feat/windows-pwsh-default' into feat/windows-acl-sandbox
# Conflicts: # .agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.i18n.yaml # .agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.md # .agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.zh.md # docs/module-graph.i18n.yaml # docs/module-graph.md # docs/module-graph.zh.md # packages/bash/tool-pwsh/README.i18n.yaml # packages/bash/tool-pwsh/README.md # packages/bash/tool-pwsh/README.zh.md
This commit is contained in:
@@ -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/subsystems/approval.md
|
||||
approval.md: 2045f2073b72fa265728cbf77b673053a2eaf371
|
||||
approval.zh.md: c021db14c4925acaeef5d984d9dfa88ad55bd8dd
|
||||
approval.md: 660ced6d75886bbea41985fd1f357bf92e8a7838
|
||||
approval.zh.md: 56975914098978af99580694ca19f20a02ed4022
|
||||
|
||||
@@ -38,8 +38,7 @@ type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
|
||||
* ask BEFORE any interactive answerer sees it:
|
||||
*
|
||||
* - `'ask'` (the default) — delegate to the composed answerers; with none
|
||||
* composed the chain falls through to the fail-closed `'unavailable'`
|
||||
* (exactly today's behavior).
|
||||
* composed the chain falls through to the fail-closed `'unavailable'`.
|
||||
* - `'never'` — never prompt anyone: every ask resolves `'rejected'`
|
||||
* deterministically. The strict headless stance (CI, unattended runs) and
|
||||
* the policy whose outcome is knowable without asking.
|
||||
@@ -142,7 +141,7 @@ overrideOf(session: Session): ApprovalPolicy | undefined
|
||||
|
||||
Types: [Agent](core.md) · [Session](session.md)
|
||||
|
||||
Source: [`packages/interaction/user-approval/src/index.ts:193`](../../packages/interaction/user-approval/src/index.ts)
|
||||
Source: [`packages/interaction/user-approval/src/index.ts:192`](../../packages/interaction/user-approval/src/index.ts)
|
||||
|
||||
<a id="approval-events"></a>
|
||||
|
||||
|
||||
@@ -38,8 +38,7 @@ type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
|
||||
* ask BEFORE any interactive answerer sees it:
|
||||
*
|
||||
* - `'ask'` (the default) — delegate to the composed answerers; with none
|
||||
* composed the chain falls through to the fail-closed `'unavailable'`
|
||||
* (exactly today's behavior).
|
||||
* composed the chain falls through to the fail-closed `'unavailable'`.
|
||||
* - `'never'` — never prompt anyone: every ask resolves `'rejected'`
|
||||
* deterministically. The strict headless stance (CI, unattended runs) and
|
||||
* the policy whose outcome is knowable without asking.
|
||||
@@ -142,7 +141,7 @@ overrideOf(session: Session): ApprovalPolicy | undefined
|
||||
|
||||
Types: [Agent](core.md) · [Session](session.md)
|
||||
|
||||
Source: [`packages/interaction/user-approval/src/index.ts:193`](../../packages/interaction/user-approval/src/index.ts)
|
||||
Source: [`packages/interaction/user-approval/src/index.ts:192`](../../packages/interaction/user-approval/src/index.ts)
|
||||
|
||||
<a id="approval-events"></a>
|
||||
|
||||
|
||||
@@ -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/subsystems/bash.md
|
||||
bash.md: 1c60f261e292f389ea4afa3f962f698e0272007b
|
||||
bash.zh.md: 2533cbe10dc0a480d285f87e458cf5ee5fa12d75
|
||||
bash.md: d2797c1e3ff73fe8ecb5a053ed4a1d13b13298dd
|
||||
bash.zh.md: b38a4f332978e94d2945f5fa49b38e8d7df8c9ca
|
||||
|
||||
@@ -12,7 +12,7 @@ Source: [`packages/bash/bash/src/types.ts`](../../packages/bash/bash/src/types.t
|
||||
|
||||
## Request vs. spec: the `resolve()` split
|
||||
|
||||
The seam separates the **model-/plugin-facing request** (optional `workdir`/`timeoutMs`/`stdoutMaxBytes`, filled from config or request policy) from the **fully-resolved spec** the executor acts on (those fields required). The tool layer calls `ctx.bash.resolve(request)` between them — this is the repo's "explicit > implicit at package boundaries" rule made concrete: the reader of a `BashExecSpec` never wonders where the working directory or output budget came from.
|
||||
The seam separates the **model-/plugin-facing request** (optional `workdir`/`timeoutMs`/`stdoutMaxBytes`, filled from config or request policy) from the **fully-resolved spec** the executor acts on (those fields required). The tool layer calls `ctx.bash.resolve(request)` between them (the repo's "explicit > implicit at package boundaries" rule); a `BashExecSpec` carries resolved values.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -162,7 +162,7 @@ interface BashSandboxInfo {
|
||||
}
|
||||
```
|
||||
|
||||
One more piece completes the vocabulary: the `SANDBOX_UNAVAILABLE` error code (owned by the [sandbox seam](sandbox.md)) is what the `ctx.sandbox` provider throws — and the executor propagates — when a confined mode has no usable backend. A selected runner refusing its profile reaches the same fail-closed foreground error; a settled background task records `runnerFailed`. The model receives denial/runner facts in results, learns the effective mode only when a denial marker names it, and can request a one-shot strictly wider retry through `sandbox_permissions` plus `justification`; `ctx.approval` must grant that exact call before anything executes. The complete policy and switching design is the [sandbox Agent Note](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md).
|
||||
The `SANDBOX_UNAVAILABLE` error code (owned by the [sandbox seam](sandbox.md)) is what the `ctx.sandbox` provider throws — and the executor propagates — when a confined mode has no usable backend. A selected runner refusing its profile reaches the same fail-closed foreground error; a settled background task records `runnerFailed`. The model receives denial/runner facts in results, learns the effective mode only when a denial marker names it, and can request a one-shot strictly wider retry through `sandbox_permissions` plus `justification`; `ctx.approval` must grant that exact call before anything executes. The complete policy and switching design is the [sandbox Agent Note](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md).
|
||||
|
||||
## Background processes: `BashProcess`
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ bash 执行 seam 分为 Service Definition([dsh-bash](../../packages/bash/bash
|
||||
|
||||
## 请求与规格:`resolve()` 拆分
|
||||
|
||||
该 seam 将**面向模型/插件的请求**(`workdir`/`timeoutMs`/`stdoutMaxBytes` 可选,由配置或请求策略补全)与执行器实际使用的**完全解析后的 spec**(这些字段均为必填)分开。工具层在二者之间调用 `ctx.bash.resolve(request)`——这具体落实了仓库的「包边界处显式优于隐式」规则:`BashExecSpec` 的读者不必猜测工作目录或输出预算来自何处。
|
||||
该 seam 将**面向模型/插件的请求**(`workdir`/`timeoutMs`/`stdoutMaxBytes` 可选,由配置或请求策略补全)与执行器实际使用的**完全解析后的 spec**(这些字段均为必填)分开。工具层在二者之间调用 `ctx.bash.resolve(request)`(仓库的「包边界处显式优于隐式」规则);`BashExecSpec` 携带的是已解析的值。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -162,7 +162,7 @@ interface BashSandboxInfo {
|
||||
}
|
||||
```
|
||||
|
||||
最后一项补全了这套词汇:当受限模式没有可用后端时,`ctx.sandbox` 提供方会抛出、执行器会传播由[沙箱 seam](sandbox.md)所有的 `SANDBOX_UNAVAILABLE` 错误码。选定的 runner 拒绝其 profile 时会触达同一个故障关闭的前台错误;已结束的后台任务则记录 `runnerFailed`。模型会在结果中收到拒绝/runner 事实,仅当拒绝标记指出生效模式时才得知该模式,并可通过 `sandbox_permissions` 加 `justification` 请求一次性、严格更宽松的重试;执行任何操作前,`ctx.approval` 必须批准该次确切调用。完整的策略与切换设计见[沙箱 Agent Note](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
|
||||
当受限模式没有可用后端时,`ctx.sandbox` 提供方会抛出、执行器会传播由[沙箱 seam](sandbox.md)所有的 `SANDBOX_UNAVAILABLE` 错误码。选定的 runner 拒绝其 profile 时会触达同一个故障关闭的前台错误;已结束的后台任务则记录 `runnerFailed`。模型会在结果中收到拒绝/runner 事实,仅当拒绝标记指出生效模式时才得知该模式,并可通过 `sandbox_permissions` 加 `justification` 请求一次性、严格更宽松的重试;执行任何操作前,`ctx.approval` 必须批准该次确切调用。完整的策略与切换设计见[沙箱 Agent Note](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
|
||||
|
||||
## 后台进程:`BashProcess`
|
||||
|
||||
|
||||
@@ -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/subsystems/client-modules.md
|
||||
client-modules.md: 19bcc6304e9d917c3950fd9857e7b2c2b3c16e73
|
||||
client-modules.zh.md: b0ad293cad2f67de1982ef271308b2a7f06ebaf4
|
||||
client-modules.md: 808fa62b2e5a4351b1dc98bbe4f3d2af63c9dd2b
|
||||
client-modules.zh.md: dcab60bea7c54770cc5e6032afb39d6dd189d037
|
||||
|
||||
@@ -12,7 +12,7 @@ The graph is the wire single source between the Node and browser halves: the hos
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One composed client entry pushed by the host (web2 §0 graph row). Wire
|
||||
* One composed client entry pushed by the host (a graph row). Wire
|
||||
* single source: the host node half (package root) produces this same shape.
|
||||
* `immediately` marks stage-one prefetch; `inject` is informational graph
|
||||
* metadata (the authoritative edges live in each package's dshClient
|
||||
|
||||
@@ -12,7 +12,7 @@ Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One composed client entry pushed by the host (web2 §0 graph row). Wire
|
||||
* One composed client entry pushed by the host (a graph row). Wire
|
||||
* single source: the host node half (package root) produces this same shape.
|
||||
* `immediately` marks stage-one prefetch; `inject` is informational graph
|
||||
* metadata (the authoritative edges live in each package's dshClient
|
||||
|
||||
@@ -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/subsystems/code-runtime.md
|
||||
code-runtime.md: 8a767287414824e6f76659c02c47193522eb21c7
|
||||
code-runtime.zh.md: e6bb7cb93cb542dceb2c7e335162a03429d161e8
|
||||
code-runtime.md: 6d3fa7fa72891897155cb9c611c7aa472629de4f
|
||||
code-runtime.zh.md: 47516c7d21c24399498a05410ab9a46b61736fb4
|
||||
|
||||
@@ -187,5 +187,5 @@ Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and subs
|
||||
abstract run(request: CodeRunRequest): Promise<CodeRunResult>
|
||||
```
|
||||
|
||||
Source: [`packages/code-runtime/code-runtime/src/index.ts:104`](../../packages/code-runtime/code-runtime/src/index.ts)
|
||||
Source: [`packages/code-runtime/code-runtime/src/index.ts:102`](../../packages/code-runtime/code-runtime/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -187,5 +187,5 @@ Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and subs
|
||||
abstract run(request: CodeRunRequest): Promise<CodeRunResult>
|
||||
```
|
||||
|
||||
Source: [`packages/code-runtime/code-runtime/src/index.ts:104`](../../packages/code-runtime/code-runtime/src/index.ts)
|
||||
Source: [`packages/code-runtime/code-runtime/src/index.ts:102`](../../packages/code-runtime/code-runtime/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -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/subsystems/commands.md
|
||||
commands.md: c71c19b8a3f94ba468e6871d5658c4eca1c6276d
|
||||
commands.zh.md: 3f1df0e8aa40524160c8f770793b6064dd5988ae
|
||||
commands.md: 5e280d659c2a74b3d8ba20f362922e9eb4e0b84c
|
||||
commands.zh.md: 24f099d1926f7dba45122bdc53c23a4274e3f507
|
||||
|
||||
@@ -49,6 +49,8 @@ The adapter owns cancellation and passes the exact target agent. `rawInput` begi
|
||||
```ts type-equiv
|
||||
/** Invocation passed to one registered command handler. */
|
||||
interface CommandInvocation {
|
||||
/** Pairing id already written to this invocation's `command/run` event. */
|
||||
readonly commandId: CommandId
|
||||
/** Exact agent whose human-facing surface received the command. */
|
||||
readonly agent: Agent
|
||||
/** Exact text following the registered command name, including separator whitespace. */
|
||||
@@ -159,7 +161,7 @@ async execute( agent: Agent, line: string, signal: AbortSignal, ): Promise<Comma
|
||||
|
||||
Types: [Agent](core.md)
|
||||
|
||||
Source: [`packages/interaction/commands/src/index.ts:305`](../../packages/interaction/commands/src/index.ts)
|
||||
Source: [`packages/interaction/commands/src/index.ts:267`](../../packages/interaction/commands/src/index.ts)
|
||||
|
||||
<a id="commands-events"></a>
|
||||
|
||||
@@ -181,5 +183,5 @@ A command was registered or unregistered. This is an unfiltered registry notific
|
||||
'commands/change'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/interaction/commands/src/index.ts:172`](../../packages/interaction/commands/src/index.ts)
|
||||
Source: [`packages/interaction/commands/src/index.ts:134`](../../packages/interaction/commands/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -49,6 +49,8 @@ interface CommandDefinition {
|
||||
```ts type-equiv
|
||||
/** Invocation passed to one registered command handler. */
|
||||
interface CommandInvocation {
|
||||
/** Pairing id already written to this invocation's `command/run` event. */
|
||||
readonly commandId: CommandId
|
||||
/** Exact agent whose human-facing surface received the command. */
|
||||
readonly agent: Agent
|
||||
/** Exact text following the registered command name, including separator whitespace. */
|
||||
@@ -159,7 +161,7 @@ async execute( agent: Agent, line: string, signal: AbortSignal, ): Promise<Comma
|
||||
|
||||
Types: [Agent](core.md)
|
||||
|
||||
Source: [`packages/interaction/commands/src/index.ts:305`](../../packages/interaction/commands/src/index.ts)
|
||||
Source: [`packages/interaction/commands/src/index.ts:267`](../../packages/interaction/commands/src/index.ts)
|
||||
|
||||
<a id="commands-events"></a>
|
||||
|
||||
@@ -181,5 +183,5 @@ A command was registered or unregistered. This is an unfiltered registry notific
|
||||
'commands/change'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/interaction/commands/src/index.ts:172`](../../packages/interaction/commands/src/index.ts)
|
||||
Source: [`packages/interaction/commands/src/index.ts:134`](../../packages/interaction/commands/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -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/subsystems/compaction.md
|
||||
compaction.md: 942cc740db6b7006952bfc33a39ea9826d673e61
|
||||
compaction.zh.md: c5b65a95f9c0db8b02912c68ef93edce2ceb7c43
|
||||
compaction.md: fdff28ac2ec83966a03050ba8d54fe0ee26c4fc6
|
||||
compaction.zh.md: e4227320d9829cac695dc4ba0c5779086f77194b
|
||||
|
||||
@@ -8,7 +8,7 @@ Source: [`packages/compact/compact/src/types.ts`](../../packages/compact/compact
|
||||
|
||||
## The `compact/*` session events
|
||||
|
||||
Compaction extends [`SessionEventMap`](session.md) with three event types via declaration merging. All three are **log-only** — they record the lock, summary, selected range, shadowed event seqs, token count, and model call without joining the surface. `SurfaceEventType` is deliberately NOT extended (only message-producing events reach the model), so the summary itself rides on a separate `user/message` with `surfaceOp: { op: 'replace', start, end }` — the only surface mutation performed by summary compaction. See the Agent Note for why reusing `user/message` is honest rather than a workaround.
|
||||
Compaction extends [`SessionEventMap`](session.md) with three event types via declaration merging. All three are **log-only** — they record the lock, summary, selected range, shadowed event seqs, token count, and model call without joining the surface. `SurfaceEventType` is deliberately NOT extended (only message-producing events reach the model), so the summary itself rides on a separate `user/message` with `surfaceOp: { op: 'replace', start, end }` — the only surface mutation performed by summary compaction. The [Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) owns the rationale for reusing `user/message`.
|
||||
|
||||
| Event | Payload | Role |
|
||||
|---|---|---|
|
||||
@@ -20,7 +20,7 @@ The lock brackets the **whole** operation: `compact/start` is appended first, th
|
||||
|
||||
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 subsystem 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.
|
||||
These variants are merged inside a `declare module '@deepseek-ai/dsh-session/types'` block, so — unlike the top-level types on the other subsystem 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`
|
||||
|
||||
@@ -29,6 +29,10 @@ What a successful compaction returns to its caller: the bookkeeping-event seqs,
|
||||
```ts type-equiv
|
||||
/** Result of a successful compaction operation. */
|
||||
interface CompactionResult {
|
||||
/** Stable identity shared by this compaction's complete durable lifecycle. */
|
||||
compactionId: CompactionId
|
||||
/** Human command that initiated this compaction, when it was manual. */
|
||||
sourceCommandId?: CommandId
|
||||
/** The seq of the appended `compact/start` event. */
|
||||
startSeq: number
|
||||
/** The seq of the appended `compact/summary` event. */
|
||||
@@ -62,7 +66,7 @@ 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, `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.
|
||||
`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 creates its replacement `user/message` source with `compactCheckpointSource(compactionId, sourceCommandId?)`; client and wire consumers import that constructor, `CompactCheckpointSource`, and `isCompactCheckpointSource()` from the cordis-free `@deepseek-ai/dsh-compact/checkpoint` subpath, while the package root re-exports them for host consumers. The required transaction identity correlates the replacement checkpoint, while the predicate keeps 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.
|
||||
|
||||
Expected manual failures use `ManualCompactionErrorCode`:
|
||||
|
||||
@@ -125,7 +129,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
|
||||
### `ctx.compact` — `CompactService` (abstract seam)
|
||||
|
||||
Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses COMPACT_CHECKPOINT_SOURCE so consumers recognize it independently of the backend. Load one implementation per context as `ctx.compact`.
|
||||
Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses compactCheckpointSource with the transaction identity so consumers recognize and correlate it independently of the backend. Load one implementation per context as `ctx.compact`.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -155,13 +159,14 @@ abstract compactIfNeeded( agent: CompactAgentContext, trigger: CompactionTrigger
|
||||
*
|
||||
* @param agent - idle agent whose durable history should be compacted.
|
||||
* @param signal - cancellation scoped to this compaction request.
|
||||
* @param sourceCommandId - initiating command identity for a manual compaction.
|
||||
* @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>
|
||||
abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, sourceCommandId?: CommandId, ): Promise<CompactionResult | null>
|
||||
|
||||
/**
|
||||
* Forcibly compact a range of surface nodes into a single summary node.
|
||||
@@ -170,7 +175,8 @@ abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): P
|
||||
* balanced so assistant tool calls remain paired with their results. A model-
|
||||
* backed implementation forwards cancellation and rejects active, missing,
|
||||
* reversed, or unbalanced ranges. The target session is `agent.session`.
|
||||
* Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}.
|
||||
* Its replacement user message must use {@link compactCheckpointSource} with
|
||||
* the transaction's `CompactionId`.
|
||||
* Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}
|
||||
* for the edge checks.
|
||||
*
|
||||
@@ -184,7 +190,9 @@ abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): P
|
||||
abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
|
||||
```
|
||||
|
||||
Source: [`packages/compact/compact/src/index.ts:93`](../../packages/compact/compact/src/index.ts)
|
||||
Types: [CommandId](commands.md)
|
||||
|
||||
Source: [`packages/compact/compact/src/index.ts:96`](../../packages/compact/compact/src/index.ts)
|
||||
|
||||
<a id="ctxtoolresultprune--toolresultpruneservice"></a>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## `compact/*` 会话事件
|
||||
|
||||
压缩通过声明合并为 [`SessionEventMap`](session.md) 扩展三种事件类型。三者都**仅写入日志**——它们记录锁、摘要、选中范围、被遮蔽事件 seq、token 数以及模型调用,绝不进入 surface。这里有意不扩展 `SurfaceEventType`(只有产生消息的事件才到达模型),因此摘要本身承载在另一条带有 `surfaceOp: { op: 'replace', start, end }` 的 `user/message` 上——这是摘要压缩执行的唯一 surface 变更。关于复用 `user/message` 为何是如实建模而非权宜之计,见对应 Agent Note。
|
||||
压缩通过声明合并为 [`SessionEventMap`](session.md) 扩展三种事件类型。三者都**仅写入日志**——它们记录锁、摘要、选中范围、被遮蔽事件 seq、token 数以及模型调用,绝不进入 surface。这里有意不扩展 `SurfaceEventType`(只有产生消息的事件才到达模型),因此摘要本身承载在另一条带有 `surfaceOp: { op: 'replace', start, end }` 的 `user/message` 上——这是摘要压缩执行的唯一 surface 变更。[Agent Note](../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) 负责复用 `user/message` 的决策依据。
|
||||
|
||||
| 事件 | 载荷 | 作用 |
|
||||
|---|---|---|
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
这些标记表示锁的时间点,而不是排他的容器。摘要等待期间,不相关的空闲注入可以出现在独立的手动 start 与 end 之间。手动路径只重新验证所选位置 span,因此替换检查点之后仍保留该注入上下文。活动的未匹配 start 会阻塞所有入口点;较新 `session/end-seed` 之前的未匹配 start 是先前生命周期留下的陈旧证据,会被忽略。
|
||||
|
||||
这些变体在 `declare module '@deepseek-ai/dsh-session'` 块内合并,因此——与其他子系统页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。
|
||||
这些变体在 `declare module '@deepseek-ai/dsh-session/types'` 块内合并,因此——与其他子系统页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。
|
||||
|
||||
## `CompactionResult`
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
```ts type-equiv
|
||||
/** Result of a successful compaction operation. */
|
||||
interface CompactionResult {
|
||||
/** Stable identity shared by this compaction's complete durable lifecycle. */
|
||||
compactionId: CompactionId
|
||||
/** Human command that initiated this compaction, when it was manual. */
|
||||
sourceCommandId?: CommandId
|
||||
/** The seq of the appended `compact/start` event. */
|
||||
startSeq: number
|
||||
/** The seq of the appended `compact/summary` event. */
|
||||
@@ -62,7 +66,7 @@ interface CompactionResult {
|
||||
type CompactionTrigger = 'pressure' | 'context-overflow'
|
||||
```
|
||||
|
||||
`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` 拥有保留策略、事件排序、按路由执行的摘要调用及其配置。
|
||||
`CompactService` 暴露 `compactIfNeeded(agent, trigger, signal)` 以执行自动 `pressure` 或 `context-overflow` 策略,暴露 `compactNow(agent, signal)` 以便即使未达到压力也对空闲会话进行一次有效缩减,还针对显式、两端均包含的 surface 范围暴露 `compactRegion(...)`。`compactNow()` 作为轮次之间的 agent maintenance 运行;没有有效范围时返回 `null` 且不写入;在摘要前记录独立的 `turn: null` 标记对,并在后续排队提示词能够从新表层派生前 flush 已闭合尝试。每个后端都使用 `compactCheckpointSource(compactionId, sourceCommandId?)` 创建替换用 `user/message` 的源;client 与 wire 消费方从无 cordis 的 `@deepseek-ai/dsh-compact/checkpoint` 子路径导入该构造函数、`CompactCheckpointSource` 和 `isCompactCheckpointSource()`,包根则为 host 消费方重新导出它们。必填的事务身份会关联替换检查点,而该判定函数使检查点识别不依赖任一特定后端。实现必须把传入的 signal 转发给摘要流程。该 seam 不拥有计价 API:单例 [`ctx.tokenMeter`](token-meter.md) 直接拥有估算与回放,而 `dsh-compact-basic` 拥有保留策略、事件排序、按路由执行的摘要调用及其配置。
|
||||
|
||||
预期的手动失败使用 `ManualCompactionErrorCode`:
|
||||
|
||||
@@ -125,7 +129,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
|
||||
### `ctx.compact` — `CompactService` (abstract seam)
|
||||
|
||||
Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses COMPACT_CHECKPOINT_SOURCE so consumers recognize it independently of the backend. Load one implementation per context as `ctx.compact`.
|
||||
Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses compactCheckpointSource with the transaction identity so consumers recognize and correlate it independently of the backend. Load one implementation per context as `ctx.compact`.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -155,13 +159,14 @@ abstract compactIfNeeded( agent: CompactAgentContext, trigger: CompactionTrigger
|
||||
*
|
||||
* @param agent - idle agent whose durable history should be compacted.
|
||||
* @param signal - cancellation scoped to this compaction request.
|
||||
* @param sourceCommandId - initiating command identity for a manual compaction.
|
||||
* @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>
|
||||
abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, sourceCommandId?: CommandId, ): Promise<CompactionResult | null>
|
||||
|
||||
/**
|
||||
* Forcibly compact a range of surface nodes into a single summary node.
|
||||
@@ -170,7 +175,8 @@ abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): P
|
||||
* balanced so assistant tool calls remain paired with their results. A model-
|
||||
* backed implementation forwards cancellation and rejects active, missing,
|
||||
* reversed, or unbalanced ranges. The target session is `agent.session`.
|
||||
* Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}.
|
||||
* Its replacement user message must use {@link compactCheckpointSource} with
|
||||
* the transaction's `CompactionId`.
|
||||
* Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}
|
||||
* for the edge checks.
|
||||
*
|
||||
@@ -184,7 +190,9 @@ abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): P
|
||||
abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
|
||||
```
|
||||
|
||||
Source: [`packages/compact/compact/src/index.ts:93`](../../packages/compact/compact/src/index.ts)
|
||||
Types: [CommandId](commands.md)
|
||||
|
||||
Source: [`packages/compact/compact/src/index.ts:96`](../../packages/compact/compact/src/index.ts)
|
||||
|
||||
<a id="ctxtoolresultprune--toolresultpruneservice"></a>
|
||||
|
||||
|
||||
@@ -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/subsystems/core.md
|
||||
core.md: ec9a966164f2b706ae16341c628fb8f849eb7382
|
||||
core.zh.md: 4de168fd9714a935b4d9b08bbabd52492d1ea1cc
|
||||
core.md: 27c4359e360223d336cd94695bb45a79f0fd370c
|
||||
core.zh.md: 4e7519665b6d9efb8075546d93325debd961905d
|
||||
|
||||
@@ -547,7 +547,7 @@ list(): Agent[]
|
||||
roots(): Agent[]
|
||||
```
|
||||
|
||||
Source: [`packages/core/agent/src/index.ts:253`](../../packages/core/agent/src/index.ts)
|
||||
Source: [`packages/core/agent/src/index.ts:254`](../../packages/core/agent/src/index.ts)
|
||||
|
||||
<a id="agent-events"></a>
|
||||
|
||||
@@ -575,7 +575,7 @@ A fully configured agent and live session were published. Setup is composition-o
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:158`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:159`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentdisposed--emit"></a>
|
||||
|
||||
@@ -597,7 +597,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence and sco
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:168`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agenterror--emit"></a>
|
||||
|
||||
@@ -621,7 +621,7 @@ A step or turn errored. The machine reports a failure here even when the error h
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:289`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:290`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentinboxclaimed--emit"></a>
|
||||
|
||||
@@ -645,7 +645,7 @@ One message left the inbox inside its open turn. If the proposed step is rejecte
|
||||
|
||||
Types: [Scoped](scope.md) · [UserMessage](session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:196`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:197`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentinboxdiscarded--emit"></a>
|
||||
|
||||
@@ -666,7 +666,7 @@ One message was discarded from the live inbox.
|
||||
|
||||
Types: [Scoped](scope.md) · [UserMessage](session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:205`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentinboxinserted--emit"></a>
|
||||
|
||||
@@ -687,7 +687,7 @@ One message entered the live inbox.
|
||||
|
||||
Types: [Scoped](scope.md) · [UserMessage](session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:185`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:186`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentpre-step--waterfall"></a>
|
||||
|
||||
@@ -712,7 +712,7 @@ Reject a proposed step or replace the messages that enter it. Calling `next()` p
|
||||
|
||||
Types: [Scoped](scope.md) · [UserMessage](session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:230`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:231`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentrequest--waterfall"></a>
|
||||
|
||||
@@ -738,7 +738,7 @@ Replace the frozen call configuration. `await next()` yields the config the mach
|
||||
|
||||
Types: [LlmCallConfig](llm-streaming.md) · [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:244`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentrequest-error--waterfall"></a>
|
||||
|
||||
@@ -767,7 +767,7 @@ Handle one failed model-request attempt before the loop retries or closes its st
|
||||
|
||||
Types: [LlmFailure](llm-streaming.md) · [ResolvedRetryPolicy](llm-streaming.md) · [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:260`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentsession-start--emit"></a>
|
||||
|
||||
@@ -791,7 +791,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:216`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:217`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentstatus--emit"></a>
|
||||
|
||||
@@ -814,7 +814,7 @@ Agent status changed (`idle` ⇄ `running`). A waking delivery enters `running`
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:177`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:178`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentturn-stopping--serial"></a>
|
||||
|
||||
@@ -845,7 +845,7 @@ The turn is about to close: the model owes no response (no live tool calls, no f
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:277`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:278`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agent-loop-events"></a>
|
||||
|
||||
|
||||
@@ -555,7 +555,7 @@ list(): Agent[]
|
||||
roots(): Agent[]
|
||||
```
|
||||
|
||||
Source: [`packages/core/agent/src/index.ts:253`](../../packages/core/agent/src/index.ts)
|
||||
Source: [`packages/core/agent/src/index.ts:254`](../../packages/core/agent/src/index.ts)
|
||||
|
||||
<a id="agent-events"></a>
|
||||
|
||||
@@ -583,7 +583,7 @@ A fully configured agent and live session were published. Setup is composition-o
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:158`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:159`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentdisposed--emit"></a>
|
||||
|
||||
@@ -605,7 +605,7 @@ An agent left the registry; AgentLoop emits this after driver quiescence and sco
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:168`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agenterror--emit"></a>
|
||||
|
||||
@@ -629,7 +629,7 @@ A step or turn errored. The machine reports a failure here even when the error h
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:289`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:290`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentinboxclaimed--emit"></a>
|
||||
|
||||
@@ -653,7 +653,7 @@ One message left the inbox inside its open turn. If the proposed step is rejecte
|
||||
|
||||
Types: [Scoped](scope.md) · [UserMessage](session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:196`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:197`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentinboxdiscarded--emit"></a>
|
||||
|
||||
@@ -674,7 +674,7 @@ One message was discarded from the live inbox.
|
||||
|
||||
Types: [Scoped](scope.md) · [UserMessage](session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:205`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentinboxinserted--emit"></a>
|
||||
|
||||
@@ -695,7 +695,7 @@ One message entered the live inbox.
|
||||
|
||||
Types: [Scoped](scope.md) · [UserMessage](session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:185`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:186`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentpre-step--waterfall"></a>
|
||||
|
||||
@@ -720,7 +720,7 @@ Reject a proposed step or replace the messages that enter it. Calling `next()` p
|
||||
|
||||
Types: [Scoped](scope.md) · [UserMessage](session.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:230`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:231`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentrequest--waterfall"></a>
|
||||
|
||||
@@ -746,7 +746,7 @@ Replace the frozen call configuration. `await next()` yields the config the mach
|
||||
|
||||
Types: [LlmCallConfig](llm-streaming.md) · [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:244`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentrequest-error--waterfall"></a>
|
||||
|
||||
@@ -775,7 +775,7 @@ Handle one failed model-request attempt before the loop retries or closes its st
|
||||
|
||||
Types: [LlmFailure](llm-streaming.md) · [ResolvedRetryPolicy](llm-streaming.md) · [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:260`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentsession-start--emit"></a>
|
||||
|
||||
@@ -799,7 +799,7 @@ The session lifecycle began, once before the first turn. Use `agent.inject()` to
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:216`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:217`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentstatus--emit"></a>
|
||||
|
||||
@@ -822,7 +822,7 @@ Agent status changed (`idle` ⇄ `running`). A waking delivery enters `running`
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:177`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:178`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agentturn-stopping--serial"></a>
|
||||
|
||||
@@ -853,7 +853,7 @@ The turn is about to close: the model owes no response (no live tool calls, no f
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:277`](../../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/runtime-types.ts:278`](../../packages/core/agent/src/runtime-types.ts)
|
||||
|
||||
<a id="agent-loop-events"></a>
|
||||
|
||||
|
||||
@@ -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/subsystems/filesystem.md
|
||||
filesystem.md: 6748ddc7f290b6714302223758587226647641e7
|
||||
filesystem.zh.md: 28535d3d9d40e92bb6d5b2b0995a535b42fd9a23
|
||||
filesystem.md: e0edad514b0c4d9108b18cc0b472600b29024c51
|
||||
filesystem.zh.md: 81c2d87b6a96740c7a99449a27c16331f5a0645a
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](filesystem.zh.md)
|
||||
|
||||
The optional filesystem capability has four parts: [dsh-fs](../../packages/fs/fs) owns `ctx.fs` and atomic text operations with optional version guards, [dsh-fs-local](../../packages/fs/fs-local) implements local disk, [dsh-fs-policy](../../packages/fs/fs-policy) adds observed-state and freshness rules through events rather than a service, and [dsh-tool-fs](../../packages/fs/tool-fs) directly executes model-facing read/write/edit calls and renders windows. It is outside the agent-loop spine; alternate backends do not change policy or tool schemas.
|
||||
The optional filesystem capability has four parts: [dsh-fs](../../packages/fs/fs) owns `ctx.fs` and atomic text operations with optional guards, [dsh-fs-local](../../packages/fs/fs-local) implements local disk, [dsh-fs-policy](../../packages/fs/fs-policy) records observed presence or absence and adds freshness rules through events rather than a service, and [dsh-tool-fs](../../packages/fs/tool-fs) directly executes model-facing read/write/edit calls and renders windows. It is outside the agent-loop spine; alternate backends do not change policy or tool schemas.
|
||||
|
||||
The model is **additive, not subtractive**: `ctx.fs` alone is a complete, unconstrained text-storage seam (`write` unconditionally creates-or-overwrites, `edit` unconditionally replaces literal text). `dsh-fs-policy` is a plugin that *adds* policy on top by deciding the `fs/*` waterfalls; removing it leaves the bare provider rather than breaking the tool, because the tool is not method-coupled to the policy. A deployment that loads `dsh-tool-fs` is expected to also load `dsh-fs-policy` so the default behavior is read-before-write/edit.
|
||||
|
||||
@@ -113,7 +113,7 @@ interface FsDirEntry {
|
||||
|
||||
## Write and edit guards (provider contract)
|
||||
|
||||
Both `writeText` and `editText` take their version guard OPTIONALLY: omit it for an unconditional (bare-provider) mutation, supply it to guard. `writeText`'s guard is an `FsWriteIntent` — `createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`; `replaceIfVersion` replaces only when the target exists at the observed version, else `FS_STALE_VERSION`. Omitting `expected` unconditionally creates-or-overwrites. The union itself carries only the two guarded intents; "no guard" is expressed by omission, so write and edit share one symmetric `expected?` shape.
|
||||
Both `writeText` and `editText` take their version guard OPTIONALLY: omit it for an unconditional (bare-provider) mutation, supply it to guard. `writeText`'s guard is an `FsWriteIntent` — `createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED`, including a target that appears after the provider's initial probe because publication itself must be no-replace; `replaceIfVersion` replaces only when the target exists at the observed version, else `FS_STALE_VERSION`. Omitting `expected` unconditionally creates-or-overwrites. The union itself carries only the two guarded intents; "no guard" is expressed by omission, so write and edit share one symmetric `expected?` shape.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -181,7 +181,18 @@ interface FsEditOutcome {
|
||||
|
||||
`dsh-fs` owns three events the tool dispatches and the policy plugin listens for, so the emitter (`dsh-tool-fs`) and the listener (`dsh-fs-policy`) share a vocabulary without the emitter depending on the policy plugin. They carry only `dsh-fs` vocabulary plus an opaque `object` actor — no model-facing concepts and no agent/session owner structure.
|
||||
|
||||
`fs/write-intent` and `fs/edit-intent` are **single-slot decision waterfalls**: the tool dispatches each with a default thunk returning `undefined` (the bare provider), and a listener fully decides without calling `next()`. The slot is first-wins by registration order — the policy plugin owning it is a deployment convention, not an enforced invariant. `fs/observed` is a fire-and-forget recording event dispatched with a plain `ctx.emit`; its listener MUST be synchronous and side-effect-only, because the tool does NOT guard the emit — a throwing listener would surface as the tool's `isError` result for a mutation that already succeeded. The generated [cordis surface](#cordis-surface) below shows the exact signatures.
|
||||
`fs/write-intent` and `fs/edit-intent` are **single-slot decision waterfalls**: the tool dispatches each with a default thunk returning `undefined` (the bare provider), and a listener fully decides without calling `next()`. The slot is first-wins by registration order — the policy plugin owning it is a deployment convention, not an enforced invariant. `fs/observed` is a fire-and-forget recording event carrying an `FsObservation`: present at a version or confirmed absent. It is dispatched with a plain `ctx.emit`; its listener MUST be synchronous and side-effect-only, because the tool does NOT guard the emit — a throwing listener can replace a read error or surface as the tool's `isError` result after a mutation already succeeded. The generated [cordis surface](#cordis-surface) below shows the exact signatures.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One authoritative observation of a target. A present observation carries the
|
||||
* version used by guarded replacement; an absent observation authorizes only a
|
||||
* guarded create, never an edit.
|
||||
*/
|
||||
type FsObservation =
|
||||
| { readonly kind: 'present'; readonly version: FsVersion }
|
||||
| { readonly kind: 'absent' }
|
||||
```
|
||||
|
||||
## Execution context (policy plugin)
|
||||
|
||||
@@ -209,7 +220,7 @@ interface FsPolicyExec {
|
||||
|
||||
## Read outcome (consumer / read rendering)
|
||||
|
||||
A text read is bounded by line window, byte cap, and backend limits. After the byte cap is reached, scanning continues without retaining more lines so `totalLines` remains exact. The outcome the model-facing `read` tool renders is purely presentational; there is no `full`/`partial` view — authorization is freshness-based (the tool emits `fs/observed` with the stat's version directly), so any windowed read can authorize a later write/edit when the file is unchanged. Read windowing and this outcome shape live in `dsh-tool-fs` (the executor that owns the read), not in the policy plugin.
|
||||
A text read is bounded by line window, byte cap, and backend limits. After the byte cap is reached, scanning continues without retaining more lines so `totalLines` remains exact. The outcome the model-facing `read` tool renders is purely presentational; there is no `full`/`partial` view — authorization is freshness-based (the tool emits a present `fs/observed` with the stat's version), so any windowed read can authorize a later write/edit when the file is unchanged. A metadata miss emits an absent observation before the tool returns `FS_NOT_FOUND`, allowing a later guarded write to recreate an externally deleted target without authorizing edit. Read windowing and this outcome shape live in `dsh-tool-fs` (the executor that owns the read), not in the policy plugin.
|
||||
|
||||
```ts type-equiv
|
||||
/** Outcome of a bounded text read — what {@link formatReadOutput} renders. */
|
||||
@@ -227,7 +238,7 @@ interface FileReadOutcome {
|
||||
|
||||
## Observed-file state (policy plugin)
|
||||
|
||||
Observed state is a `WeakMap<owner, Map<targetKey, { version }>>` held inside the `dsh-fs-policy` plugin. An entry exists **iff** the owner has read, written, OR edited that target (every success emits `fs/observed`), so its presence is the prior-observation record — there is no separate `hasRead` flag and no view distinction. The owner is derived from the event actor (normally `exec.agent.session`), treated as opaque and never read. A successful read/write/edit refreshes the recorded version for that owner; disposal drops everything (HMR safety).
|
||||
Observed state is a `WeakMap<owner, Map<targetKey, FsObservation>>` held inside the `dsh-fs-policy` plugin. Missing map entry means unseen; `{ kind: 'absent' }` means a `read` or `str_replace_editor` `view`, `str_replace`, or `insert` metadata miss confirmed absence; `{ kind: 'present', version }` means a read, write, or edit observed that version. The write decision maps unseen and absent to `createIfAbsent`, while present maps to `replaceIfVersion`; the edit decision maps unseen to `FS_NOT_OBSERVED`, absent to `FS_NOT_FOUND`, and present to its version guard. The owner is derived from the event actor (normally `exec.agent.session`), treated as opaque and never read. Disposal drops everything (HMR safety), and the policy performs no filesystem I/O.
|
||||
|
||||
## Error taxonomy (provider contract)
|
||||
|
||||
@@ -254,15 +265,15 @@ type FsErrorCode =
|
||||
| 'FS_ABORTED'
|
||||
```
|
||||
|
||||
`FS_NOT_DIRECTORY`, `FS_PERMISSION_DENIED`, and `FS_IO_ERROR` are used by directory listing to distinguish an existing non-directory target, a denied listing, and an unexpected backend I/O failure. `FS_SANDBOX_DENIED` is a POLICY refusal from a sandbox-enforcing backend (`dsh-fs-sandbox`) — the mode fence denied a write/edit — distinct from `FS_PERMISSION_DENIED` (the host kernel refusing). `FS_NOT_OBSERVED` means the policy plugin has no prior-observation record for this owner (or a `createIfAbsent` hit an existing file). `FS_STALE_VERSION` means the backend version no longer matches the observed one (or an edit hit a missing target). Freshness authorization has no partial/full distinction, so there is no `FS_PARTIAL_OBSERVATION`.
|
||||
`FS_NOT_DIRECTORY`, `FS_PERMISSION_DENIED`, and `FS_IO_ERROR` are used by directory listing to distinguish an existing non-directory target, a denied listing, and an unexpected backend I/O failure. `FS_SANDBOX_DENIED` is a POLICY refusal from a sandbox-enforcing backend (`dsh-fs-sandbox`) — the mode fence denied a write/edit — distinct from `FS_PERMISSION_DENIED` (the host kernel refusing). `FS_NOT_OBSERVED` means the policy plugin has no prior-observation record for this owner (or a `createIfAbsent` hit an existing file). `FS_NOT_FOUND` also represents an edit rejected from confirmed absence. `FS_STALE_VERSION` means the backend version no longer matches the observed one (or the provider itself receives an edit for a missing target). Freshness authorization has no partial/full distinction, so there is no `FS_PARTIAL_OBSERVATION`.
|
||||
|
||||
## No timeouts on file IO
|
||||
|
||||
`read`/`write`/`edit` take **no** `timeoutMs`, and the provider contract arms no deadline — unlike bash and web (which consume [`@deepseek-ai/dsh-timeout`](../../packages/util/timeout/README.md)) and the bash-backed `glob`/`grep` (whose declared `timeoutMs` is enforced by `@deepseek-ai/dsh-timeout-policy`): those are process-backed, where a deadline can really kill the work. A local syscall is best-effort-abortable at most — a timeout could not force an in-progress `fsync`/`rename` to stop, so a deadline here would be a knob that cannot deliver on its promise, and an implicit default in the exact place explicit-over-implicit forbids. Both reference agents (Claude Code, Codex) leave file IO untimed for the same reason; cancellation still propagates through the tool-execution signal for best-effort abort at syscall boundaries.
|
||||
`read`/`write`/`edit` take **no** `timeoutMs`, and the provider contract arms no deadline — unlike bash and web (which consume [`@deepseek-ai/dsh-timeout`](../../packages/util/timeout/README.md)) and the subprocess-backed `glob`/`grep` (whose declared `timeoutMs` is enforced by `@deepseek-ai/dsh-timeout-policy`): those are process-backed, where a deadline can really kill the work. A local syscall is best-effort-abortable at most — a timeout could not force an in-progress `fsync`/`rename` to stop, so a `timeoutMs` here would be a deadline the seam cannot enforce, and an implicit default in the exact place explicit-over-implicit forbids. Cancellation still propagates through the tool-execution signal for best-effort abort at syscall boundaries.
|
||||
|
||||
## The service and the plugin
|
||||
|
||||
`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls (supplying `createIfAbsent`/`replaceIfVersion`/`{ version }` or throwing `FS_NOT_OBSERVED`) and records on `fs/observed`. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated [`ctx.fs` section](#ctxfs--filesystem-abstract-seam) below shows the exact signatures.
|
||||
`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls from unseen/absent/present state and records `FsObservation` values. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated [`ctx.fs` section](#ctxfs--filesystem-abstract-seam) below shows the exact signatures.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
@@ -376,7 +387,7 @@ abstract listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>
|
||||
* @param target - the resolved target to write.
|
||||
* @param content - the full new file content.
|
||||
* @param expected - the write intent guarding the write; omit for unconditional.
|
||||
* @param signal - aborts before the atomic rename takes effect.
|
||||
* @param signal - aborts before atomic publication takes effect.
|
||||
* @param sandboxPolicy - the per-call mode and workspace root this write
|
||||
* runs under; a sandboxing backend fences the write by it, the bare backend
|
||||
* ignores it. Omit to leave the backend its own default.
|
||||
@@ -391,7 +402,7 @@ abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent,
|
||||
* @param target - the resolved target to edit.
|
||||
* @param edit - the literal search/replace request.
|
||||
* @param expected - the version guard; omit for an unconditional edit.
|
||||
* @param signal - aborts before the atomic rename takes effect.
|
||||
* @param signal - aborts before atomic publication takes effect.
|
||||
* @param sandboxPolicy - the per-call mode and workspace root this edit runs
|
||||
* under; a sandboxing backend fences the edit by it, the bare backend
|
||||
* ignores it. Omit to leave the backend its own default.
|
||||
@@ -402,7 +413,7 @@ abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version:
|
||||
|
||||
Types: [SandboxExecutionPolicy](sandbox.md)
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:83`](../../packages/fs/fs/src/index.ts)
|
||||
Source: [`packages/fs/fs/src/index.ts:86`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
<a id="fs-events"></a>
|
||||
|
||||
@@ -425,27 +436,28 @@ Single-slot decision for the next FileSystem.editText. Calling `next()` yields a
|
||||
'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>
|
||||
```
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:64`](../../packages/fs/fs/src/index.ts)
|
||||
Source: [`packages/fs/fs/src/index.ts:66`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
<a id="fsobserved--emit"></a>
|
||||
|
||||
#### `fs/observed` — emit
|
||||
|
||||
Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited.
|
||||
Record an authoritative positive or negative observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Record a successful observation. Listeners must be synchronous recorders:
|
||||
* throws fail the tool call and returned promises are not awaited.
|
||||
* @param target - the target that was read/written/edited.
|
||||
* @param version - the version the actor now holds as its observation.
|
||||
* Record an authoritative positive or negative observation. Listeners must
|
||||
* be synchronous recorders: throws fail the tool call and returned promises
|
||||
* are not awaited.
|
||||
* @param target - the target whose presence or absence was observed.
|
||||
* @param observation - present with its version, or confirmed absent.
|
||||
* @param actor - the observing tool-execution context; undefined records nothing useful.
|
||||
* @mode emit
|
||||
*/
|
||||
'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void
|
||||
'fs/observed'(target: FsTarget, observation: FsObservation, actor: object | undefined): void
|
||||
```
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:73`](../../packages/fs/fs/src/index.ts)
|
||||
Source: [`packages/fs/fs/src/index.ts:76`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
<a id="fswrite-intent--waterfall"></a>
|
||||
|
||||
@@ -465,5 +477,5 @@ Single-slot decision for the next FileSystem.writeText. Calling `next()` yields
|
||||
'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>
|
||||
```
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:56`](../../packages/fs/fs/src/index.ts)
|
||||
Source: [`packages/fs/fs/src/index.ts:58`](../../packages/fs/fs/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](filesystem.md) | 中文
|
||||
|
||||
可选的文件系统能力由四个部分组成:[dsh-fs](../../packages/fs/fs) 拥有 `ctx.fs` 以及带可选版本守卫的原子文本操作;[dsh-fs-local](../../packages/fs/fs-local) 实现本地磁盘后端;[dsh-fs-policy](../../packages/fs/fs-policy) 通过事件(而非服务)添加观测状态与新鲜度规则;[dsh-tool-fs](../../packages/fs/tool-fs) 直接执行面向模型的 read/write/edit 调用并渲染窗口。它位于 agent loop(智能体循环)主干之外;替换后端不会改变策略或工具 schema。
|
||||
可选的文件系统能力由四个部分组成:[dsh-fs](../../packages/fs/fs) 拥有 `ctx.fs` 以及带可选守卫的原子文本操作;[dsh-fs-local](../../packages/fs/fs-local) 实现本地磁盘后端;[dsh-fs-policy](../../packages/fs/fs-policy) 记录观测到的存在或缺失状态,并通过事件(而非服务)添加新鲜度规则;[dsh-tool-fs](../../packages/fs/tool-fs) 直接执行面向模型的 read/write/edit 调用并渲染窗口。它位于 agent loop(智能体循环)主干之外;替换后端不会改变策略或工具 schema。
|
||||
|
||||
该模型是**加法式而非减法式**的:`ctx.fs` 本身就是一个完整、无约束的文本存储 seam(`write` 无条件创建或覆盖,`edit` 无条件替换字面文本)。`dsh-fs-policy` 是一个插件,通过裁决 `fs/*` waterfall(瀑布式事件)在上层*叠加*策略;移除它只会暴露裸提供方,而不会破坏工具,因为工具与策略之间没有方法级耦合。加载了 `dsh-tool-fs` 的部署通常也应加载 `dsh-fs-policy`,使默认行为为「先读后写/编辑」。
|
||||
|
||||
@@ -113,7 +113,7 @@ interface FsDirEntry {
|
||||
|
||||
## 写入与编辑守卫(提供方约定)
|
||||
|
||||
`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
|
||||
/**
|
||||
@@ -181,7 +181,18 @@ 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` 结果。下方生成的 [cordis surface](#cordis-surface) 展示确切签名。
|
||||
`fs/write-intent` 与 `fs/edit-intent` 是**单槽决策 waterfall**:工具分发时附带一个默认 thunk(返回 `undefined`,即裸提供方),监听方完全决策而不调用 `next()`。该槽按注册顺序先到先得——由策略插件占据是部署约定,而非强制不变式。`fs/observed` 是一个即发即弃的记录事件,携带 `FsObservation`:存在于某个版本,或确认缺失。该事件通过普通 `ctx.emit` 分发;其监听方必须是同步的、仅产生副作用,因为工具不会捕获该 emit 抛出的异常——抛出异常的监听方可能取代读取操作原本待返回的错误,或使工具在变更已经成功后返回 `isError` 结果。下方生成的 [cordis surface](#cordis-surface) 展示确切签名。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One authoritative observation of a target. A present observation carries the
|
||||
* version used by guarded replacement; an absent observation authorizes only a
|
||||
* guarded create, never an edit.
|
||||
*/
|
||||
type FsObservation =
|
||||
| { readonly kind: 'present'; readonly version: FsVersion }
|
||||
| { readonly kind: 'absent' }
|
||||
```
|
||||
|
||||
## 执行上下文(策略插件)
|
||||
|
||||
@@ -209,7 +220,7 @@ interface FsPolicyExec {
|
||||
|
||||
## 读取结果(消费方 / 读取渲染)
|
||||
|
||||
文本读取受行窗口、字节上限和后端限制约束。达到字节上限后,扫描仍会继续,但不再保留更多行,因此 `totalLines` 仍为精确值。面向模型的 `read` 工具渲染的结果纯粹是展示性的;不存在 `full`/`partial` 视图区分——授权基于新鲜度(工具直接用 stat 的版本 emit `fs/observed`),因此任何窗口化读取在文件未变时都能授权后续的 write/edit。读取窗口化与此结果形状位于 `dsh-tool-fs`(拥有读取操作的执行器)中,而非策略插件中。
|
||||
文本读取受行窗口、字节上限和后端限制约束。达到字节上限后,扫描仍会继续,但不再保留更多行,因此 `totalLines` 仍为精确值。面向模型的 `read` 工具渲染的结果纯粹是展示性的;不存在 `full`/`partial` 视图区分——授权基于新鲜度(工具以 stat 的版本 emit 表示存在的 `fs/observed`),因此任何窗口化读取在文件未变时都能授权后续的 write/edit。元数据未命中时,工具会在返回 `FS_NOT_FOUND` 前 emit 缺失观测,使后续带防护的写入可以重新创建外部删除的目标,但不会授权 edit。读取窗口化与此结果形状位于 `dsh-tool-fs`(拥有读取操作的执行器)中,而非策略插件中。
|
||||
|
||||
```ts type-equiv
|
||||
/** Outcome of a bounded text read — what {@link formatReadOutput} renders. */
|
||||
@@ -227,7 +238,7 @@ interface FileReadOutcome {
|
||||
|
||||
## 已观测文件状态(策略插件)
|
||||
|
||||
已观测状态是 `dsh-fs-policy` 插件内部持有的 `WeakMap<owner, Map<targetKey, { version }>>`。**当且仅当**所有者已读取、写入或编辑过该目标时(每次成功都 emit `fs/observed`),条目才存在,因此其存在本身就是先前观测的记录——没有单独的 `hasRead` 标志,也没有视图区分。所有者从事件 actor 推导(通常是 `exec.agent.session`),被视为不透明且从不读取。成功的 read/write/edit 会刷新该所有者对应的已记录版本;dispose(资源释放)时丢弃全部数据(HMR(热模块替换)安全)。
|
||||
已观测状态是 `dsh-fs-policy` 插件内部持有的 `WeakMap<owner, Map<targetKey, FsObservation>>`。映射中没有条目表示未见;`{ kind: 'absent' }` 表示 `read` 的元数据未命中,或 `str_replace_editor` 的 `view`、`str_replace`、`insert` 命令发生元数据未命中,从而确认缺失;`{ kind: 'present', version }` 表示 read、write 或 edit 观测到该版本。写入决策把未见和缺失映射到 `createIfAbsent`,把存在映射到 `replaceIfVersion`;编辑决策把未见映射到 `FS_NOT_OBSERVED`,把缺失映射到 `FS_NOT_FOUND`,把存在映射到其版本守卫。所有者从事件 actor 推导(通常是 `exec.agent.session`),被视为不透明且从不读取。dispose(资源释放)时丢弃全部数据(HMR(热模块替换)安全),策略不执行任何文件系统 I/O。
|
||||
|
||||
## 错误分类体系(提供方约定)
|
||||
|
||||
@@ -254,15 +265,15 @@ type FsErrorCode =
|
||||
| 'FS_ABORTED'
|
||||
```
|
||||
|
||||
目录列表使用 `FS_NOT_DIRECTORY`、`FS_PERMISSION_DENIED` 与 `FS_IO_ERROR` 区分已存在但并非目录的目标、被拒绝的列表操作和意外的后端 I/O 失败。`FS_SANDBOX_DENIED` 是强制执行沙箱的后端(`dsh-fs-sandbox`)所作的策略拒绝——模式边界拒绝了写入/编辑——与 `FS_PERMISSION_DENIED`(宿主内核拒绝)不同。`FS_NOT_OBSERVED` 表示策略插件没有此所有者的先前观察记录(或 `createIfAbsent` 遇到了现有文件)。`FS_STALE_VERSION` 表示后端版本不再与观察到的版本匹配(或编辑操作遇到缺失目标)。新鲜度授权没有部分/完整之分,因此不存在 `FS_PARTIAL_OBSERVATION`。
|
||||
目录列表使用 `FS_NOT_DIRECTORY`、`FS_PERMISSION_DENIED` 与 `FS_IO_ERROR` 区分已存在但并非目录的目标、被拒绝的列表操作和意外的后端 I/O 失败。`FS_SANDBOX_DENIED` 是强制执行沙箱的后端(`dsh-fs-sandbox`)所作的策略拒绝——模式边界拒绝了写入/编辑——与 `FS_PERMISSION_DENIED`(宿主内核拒绝)不同。`FS_NOT_OBSERVED` 表示策略插件没有此所有者的先前观测记录(或 `createIfAbsent` 遇到了现有文件)。`FS_NOT_FOUND` 也表示策略因确认缺失而拒绝 edit。`FS_STALE_VERSION` 表示后端版本不再与观测到的版本匹配(或提供方本身收到针对缺失目标的 edit)。新鲜度授权没有部分/完整之分,因此不存在 `FS_PARTIAL_OBSERVATION`。
|
||||
|
||||
## 文件 IO 不设超时
|
||||
|
||||
`read`/`write`/`edit` **不**接受 `timeoutMs`,提供方约定也不设置截止时间——不同于 bash 与 web(它们消费 [`@deepseek-ai/dsh-timeout`](../../packages/util/timeout/README.md))以及 bash 支撑的 `glob`/`grep`(其声明的 `timeoutMs` 由 `@deepseek-ai/dsh-timeout-policy` 强制执行):那些是进程支撑的,截止时间可以真正终止工作。本地系统调用至多是尽力中止——超时无法迫使进行中的 `fsync`/`rename` 停下,因此这里的截止时间会成为无法兑现承诺的旋钮,而且恰好落在"显式优于隐式"禁止隐式默认值的位置。两个参照 agent(Claude Code、Codex)出于同样原因不给文件 IO 计时;取消仍通过工具执行 signal 传播,在系统调用边界尽力中止。
|
||||
`read`/`write`/`edit` **不**接受 `timeoutMs`,提供方约定也不设置截止时间——不同于 bash 与 web(它们消费 [`@deepseek-ai/dsh-timeout`](../../packages/util/timeout/README.md))以及 subprocess 支撑的 `glob`/`grep`(其声明的 `timeoutMs` 由 `@deepseek-ai/dsh-timeout-policy` 强制执行):那些是进程支撑的,截止时间可以真正终止工作。本地系统调用至多是尽力中止——超时无法迫使进行中的 `fsync`/`rename` 停下,因此这里的 `timeoutMs` 会成为 seam 无法强制执行的截止时间,而且恰好落在"显式优于隐式"禁止隐式默认值的位置。取消仍通过工具执行 signal 传播,在系统调用边界尽力中止。
|
||||
|
||||
## 服务与插件
|
||||
|
||||
`FileSystem`(`ctx.fs`,abstract)拥有提供方原语:`resolve`、`processPath`、`fileUrl`、`contains`、`stat`、`lstat`、`readText`、`streamText`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:对写入/编辑意图 waterfall 作出决策(提供 `createIfAbsent`/`replaceIfVersion`/`{ version }`,或抛出 `FS_NOT_OBSERVED`),并在 `fs/observed` 上记录。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall,并 emit 记录事件。下方生成的 [`ctx.fs` 小节](#ctxfs--filesystem-abstract-seam) 展示确切的 `ctx.fs` 签名。
|
||||
`FileSystem`(`ctx.fs`,abstract)拥有提供方原语:`resolve`、`processPath`、`fileUrl`、`contains`、`stat`、`lstat`、`readText`、`streamText`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:根据未见/缺失/存在状态对写入与编辑意图 waterfall 作出决策,并记录 `FsObservation` 值。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall,并 emit 记录事件。下方生成的 [`ctx.fs` 小节](#ctxfs--filesystem-abstract-seam) 展示确切的 `ctx.fs` 签名。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
@@ -376,7 +387,7 @@ abstract listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>
|
||||
* @param target - the resolved target to write.
|
||||
* @param content - the full new file content.
|
||||
* @param expected - the write intent guarding the write; omit for unconditional.
|
||||
* @param signal - aborts before the atomic rename takes effect.
|
||||
* @param signal - aborts before atomic publication takes effect.
|
||||
* @param sandboxPolicy - the per-call mode and workspace root this write
|
||||
* runs under; a sandboxing backend fences the write by it, the bare backend
|
||||
* ignores it. Omit to leave the backend its own default.
|
||||
@@ -391,7 +402,7 @@ abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent,
|
||||
* @param target - the resolved target to edit.
|
||||
* @param edit - the literal search/replace request.
|
||||
* @param expected - the version guard; omit for an unconditional edit.
|
||||
* @param signal - aborts before the atomic rename takes effect.
|
||||
* @param signal - aborts before atomic publication takes effect.
|
||||
* @param sandboxPolicy - the per-call mode and workspace root this edit runs
|
||||
* under; a sandboxing backend fences the edit by it, the bare backend
|
||||
* ignores it. Omit to leave the backend its own default.
|
||||
@@ -402,7 +413,7 @@ abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version:
|
||||
|
||||
Types: [SandboxExecutionPolicy](sandbox.md)
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:83`](../../packages/fs/fs/src/index.ts)
|
||||
Source: [`packages/fs/fs/src/index.ts:86`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
<a id="fs-events"></a>
|
||||
|
||||
@@ -425,27 +436,28 @@ Single-slot decision for the next FileSystem.editText. Calling `next()` yields a
|
||||
'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>
|
||||
```
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:64`](../../packages/fs/fs/src/index.ts)
|
||||
Source: [`packages/fs/fs/src/index.ts:66`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
<a id="fsobserved--emit"></a>
|
||||
|
||||
#### `fs/observed` — emit
|
||||
|
||||
Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited.
|
||||
Record an authoritative positive or negative observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Record a successful observation. Listeners must be synchronous recorders:
|
||||
* throws fail the tool call and returned promises are not awaited.
|
||||
* @param target - the target that was read/written/edited.
|
||||
* @param version - the version the actor now holds as its observation.
|
||||
* Record an authoritative positive or negative observation. Listeners must
|
||||
* be synchronous recorders: throws fail the tool call and returned promises
|
||||
* are not awaited.
|
||||
* @param target - the target whose presence or absence was observed.
|
||||
* @param observation - present with its version, or confirmed absent.
|
||||
* @param actor - the observing tool-execution context; undefined records nothing useful.
|
||||
* @mode emit
|
||||
*/
|
||||
'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void
|
||||
'fs/observed'(target: FsTarget, observation: FsObservation, actor: object | undefined): void
|
||||
```
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:73`](../../packages/fs/fs/src/index.ts)
|
||||
Source: [`packages/fs/fs/src/index.ts:76`](../../packages/fs/fs/src/index.ts)
|
||||
|
||||
<a id="fswrite-intent--waterfall"></a>
|
||||
|
||||
@@ -465,5 +477,5 @@ Single-slot decision for the next FileSystem.writeText. Calling `next()` yields
|
||||
'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>
|
||||
```
|
||||
|
||||
Source: [`packages/fs/fs/src/index.ts:56`](../../packages/fs/fs/src/index.ts)
|
||||
Source: [`packages/fs/fs/src/index.ts:58`](../../packages/fs/fs/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -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/subsystems/llm-streaming.md
|
||||
llm-streaming.md: 0cfce7a06d1db73268e2457c4979d1b1ccc889b7
|
||||
llm-streaming.zh.md: 19541462805c50b0ed446a346523d20de91d0351
|
||||
llm-streaming.md: 4d450c19ec2bbfacabcefc83466e67c8a6c82bd6
|
||||
llm-streaming.zh.md: 777c44eaff2b1ee6e5939e04e580d6ad34b1ad1a
|
||||
|
||||
@@ -87,8 +87,8 @@ Producer identity and content shape are independent. `kind` answers *who produce
|
||||
*
|
||||
* `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.
|
||||
* producers share one form, 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
|
||||
@@ -209,11 +209,9 @@ 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 - and prove it with a wire-level test.
|
||||
- **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 plus provider/model fields 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.
|
||||
|
||||
## `ResolvedRetryPolicy`
|
||||
|
||||
Provider configuration resolves before route registration into an immutable discriminated union. Normal mode carries `mode: 'normal'`, finite `maxRetries`, `retryableCodes`, and required `initialDelayMs`, `maxDelayMs`, and `jitterRatio`; always mode carries `mode: 'always'` and the same required backoff fields without a finite maximum. `LlmService.providerRetryPolicy(provider)` returns the currently registered value and supplies normal defaults when the adapter omits one; `llmRetryPolicyOf(stream)` returns the exact serving registration's captured value after that call enters its final adapter boundary, so later route disposal or replacement cannot change an in-flight failure's recovery policy. The [generated config catalog](../config-catalog.md) owns the optional input shapes.
|
||||
|
||||
@@ -87,8 +87,8 @@ interface MessageSourceMap {
|
||||
*
|
||||
* `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.
|
||||
* producers share one form, 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
|
||||
@@ -213,11 +213,9 @@ 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` 才会传递该状态。该适配器负责校验状态并拥有所有跨模型或跨提供方转换;其他适配器只会收到提供方无关的内容以及提供方/模型字段,不会收到私有状态。
|
||||
|
||||
该约定由两个有意保持独立的实现锁定:`dsh-llm-deepseek`(直接 fetch,SSE(Server-Sent Events)分帧经由 `eventsource-parser`)和 `dsh-llm-pi-ai`(通过 `@earendil-works/pi-ai` 实现的通用多提供方适配器)。基于库的适配器覆盖 finish 分片错误路径,而传输边界测试证明每个空闲 watchdog 都会停止其实际请求。
|
||||
|
||||
## `ResolvedRetryPolicy`
|
||||
|
||||
提供方配置会在路由注册前解析为不可变的可辨识联合。normal mode 携带 `mode: 'normal'`、有限的 `maxRetries`、`retryableCodes`,以及必填的 `initialDelayMs`、`maxDelayMs` 与 `jitterRatio`;always mode 携带 `mode: 'always'` 和相同的必填退避字段,但没有有限上限。`LlmService.providerRetryPolicy(provider)` 返回当前注册的值,并在适配器省略策略时提供 normal 默认值;调用进入最终适配器边界后,`llmRetryPolicyOf(stream)` 返回为其提供服务的确切注册所捕获的值,因此之后释放或替换路由都无法改变进行中失败的恢复策略。可选输入形状由[生成的配置目录](../config-catalog.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/subsystems/lsp.md
|
||||
lsp.md: f8fa2be3d79c0aff9b288f61bf64bbb91e327f1d
|
||||
lsp.zh.md: fc9214fa7b10c2dac2a6cd90e0c29776313653f4
|
||||
lsp.md: 96a365a7f16e53a6c8b28fdc5997724bcbf81115
|
||||
lsp.zh.md: 28b16cab9cb302de7db76be0b12788d1bef04541
|
||||
|
||||
@@ -14,7 +14,7 @@ The seam and model expose exactly four semantic queries; the union is closed, so
|
||||
/**
|
||||
* The four semantic queries the seam and model expose. A closed union: adding an operation is a
|
||||
* compile-enforced change across the seam, providers, and the tool. Symbols and call hierarchy are
|
||||
* deliberately deferred (they need different schemas).
|
||||
* not operations here; they need different schemas.
|
||||
*/
|
||||
type LspOperation = 'goToDefinition' | 'findReferences' | 'goToImplementation' | 'hover'
|
||||
```
|
||||
|
||||
@@ -14,7 +14,7 @@ seam 与模型恰好公开 4 项语义查询;该联合是闭合的,因此新
|
||||
/**
|
||||
* The four semantic queries the seam and model expose. A closed union: adding an operation is a
|
||||
* compile-enforced change across the seam, providers, and the tool. Symbols and call hierarchy are
|
||||
* deliberately deferred (they need different schemas).
|
||||
* not operations here; they need different schemas.
|
||||
*/
|
||||
type LspOperation = 'goToDefinition' | 'findReferences' | 'goToImplementation' | 'hover'
|
||||
```
|
||||
|
||||
@@ -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/subsystems/persistence.md
|
||||
persistence.md: 991efe39a059e08cdf7904b473486b336cb2071c
|
||||
persistence.zh.md: 2391eeb2a751388b6aa139d0646b52fe5af04601
|
||||
persistence.md: 640e2c0122b01ae869d20c1805742abad782c95b
|
||||
persistence.zh.md: 7e28d6d1f63a78741840fb74194d3249696423ae
|
||||
|
||||
@@ -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/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 seam is a [capability seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md): one abstract service ([dsh-session-persistence](../../packages/session/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
|
||||
|
||||
@@ -200,7 +200,7 @@ interface SessionPersistenceSnapshot {
|
||||
|
||||
## The backends
|
||||
|
||||
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`:
|
||||
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 the shared `runPersistenceContract` suite:
|
||||
|
||||
- **[dsh-session-persistence-jsonl](../../packages/session/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/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/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)。
|
||||
该 seam 是一个[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md):一个抽象服务([dsh-session-persistence](../../packages/session/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 检查点
|
||||
|
||||
@@ -200,7 +200,7 @@ interface SessionPersistenceSnapshot {
|
||||
|
||||
## 后端
|
||||
|
||||
两者都实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 上执行 locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots,观察方法可选支持取消),并通过 `runPersistenceContract`:
|
||||
两者都实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 上执行 locate/create/append/prepare/load/inspect/readFrom/list/listSnapshots,观察方法可选支持取消),并通过共享的 `runPersistenceContract` 套件:
|
||||
|
||||
- **[dsh-session-persistence-jsonl](../../packages/session/session-persistence-jsonl)**——每个会话一份仅追加的逻辑 JSONL 日志,默认存储为带 checksum 的连续 Zstandard frame,也可配置为原始行;支持崩溃安全的原子写入、被中断轮次的恢复以及读取/回放路径。
|
||||
- **[dsh-session-persistence-sqlite](../../packages/session/session-persistence-sqlite)**:基于 `node:sqlite`,每个 `SessionEvent` 一行。行结构 `(session_id, seq, type, time, data, source_event_seqs, surface_op)` 与事件 1:1 映射(包含可选的 surface 元数据),因此没有需要保持同步的并行持久化 schema。
|
||||
|
||||
@@ -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/subsystems/session-projection.md
|
||||
session-projection.md: ddb3877618904d3185eba7d9d351afbcc81b2d04
|
||||
session-projection.zh.md: 39e6099130ba0d91e72e8f1afd5661e5894f8d0f
|
||||
session-projection.md: d91a3faa50dc092d89aa2c7d5ce1e6118df7ebd6
|
||||
session-projection.zh.md: 7dac3db8470a2941b711db6a30fced8dbe7c7a8d
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](session-projection.zh.md)
|
||||
|
||||
The session-projection seam — a [capability seam](../capability-seams.md) through which domain host plugins serve whole current values of log-derived per-session state to client carriers: the Service Definition and registry ([dsh-session-projection](../../packages/session/session-projection), `ctx.sessionProjections`), domain contributors (each registering one pure unit), and carriers ([dsh-host-apiproxy](../../packages/host/apiproxy)'s history tail page and `session/projection` push frame today). It is one optional capability, not part of the agent-loop spine. The framework drives, the domain computes: the registry subscribes to `session/event` once and folds every committed event through every unit; domains hold no subscriptions and clients never fold domain events — they receive finished values. Design authority: the [session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md); drive/cache/feed contracts: the [package README](../../packages/session/session-projection/README.md).
|
||||
The session-projection seam — a [capability seam](../capability-seams.md) through which domain host plugins serve whole current values of log-derived per-session state to client carriers: the Service Definition and registry ([dsh-session-projection](../../packages/session/session-projection), `ctx.sessionProjections`), domain contributors (each registering one pure unit), and carriers ([dsh-host-apiproxy](../../packages/host/apiproxy)'s history tail page and `session/projection` push frame). It is one optional capability, not part of the agent-loop spine. The framework drives, the domain computes: the registry subscribes to `session/event` once and folds every committed event through every unit; domains hold no subscriptions and clients never fold domain events — they receive finished values. Design authority: the [session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md); drive/cache/feed contracts: the [package README](../../packages/session/session-projection/README.md).
|
||||
|
||||
Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](session-projection.md) | 中文
|
||||
|
||||
会话投影 seam 是一项[能力 seam](../capability-seams.md):领域 host 插件经由它向客户端载体供给按会话的日志派生状态的当前全量值;三方分别是 Service Definition 与注册表([dsh-session-projection](../../packages/session/session-projection),`ctx.sessionProjections`)、领域贡献方(每个领域注册一个纯单元)与载体(今天是 [dsh-host-apiproxy](../../packages/host/apiproxy) 的历史尾页与 `session/projection` 推送帧)。它是一项可选能力,不属于 agent loop(智能体循环)主干。框架负责驱动,领域负责计算:注册表只订阅一次 `session/event`,并把每个已提交事件折叠进每个单元;领域不持有任何订阅,客户端也从不折叠领域事件——它们收到的是成品值。设计权威:[session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md);驱动、缓存与变更流约定:[包(package)README](../../packages/session/session-projection/README.md)。
|
||||
会话投影 seam 是一项[能力 seam](../capability-seams.md):领域 host 插件经由它向客户端载体供给按会话的日志派生状态的当前全量值;三方分别是 Service Definition 与注册表([dsh-session-projection](../../packages/session/session-projection),`ctx.sessionProjections`)、领域贡献方(每个领域注册一个纯单元)与载体([dsh-host-apiproxy](../../packages/host/apiproxy) 的历史尾页与 `session/projection` 推送帧)。它是一项可选能力,不属于 agent loop(智能体循环)主干。框架负责驱动,领域负责计算:注册表只订阅一次 `session/event`,并把每个已提交事件折叠进每个单元;领域不持有任何订阅,客户端也从不折叠领域事件——它们收到的是成品值。设计权威:[session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md);驱动、缓存与变更流约定:[包(package)README](../../packages/session/session-projection/README.md)。
|
||||
|
||||
源码:[`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts)
|
||||
|
||||
|
||||
@@ -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/subsystems/session.md
|
||||
session.md: a5162c77d510c5ab66fa2dd6789693e1449970fb
|
||||
session.zh.md: 989a3f5368aa0b0475dec4272fde724fd4a98230
|
||||
session.md: f5b9e63e2320885cc41b30a09398dd341700152d
|
||||
session.zh.md: 985e0a448d1cf860ccbb0f2885d855ad6830af9f
|
||||
|
||||
@@ -171,7 +171,7 @@ 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.
|
||||
Canonical form represents an empty system prompt or tool list as an absent field, matching how requests are built. Legacy v0 logs containing the legacy `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`
|
||||
|
||||
@@ -266,7 +266,8 @@ type SurfaceEventType =
|
||||
* (inclusive) through `end` (inclusive) with this node. Both must exist as
|
||||
* surface nodes in the current surface. `start === end` replaces a single
|
||||
* node. The node's {@link SessionEvent.sourceEventSeqs} must include every
|
||||
* shadowed surface node. Used by compaction and possible other manipulations.
|
||||
* shadowed surface node. Used by compaction; any surface-replacing producer
|
||||
* may use it.
|
||||
*/
|
||||
type SurfaceOp =
|
||||
| 'append'
|
||||
@@ -580,6 +581,8 @@ Activity ordering excludes the boundary through `lastActivityTime(events)`: pick
|
||||
|
||||
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 generated [persistence log event catalog](../persistence-catalog.md) enumerates every core and plugin-contributed event with its payload, surface badge, and declaration site; the compaction seam's `compact/*` semantics are discussed on [compaction.md](compaction.md).
|
||||
|
||||
When several events in one plugin-owned family assemble into one Web Client Conversation Node, every start, update, result, resource, or interruption event in that family carries or independently derives the same stable business id. This requirement applies to correlated Node families, not to every Session event; it lets the client group each event without guessing from adjacency or scanning history. See the [Conversation Node cookbook](../cookbook/adding-a-conversation-node.md).
|
||||
|
||||
The hook bridges' `hook/invoked` / `hook/result` 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
|
||||
|
||||
@@ -173,7 +173,7 @@ interface EpochHeader {
|
||||
}
|
||||
```
|
||||
|
||||
规范形式:空系统提示词和空工具列表都表示为字段缺失,与请求构建方式一致。包含已移除的 `request/header-delta` 事件或完整快照原因为 `fallback` 的旧版 v0 日志,会在 seed、append 和持久化加载边界被拒绝,而不会以不完整方式回放。
|
||||
规范形式:空系统提示词和空工具列表都表示为字段缺失,与请求构建方式一致。包含旧版 `request/header-delta` 事件或完整快照原因为 `fallback` 的旧版 v0 日志,会在 seed、append 和持久化加载边界被拒绝,而不会以不完整方式回放。
|
||||
|
||||
### 路由容量事件:`request/context`
|
||||
|
||||
@@ -268,7 +268,8 @@ type SurfaceEventType =
|
||||
* (inclusive) through `end` (inclusive) with this node. Both must exist as
|
||||
* surface nodes in the current surface. `start === end` replaces a single
|
||||
* node. The node's {@link SessionEvent.sourceEventSeqs} must include every
|
||||
* shadowed surface node. Used by compaction and possible other manipulations.
|
||||
* shadowed surface node. Used by compaction; any surface-replacing producer
|
||||
* may use it.
|
||||
*/
|
||||
type SurfaceOp =
|
||||
| 'append'
|
||||
@@ -584,6 +585,8 @@ interface TurnEndReasonMap {
|
||||
|
||||
插件可以通过 declaration merging 添加额外的 `SessionEventMap` 类型。这些是**仅日志**事件:不是 `SurfaceEventType`(不携带 `surfaceOp`,不参与派生历史)。事件所有方决定它们属于一个开放的执行轮次,还是可以独立位于轮次之间,并在自己的不变量配套插件中强制所需关系。生成的[持久化日志事件目录](../persistence-catalog.md)会列出每个核心或插件贡献的事件,以及其 payload、surface 标记和声明位置;压缩 seam 的 `compact/*` 语义在 [compaction.md](compaction.md) 中讨论。
|
||||
|
||||
如果同一个插件事件族中的多条事件要组装成一个 Web Client Conversation Node,该事件族中的每条 start、update、result、resource 或 interruption 事件都必须携带或独立推导出同一个稳定业务 id。此要求只约束需要关联的 Node 事件族,并不要求每条 Session 事件都有业务 id;Client 因此无须根据相邻关系猜测归属,也无须扫描历史。参见 [Conversation Node 实操手册](../cookbook/adding-a-conversation-node.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))。
|
||||
|
||||
## 持久性约定
|
||||
|
||||
@@ -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/subsystems/skills.md
|
||||
skills.md: 1c3d683c6fb2c2ac26e00c32329e41b8d4636fb1
|
||||
skills.zh.md: b38effdd5858625bb1aa5f10108d5d654f692b7c
|
||||
skills.md: 696f759179203230bf748c8a4be2cee333acd9a3
|
||||
skills.zh.md: d2244e86f0ceaf77c1eab02508dc3f07df727dbb
|
||||
|
||||
@@ -80,7 +80,7 @@ Chokidar watches existing roots for direct bundle/flat-entry additions and remov
|
||||
|
||||
## Skill identity
|
||||
|
||||
Skill names are kebab-case (`^[a-z0-9]+(?:-[a-z0-9]+)*$`). The local provider accepts directory bundles (`<name>/SKILL.md`) and flat Markdown files (`<name>.md`). Nested recursive `**/SKILL.md` discovery is intentionally outside v1.
|
||||
Skill names are kebab-case (`^[a-z0-9]+(?:-[a-z0-9]+)*$`). The local provider accepts directory bundles (`<name>/SKILL.md`) and flat Markdown files (`<name>.md`). Nested recursive `**/SKILL.md` discovery is not supported.
|
||||
|
||||
```ts type-equiv
|
||||
/** Origin bucket for a skill contribution. The value is prompt-visible metadata, not precedence by itself. */
|
||||
|
||||
@@ -80,7 +80,7 @@ Chokidar 会监视现有根目录中直属 bundle 和平铺条目的添加与移
|
||||
|
||||
## skill 身份
|
||||
|
||||
skill 名称为 kebab-case(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)。本地提供方接受目录包(`<name>/SKILL.md`)和扁平 Markdown 文件(`<name>.md`)。嵌套递归的 `**/SKILL.md` 发现有意不在 v1 范围内。
|
||||
skill 名称为 kebab-case(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)。本地提供方接受目录包(`<name>/SKILL.md`)和扁平 Markdown 文件(`<name>.md`)。嵌套递归的 `**/SKILL.md` 发现不受支持。
|
||||
|
||||
```ts type-equiv
|
||||
/** Origin bucket for a skill contribution. The value is prompt-visible metadata, not precedence by itself. */
|
||||
|
||||
@@ -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/subsystems/storage.md
|
||||
storage.md: 94d3c3f39a65462cd8cd58a65b02066d5bad4b8c
|
||||
storage.zh.md: c3cea8eb5e44f2e589b0196dc690c6473e3552ca
|
||||
storage.md: 3234fdf9587dc2853b4f9ec4205d230aae4b633a
|
||||
storage.zh.md: fb13ec96099f2173478aecf1b91799237755414c
|
||||
|
||||
@@ -44,7 +44,7 @@ interface StorageBackend {
|
||||
}
|
||||
```
|
||||
|
||||
A backend owns one medium (a file-tree root, a database file) and exposes optional data-shape facets; `kv` is the only facet today. `KvFacet.open(descriptor)` opens one named unit — `KvUnitDescriptor` carries the name, format version, table names, and whether a global singleton slot exists — and returns a `KvUnit` with `loadAll`, `putRecord`, `deleteRecord`, `setGlobal`, and `close`. Unit and table names must match `UNIT_NAME_RE` (safe as a file name and as a SQL identifier segment); record keys are arbitrary strings that never reach file paths. A unit does not serialize concurrent writes — ordering belongs to the caller — but each single call is atomic on the medium and durable once resolved. A medium stamped with a different version rejects `version-mismatch`; one that cannot be parsed as the unit rejects `malformed-medium` (no migration, pre-release stance). [`backend.ts`](../../packages/storage/storage/src/backend.ts) is the normative clause-by-clause contract, and the shared conformance suite in [`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) asserts every clause against each backend. The [json backend](../../packages/storage/storage-json/README.md) republishes one whole human-readable file per unit atomically; the [sqlite backend](../../packages/storage/storage-sqlite/README.md) stores document-per-row in one database, the route for high-churn domains.
|
||||
A backend owns one medium (a file-tree root, a database file) and exposes optional data-shape facets; `kv` is the sole facet. `KvFacet.open(descriptor)` opens one named unit — `KvUnitDescriptor` carries the name, format version, table names, and whether a global singleton slot exists — and returns a `KvUnit` with `loadAll`, `putRecord`, `deleteRecord`, `setGlobal`, and `close`. Unit and table names must match `UNIT_NAME_RE` (safe as a file name and as a SQL identifier segment); record keys are arbitrary strings that never reach file paths. A unit does not serialize concurrent writes — ordering belongs to the caller — but each single call is atomic on the medium and durable once resolved. A medium stamped with a different version rejects `version-mismatch`; one that cannot be parsed as the unit rejects `malformed-medium` (no migration, pre-release stance). [`backend.ts`](../../packages/storage/storage/src/backend.ts) is the normative clause-by-clause contract, and the shared conformance suite in [`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) asserts every clause against each backend. The [json backend](../../packages/storage/storage-json/README.md) republishes one whole human-readable file per unit atomically; the [sqlite backend](../../packages/storage/storage-sqlite/README.md) stores document-per-row in one database, the route for high-churn domains.
|
||||
|
||||
## Declaring a domain
|
||||
|
||||
@@ -122,7 +122,7 @@ interface DomainChangedBase {
|
||||
type DomainChanged = DomainChangedPut | DomainChangedDeleted
|
||||
```
|
||||
|
||||
`put` (inserts, overwrites, and global writes) carries the new snapshot in `value` — never the old value; a diffing consumer keeps its own previous snapshot. `deleted` is a tombstone with no value. The event is a notification, not a transaction participant: the commit point has passed at emission, so a synchronously throwing listener is contained with a logged warning rather than rejecting the already-durable write, and emitted values equal the in-memory state at emission. The event is in-process only; cross-process change push is deferred work recorded in the [package README](../../packages/storage/storage-domain/README.md).
|
||||
`put` (inserts, overwrites, and global writes) carries the new snapshot in `value` — never the old value; a diffing consumer keeps its own previous snapshot. `deleted` is a tombstone with no value. The event is a notification, not a transaction participant: the commit point has passed at emission, so a synchronously throwing listener is contained with a logged warning rather than rejecting the already-durable write, and emitted values equal the in-memory state at emission. The event is in-process only; cross-process change push is a recorded limitation ([package README](../../packages/storage/storage-domain/README.md)).
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ interface StorageBackend {
|
||||
}
|
||||
```
|
||||
|
||||
一个后端拥有一个介质(一棵文件树的根目录、一个数据库文件),并暴露可选的数据形状 facet;目前 `kv` 是唯一的 facet。`KvFacet.open(descriptor)` 打开一个具名 unit——`KvUnitDescriptor` 携带名称、格式版本、表名清单,以及是否存在全局单例槽位——并返回提供 `loadAll`、`putRecord`、`deleteRecord`、`setGlobal` 和 `close` 的 `KvUnit`。unit 名与表名必须匹配 `UNIT_NAME_RE`(既可安全用作文件名,也可安全用作 SQL 标识符片段);记录键是任意字符串,绝不进入文件路径。unit 不对并发写入做串行化——顺序由调用方负责——但每次单独调用在介质上都是原子的,且 resolve 后即已持久。介质上记录的版本与之不同时拒绝 `version-mismatch`;无法按该 unit 解析的介质拒绝 `malformed-medium`(不做迁移:预发布立场)。[`backend.ts`](../../packages/storage/storage/src/backend.ts) 是逐条款的规范性约定,[`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) 中的共享一致性套件对每个后端断言其中每一条款。[json 后端](../../packages/storage/storage-json/README.md)以原子方式为每个 unit 整文件重新发布一份人类可读文件;[sqlite 后端](../../packages/storage/storage-sqlite/README.md)在单个数据库中按一行一文档存储,是高频更新领域的路由选择。
|
||||
一个后端拥有一个介质(一棵文件树的根目录、一个数据库文件),并暴露可选的数据形状 facet;`kv` 是唯一的 facet。`KvFacet.open(descriptor)` 打开一个具名 unit——`KvUnitDescriptor` 携带名称、格式版本、表名清单,以及是否存在全局单例槽位——并返回提供 `loadAll`、`putRecord`、`deleteRecord`、`setGlobal` 和 `close` 的 `KvUnit`。unit 名与表名必须匹配 `UNIT_NAME_RE`(既可安全用作文件名,也可安全用作 SQL 标识符片段);记录键是任意字符串,绝不进入文件路径。unit 不对并发写入做串行化——顺序由调用方负责——但每次单独调用在介质上都是原子的,且 resolve 后即已持久。介质上记录的版本与之不同时拒绝 `version-mismatch`;无法按该 unit 解析的介质拒绝 `malformed-medium`(不做迁移:预发布立场)。[`backend.ts`](../../packages/storage/storage/src/backend.ts) 是逐条款的规范性约定,[`tests/contract.ts`](../../packages/storage/storage/tests/contract.ts) 中的共享一致性套件对每个后端断言其中每一条款。[json 后端](../../packages/storage/storage-json/README.md)以原子方式为每个 unit 整文件重新发布一份人类可读文件;[sqlite 后端](../../packages/storage/storage-sqlite/README.md)在单个数据库中按一行一文档存储,是高频更新领域的路由选择。
|
||||
|
||||
## 声明领域
|
||||
|
||||
@@ -122,7 +122,7 @@ interface DomainChangedBase {
|
||||
type DomainChanged = DomainChangedPut | DomainChangedDeleted
|
||||
```
|
||||
|
||||
`put`(插入、覆写和 global 写入)在 `value` 中携带新快照——绝不携带旧值;需要做差异比较的消费方自行保留上一份快照。`deleted` 是不携带值的墓碑。该事件是通知,不是事务参与者:发出时提交点已经过去,因此同步抛出的监听器会被兜住并记录一条警告,而不会让已经持久的写入被拒绝;发出的值等于发出时刻的内存态。该事件仅限进程内;跨进程的变更推送是延后工作,记录在[包 README](../../packages/storage/storage-domain/README.md)中。
|
||||
`put`(插入、覆写和 global 写入)在 `value` 中携带新快照——绝不携带旧值;需要做差异比较的消费方自行保留上一份快照。`deleted` 是不携带值的墓碑。该事件是通知,不是事务参与者:发出时提交点已经过去,因此同步抛出的监听器会被兜住并记录一条警告,而不会让已经持久的写入被拒绝;发出的值等于发出时刻的内存态。该事件仅限进程内;跨进程的变更推送是一项已记录的限制([包 README](../../packages/storage/storage-domain/README.md))。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -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/subsystems/subagent.md
|
||||
subagent.md: a14e9ec5fad3f25f231016c142c3965561790afe
|
||||
subagent.zh.md: 7bf3595324ecfec85329ea3f51ab5a8cc0d4c329
|
||||
subagent.md: 99c54e0696c9c3585c50285ecb69b14c90d83c11
|
||||
subagent.zh.md: c5a79247a81f5174662f2b5d1ed2d3c20cf6c672
|
||||
|
||||
@@ -265,7 +265,7 @@ A local one-shot provider appends the descriptor inside the child's initial turn
|
||||
|
||||
## Durable enumeration: `listChildren()`, `listDescendants()`, and their entries
|
||||
|
||||
`SubagentService.listChildren(parentSessionId)` enumerates the parent's direct session-backed subagents from the live-preferred merge of `ctx.sessions.list()` and optional `ctx.sessionPersistence.list()` — no query service, and no Agent is loaded or resumed. Candidates are the direct children whose durable header carries `origin: 'subagent'`; the marker classifies enumeration and coarse generic-route denial but cannot establish a valid descriptor, resumability, or authorization — the projection fold owns identity, and the Activation contract owns resume. Each row's `mode`/`label` is the registered `subagent` projection unit's value, served through a three-rung ladder: the registry's watermark cache for a live child (zero log reads); the optional projection checkpoint cache for a cold one (`cachedSnapshot` — an identity passing the own-suffix seq gate is final, because an own descriptor is immutable once appended); otherwise one `persistence.inspect()` reading folded through the registry (bounded concurrency, recomputed per listing). The cache is a pure optional accelerator: absent, serving the `null` sentinel or missing the key, failing the seq gate, or faulting, it falls silently through to the authoritative refold. The fold is `subagent/descriptor` last-wins with no failure channel: the child's own descriptor overrides a fork-seeded ancestor's, and a malformed or unknown-version payload folds to a serializable `null` sentinel, treated as no value. The result is one `SubagentListEntry[]` in `createdAt`-then-id order: a served identity yields a `child` entry with `mode: 'one-shot' | 'continuable'` and `activity: 'running' | 'inactive'`; continuable entries always carry `label`, while one-shot entries carry it only when the start caller supplied presentation metadata. A settled candidate whose fold served no identity yields a `corrupt` diagnostic — missing, malformed, and unknown-version descriptors deliberately undistinguished, with `unsupported` kept in the type for consumers already routing on it but no longer produced; a running candidate without an identity is omitted (the creation window before its descriptor lands); a failed cold inspection yields one `unavailable` diagnostic retried on the next listing, so one damaged sibling cannot hide healthy children. `hasChildren` marks a direct descendant with durable subagent origin, read from the same merged material. Activity snapshots only whether the logical record is live in `ctx.sessions`, not outcome or resumability. Absent persistence, enumeration is live-only rather than an error — a cold child cannot be resumed then either. `listChildren()` throws `SubagentError` with code `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` when the `ctx.sessionProjections` registry is absent and `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` when the session store is, both checked before any read so a deployment with zero children still fails deterministically; the list tool requires `ctx.subagents` and `ctx.agents` at plugin load. A service consumer such as a UI can display both modes and choose an unlabeled one-shot fallback, while the model-facing `list_agents` adapter (the separately loadable `/list-agents` plugin of [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)) keeps only continuable entries and refines status through the live Agent registry's `running`/`idle`/`complete` vocabulary. Listing does not consult the continuation manager's Activation map, Agent registry, or provider availability; `send_message` remains the authoritative delivery-time operation, and a listed running continuable child may still reject delivery as an ownership conflict. The read-path rationale lives in [the list-identity-projection Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md).
|
||||
`SubagentService.listChildren(parentSessionId)` enumerates the parent's direct session-backed subagents from the live-preferred merge of `ctx.sessions.list()` and optional `ctx.sessionPersistence.list()` — no query service, and no Agent is loaded or resumed. Candidates are the direct children whose durable header carries `origin: 'subagent'`; the marker classifies enumeration and coarse generic-route denial but cannot establish a valid descriptor, resumability, or authorization — the projection fold owns identity, and the Activation contract owns resume. Each row's `mode`/`label` is the registered `subagent` projection unit's value, served through a three-rung ladder: the registry's watermark cache for a live child (zero log reads); the optional projection checkpoint cache for a cold one (`cachedSnapshot` — an identity passing the own-suffix seq gate is final, because an own descriptor is immutable once appended); otherwise one `persistence.inspect()` reading folded through the registry (bounded concurrency, recomputed per listing). The cache is a pure optional accelerator: absent, serving the `null` sentinel or missing the key, failing the seq gate, or faulting, it falls silently through to the authoritative refold. The fold is `subagent/descriptor` last-wins with no failure channel: the child's own descriptor overrides a fork-seeded ancestor's, and a malformed or unknown-version payload folds to a serializable `null` sentinel, treated as no value. The result is one `SubagentListEntry[]` in `createdAt`-then-id order: a served identity yields a `child` entry with `mode: 'one-shot' | 'continuable'` and `activity: 'running' | 'inactive'`; continuable entries always carry `label`, while one-shot entries carry it only when the start caller supplied presentation metadata. A settled candidate whose fold served no identity yields a `corrupt` diagnostic — missing, malformed, and unknown-version descriptors deliberately undistinguished (`unsupported` remains in the type but is never produced); a running candidate without an identity is omitted (the creation window before its descriptor lands); a failed cold inspection yields one `unavailable` diagnostic retried on the next listing, so one damaged sibling cannot hide healthy children. `hasChildren` marks a direct descendant with durable subagent origin, read from the same merged material. Activity snapshots only whether the logical record is live in `ctx.sessions`, not outcome or resumability. Absent persistence, enumeration is live-only rather than an error — a cold child cannot be resumed then either. `listChildren()` throws `SubagentError` with code `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` when the `ctx.sessionProjections` registry is absent and `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE` when the session store is, both checked before any read so a deployment with zero children still fails deterministically; the list tool requires `ctx.subagents` and `ctx.agents` at plugin load. A service consumer such as a UI can display both modes and choose an unlabeled one-shot fallback, while the model-facing `list_agents` adapter (the separately loadable `/list-agents` plugin of [dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control)) keeps only continuable entries and refines status through the live Agent registry's `running`/`idle`/`complete` vocabulary. Listing does not consult the continuation manager's Activation map, Agent registry, or provider availability; `send_message` remains the authoritative delivery-time operation, and a listed running continuable child may still reject delivery as an ownership conflict. The read-path rationale lives in [the list-identity-projection Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md).
|
||||
|
||||
`SubagentService.listDescendants(rootSessionId)` applies the same live-preferred corpus and projection-backed interpretation to the root's complete descendant tree in stable pre-order. Ordinary sessions and one-shot children remain traversal nodes, so continuable descendants below them are discovered; only `origin: 'subagent'` candidates produce rows. Each returned child or diagnostic adds its position from the enumerated durable header, while a cold inspection revalidates that complete lifecycle before serving identity:
|
||||
|
||||
@@ -411,7 +411,7 @@ interface SubagentProvider {
|
||||
/**
|
||||
* OPTIONAL (continuable-creation capability): contribute the detached
|
||||
* creation inputs that distinguish this provider's continuable children —
|
||||
* today only whether the child session is seeded with parent history. Method
|
||||
* only whether the child session is seeded with parent history. Method
|
||||
* presence IS the capability: the service rejects continuable starts on
|
||||
* providers without it, while a provider that has it may still serve
|
||||
* ordinary one-shot delegations.
|
||||
|
||||
@@ -265,7 +265,7 @@ interface ContinuableCreateSpec {
|
||||
|
||||
## 持久化枚举:`listChildren()`、`listDescendants()` 与其条目
|
||||
|
||||
`SubagentService.listChildren(parentSessionId)` 从 `ctx.sessions.list()` 与可选 `ctx.sessionPersistence.list()` 的实时优先合并中枚举 parent 直接且由会话支撑的 subagent——不经查询服务,也不会加载或恢复任何 Agent。候选是持久 header 携带 `origin: 'subagent'` 的直接 child;该标记只负责枚举分类与粗粒度的通用路由拒绝,不能证明描述符有效、child 可恢复或操作已获授权——身份由投影折叠负责,恢复由 Activation 约定负责。每行的 `mode`/`label` 是已注册 `subagent` projection unit 的值,经三级阶梯供值:存活 child 由注册表水位缓存供值(零日志读取);冷 child 先读可选的投影 checkpoint 缓存(`cachedSnapshot`——过 own-suffix seq 门的身份即定值,own descriptor 一经追加不可变);否则在一次 `persistence.inspect()` 读取上经注册表折叠(有界并发,每次列表重新计算)。该缓存是纯可选加速层:服务缺席、行里是 `null` 哨兵或 key 缺席、seq 门不过、读取出错,都静默落到权威重折。折叠规则是 `subagent/descriptor` last-wins 且没有失败通道:子 agent 自己的描述符覆盖 fork seed 中祖先的描述符,格式错误或版本不认识的载荷折叠为可序列化的 `null` 哨兵,视同无值。结果是按 `createdAt`、再按 id 排序的 `SubagentListEntry[]`:取到身份即生成带有 `mode: 'one-shot' | 'continuable'` 和 `activity: 'running' | 'inactive'` 的 `child` 条目;可继续条目始终携带 `label`,一次性条目则只在启动调用方提供展示元数据时携带该字段。已定局而折叠无身份的候选生成 `corrupt` diagnostic——缺失、格式错误与版本不认识的描述符有意不再细分,`unsupported` 为已按其路由的消费方保留在类型中但不再产出;运行中而无身份的候选被省略(描述符落盘前的创建窗口);冷检查失败生成一条 `unavailable` diagnostic 并在下次列表自然重试,因此一个损坏的 sibling 不会隐藏健康 child。`hasChildren` 标记存在持久 subagent origin 的直接后代,读取自同一份合并材料。活动状态只表示逻辑记录是否在 `ctx.sessions` 中存活,而不表示结果或可恢复性。缺少持久化时,枚举退化为仅存活枚举而不是报错——此时冷 child 本就无法恢复。缺少 `ctx.sessionProjections` 注册表时,`listChildren()` 抛出携带错误码 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 的 `SubagentError`,缺少会话存储时则抛出 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`,两者都在任何读取之前检查,因此零 child 的部署同样确定失败;列表工具在插件加载时要求 `ctx.subagents` 与 `ctx.agents`。UI 等服务消费方可以展示两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 适配器([dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) 中可单独加载的 `/list-agents` 插件)则只保留可继续条目,并通过在线 Agent 注册表将状态细化为 `running`/`idle`/`complete` 词汇。枚举不会查询继续执行管理器的 Activation map、Agent 注册表或提供方可用性;`send_message` 仍是消息送达时的权威操作,列表中的运行中可继续 child 仍可能因所有权冲突而拒绝投递。读路径的设计理由见[列表身份投影 Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md)。
|
||||
`SubagentService.listChildren(parentSessionId)` 从 `ctx.sessions.list()` 与可选 `ctx.sessionPersistence.list()` 的实时优先合并中枚举 parent 直接且由会话支撑的 subagent——不经查询服务,也不会加载或恢复任何 Agent。候选是持久 header 携带 `origin: 'subagent'` 的直接 child;该标记只负责枚举分类与粗粒度的通用路由拒绝,不能证明描述符有效、child 可恢复或操作已获授权——身份由投影折叠负责,恢复由 Activation 约定负责。每行的 `mode`/`label` 是已注册 `subagent` projection unit 的值,经三级阶梯供值:存活 child 由注册表水位缓存供值(零日志读取);冷 child 先读可选的投影 checkpoint 缓存(`cachedSnapshot`——过 own-suffix seq 门的身份即定值,own descriptor 一经追加不可变);否则在一次 `persistence.inspect()` 读取上经注册表折叠(有界并发,每次列表重新计算)。该缓存是纯可选加速层:服务缺席、行里是 `null` 哨兵或 key 缺席、seq 门不过、读取出错,都静默落到权威重折。折叠规则是 `subagent/descriptor` last-wins 且没有失败通道:子 agent 自己的描述符覆盖 fork seed 中祖先的描述符,格式错误或版本不认识的载荷折叠为可序列化的 `null` 哨兵,视同无值。结果是按 `createdAt`、再按 id 排序的 `SubagentListEntry[]`:取到身份即生成带有 `mode: 'one-shot' | 'continuable'` 和 `activity: 'running' | 'inactive'` 的 `child` 条目;可继续条目始终携带 `label`,一次性条目则只在启动调用方提供展示元数据时携带该字段。已定局而折叠无身份的候选生成 `corrupt` diagnostic——缺失、格式错误与版本不认识的描述符有意不再细分(`unsupported` 仍保留在类型中但从不产出);运行中而无身份的候选被省略(描述符落盘前的创建窗口);冷检查失败生成一条 `unavailable` diagnostic 并在下次列表自然重试,因此一个损坏的 sibling 不会隐藏健康 child。`hasChildren` 标记存在持久 subagent origin 的直接后代,读取自同一份合并材料。活动状态只表示逻辑记录是否在 `ctx.sessions` 中存活,而不表示结果或可恢复性。缺少持久化时,枚举退化为仅存活枚举而不是报错——此时冷 child 本就无法恢复。缺少 `ctx.sessionProjections` 注册表时,`listChildren()` 抛出携带错误码 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE` 的 `SubagentError`,缺少会话存储时则抛出 `SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE`,两者都在任何读取之前检查,因此零 child 的部署同样确定失败;列表工具在插件加载时要求 `ctx.subagents` 与 `ctx.agents`。UI 等服务消费方可以展示两种模式,并为无标签的一次性 child 选择回退展示;面向模型的 `list_agents` 适配器([dsh-tool-subagent-control](../../packages/subagent/tool-subagent-control) 中可单独加载的 `/list-agents` 插件)则只保留可继续条目,并通过在线 Agent 注册表将状态细化为 `running`/`idle`/`complete` 词汇。枚举不会查询继续执行管理器的 Activation map、Agent 注册表或提供方可用性;`send_message` 仍是消息送达时的权威操作,列表中的运行中可继续 child 仍可能因所有权冲突而拒绝投递。读路径的设计理由见[列表身份投影 Agent Note](../../.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md)。
|
||||
|
||||
`SubagentService.listDescendants(rootSessionId)` 将同一份实时优先语料与基于投影的解释应用到根的完整后代树,并按稳定 pre-order 输出。普通会话和一次性 child 仍作为遍历节点,因此其下的可继续后代仍可发现;只有 `origin: 'subagent'` 的候选会生成条目。每个返回的 child 或 diagnostic 都从枚举所得的持久 header 附加树位置;冷检查在提供身份前还会重新校验完整生命周期:
|
||||
|
||||
@@ -413,7 +413,7 @@ interface SubagentProvider {
|
||||
/**
|
||||
* OPTIONAL (continuable-creation capability): contribute the detached
|
||||
* creation inputs that distinguish this provider's continuable children —
|
||||
* today only whether the child session is seeded with parent history. Method
|
||||
* only whether the child session is seeded with parent history. Method
|
||||
* presence IS the capability: the service rejects continuable starts on
|
||||
* providers without it, while a provider that has it may still serve
|
||||
* ordinary one-shot delegations.
|
||||
|
||||
@@ -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/subsystems/subprocess.md
|
||||
subprocess.md: eedf95abc2a51872ffc9964481bb3685ace31993
|
||||
subprocess.zh.md: 669baa52a92fc3869b057dc802c72989fa8cfcf1
|
||||
subprocess.md: 4fb20e1b440b8fc10540fa2adbd00ae63fd7e221
|
||||
subprocess.zh.md: a41f14aa550c4780b5661b546880d932d513b817
|
||||
|
||||
@@ -293,8 +293,8 @@ Implementations must honor these semantics:
|
||||
* Resolve one configured executable in this provider's execution world.
|
||||
* Absolute paths are verified; bare names use the provider's scrubbed PATH
|
||||
* plus explicit environment overrides. Relative paths containing separators
|
||||
* are rejected: no current consumer defines which directory they would
|
||||
* resolve against, so providers fail loud instead of guessing.
|
||||
* are rejected: the resolution base is undefined, so providers fail loud
|
||||
* instead of guessing.
|
||||
* @param command - absolute executable path or bare PATH name.
|
||||
* @param env - explicit environment entries used for lookup.
|
||||
* @param signal - aborts remote or local lookup.
|
||||
|
||||
@@ -293,8 +293,8 @@ Implementations must honor these semantics:
|
||||
* Resolve one configured executable in this provider's execution world.
|
||||
* Absolute paths are verified; bare names use the provider's scrubbed PATH
|
||||
* plus explicit environment overrides. Relative paths containing separators
|
||||
* are rejected: no current consumer defines which directory they would
|
||||
* resolve against, so providers fail loud instead of guessing.
|
||||
* are rejected: the resolution base is undefined, so providers fail loud
|
||||
* instead of guessing.
|
||||
* @param command - absolute executable path or bare PATH name.
|
||||
* @param env - explicit environment entries used for lookup.
|
||||
* @param signal - aborts remote or local lookup.
|
||||
|
||||
@@ -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/subsystems/telemetry.md
|
||||
telemetry.md: c9e51eb38e680723ead0a88f4a9748620fd6b893
|
||||
telemetry.zh.md: 1af25c505574db6b3513547ae604eda66fc21766
|
||||
telemetry.md: 93869f2344eeedfee344735191af842fef188886
|
||||
telemetry.zh.md: 50642f1ed917ea52ee152e333f70753c12fba583
|
||||
|
||||
@@ -84,7 +84,7 @@ interface TelemetryBackend {
|
||||
* leave this unimplemented and let their SDK's own batching cadence govern
|
||||
* export timing: a backend that does implement it owns the interaction
|
||||
* between its concurrent flushes and {@link shutdown}'s drain (the OTel
|
||||
* backend removed its implementation for exactly that hazard — see the
|
||||
* backend leaves it unimplemented for exactly that hazard — see the
|
||||
* revival Agent Note).
|
||||
*/
|
||||
flush?(): void
|
||||
|
||||
@@ -84,7 +84,7 @@ interface TelemetryBackend {
|
||||
* leave this unimplemented and let their SDK's own batching cadence govern
|
||||
* export timing: a backend that does implement it owns the interaction
|
||||
* between its concurrent flushes and {@link shutdown}'s drain (the OTel
|
||||
* backend removed its implementation for exactly that hazard — see the
|
||||
* backend leaves it unimplemented for exactly that hazard — see the
|
||||
* revival Agent Note).
|
||||
*/
|
||||
flush?(): void
|
||||
|
||||
@@ -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/subsystems/tools.md
|
||||
tools.md: 87910dfbb86e9db0f0545bc2fe1135c4aefb2e69
|
||||
tools.zh.md: d312090b722de3fd1b8efc7703eafdf7f3c38682
|
||||
tools.md: 83389f39c3188fc251504ed5786249ff1921acae
|
||||
tools.zh.md: 45cb85b3f2940f84b46bc58406bc8255cbe08be7
|
||||
|
||||
@@ -184,6 +184,11 @@ type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true }
|
||||
*/
|
||||
interface ToolExecutionInput {
|
||||
readonly callId: CallId
|
||||
/**
|
||||
* Root model-requested call owning this execution tree. Callers omit it for
|
||||
* a root execution; nested dispatchers propagate the enclosing value.
|
||||
*/
|
||||
readonly rootCallId?: CallId
|
||||
readonly name: string
|
||||
/** Losslessly JSON-serializable parsed arguments (tools validate their own schema). */
|
||||
readonly arguments: unknown
|
||||
@@ -280,6 +285,8 @@ interface CodeDispatchLog {
|
||||
* observers run.
|
||||
*/
|
||||
interface ToolExecution extends ToolExecutionInput {
|
||||
/** Root model-requested call, resolved for every root and nested execution. */
|
||||
readonly rootCallId: CallId
|
||||
/** Registry-assigned identity shared with nested calls only as their opaque `parent` token. */
|
||||
readonly token: ToolExecutionToken
|
||||
}
|
||||
@@ -547,7 +554,7 @@ async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
|
||||
|
||||
Types: [ScopeKey](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:739`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:747`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="tools-events"></a>
|
||||
|
||||
@@ -572,7 +579,7 @@ A tool was registered or unregistered, or a scoped restriction changed (the avai
|
||||
'tools/change'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:191`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:192`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="toolscode-dispatch-log--waterfall"></a>
|
||||
|
||||
@@ -598,7 +605,7 @@ Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bri
|
||||
|
||||
Types: [ContentBlock](llm-streaming.md) · [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:173`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:174`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="toolsexecute--waterfall"></a>
|
||||
|
||||
@@ -622,7 +629,7 @@ Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a nor
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:148`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:149`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="toolspost-execute--waterfall"></a>
|
||||
|
||||
@@ -647,7 +654,7 @@ Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:160`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:161`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="toolspre-execute--waterfall"></a>
|
||||
|
||||
@@ -670,7 +677,7 @@ Allow, deny, or ask before dispatch. `next()` delegates to allow; missing approv
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:137`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:138`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="toolsresult--emit"></a>
|
||||
|
||||
@@ -691,5 +698,5 @@ Observe the frozen, lossless-JSON final outcome. Listener failures are contained
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:181`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:182`](../../packages/core/tools/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -184,6 +184,11 @@ type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true }
|
||||
*/
|
||||
interface ToolExecutionInput {
|
||||
readonly callId: CallId
|
||||
/**
|
||||
* Root model-requested call owning this execution tree. Callers omit it for
|
||||
* a root execution; nested dispatchers propagate the enclosing value.
|
||||
*/
|
||||
readonly rootCallId?: CallId
|
||||
readonly name: string
|
||||
/** Losslessly JSON-serializable parsed arguments (tools validate their own schema). */
|
||||
readonly arguments: unknown
|
||||
@@ -280,6 +285,8 @@ interface CodeDispatchLog {
|
||||
* observers run.
|
||||
*/
|
||||
interface ToolExecution extends ToolExecutionInput {
|
||||
/** Root model-requested call, resolved for every root and nested execution. */
|
||||
readonly rootCallId: CallId
|
||||
/** Registry-assigned identity shared with nested calls only as their opaque `parent` token. */
|
||||
readonly token: ToolExecutionToken
|
||||
}
|
||||
@@ -547,7 +554,7 @@ async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
|
||||
|
||||
Types: [ScopeKey](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:739`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:747`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="tools-events"></a>
|
||||
|
||||
@@ -572,7 +579,7 @@ A tool was registered or unregistered, or a scoped restriction changed (the avai
|
||||
'tools/change'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:191`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:192`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="toolscode-dispatch-log--waterfall"></a>
|
||||
|
||||
@@ -598,7 +605,7 @@ Shape the DURABLE LOG COPY of one `run_code` sub-dispatch outcome before the bri
|
||||
|
||||
Types: [ContentBlock](llm-streaming.md) · [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:173`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:174`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="toolsexecute--waterfall"></a>
|
||||
|
||||
@@ -622,7 +629,7 @@ Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a nor
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:148`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:149`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="toolspost-execute--waterfall"></a>
|
||||
|
||||
@@ -647,7 +654,7 @@ Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:160`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:161`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="toolspre-execute--waterfall"></a>
|
||||
|
||||
@@ -670,7 +677,7 @@ Allow, deny, or ask before dispatch. `next()` delegates to allow; missing approv
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:137`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:138`](../../packages/core/tools/src/index.ts)
|
||||
|
||||
<a id="toolsresult--emit"></a>
|
||||
|
||||
@@ -691,5 +698,5 @@ Observe the frozen, lossless-JSON final outcome. Listener failures are contained
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/core/tools/src/index.ts:181`](../../packages/core/tools/src/index.ts)
|
||||
Source: [`packages/core/tools/src/index.ts:182`](../../packages/core/tools/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -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/subsystems/web.md
|
||||
web.md: e6dc49353e50181b7844a658008edfa075cfedc3
|
||||
web.zh.md: 63f85a8d235e5c0778af8547251ebf2c299f036a
|
||||
web.md: 2a7cc499b42dd8b45ce6783a35a2fadd8df1183d
|
||||
web.zh.md: 6857f2a06a7d90702b6626409b4d8db914b3c465
|
||||
|
||||
@@ -36,7 +36,8 @@ interface WebSearchRequest {
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Normalized search outcome. `content` is optional provider-generated answer
|
||||
* text or summary (Exa returns none; Perplexity returns a generated answer).
|
||||
* text or summary (Exa and DeepSeek return none; Perplexity returns a
|
||||
* generated answer).
|
||||
* `sources[]` is the portable citation surface. `truncated` is set by the seam
|
||||
* when it cut `sources[]` down to `maxResults`.
|
||||
*/
|
||||
@@ -50,8 +51,6 @@ interface WebSearchResult {
|
||||
}
|
||||
```
|
||||
|
||||
`content` is optional provider-generated answer text (Exa and DeepSeek return none; Perplexity returns a generated answer). `sources[]` is the portable citation surface. A source always has a `url`; `title`/`snippet`/`publishedAt` are optional because not every provider returns them — Perplexity citations may be URL-only, and forcing adapters to invent the rest would make the seam lie. `dsh-tool-web` renders `title ?? hostname(url)`.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One citeable source. A source always has a URL; `title`, `snippet`, and
|
||||
@@ -103,8 +102,6 @@ interface WebFetchResult {
|
||||
}
|
||||
```
|
||||
|
||||
`WebFetchBody` is a **closed** discriminated union owned by `dsh-web` (not a merge-extensible map): the provider decodes the kind and `dsh-tool-web` renders it, so a new kind is a coordinated change across known packages, not a plugin extension. Consumers `switch` on `kind` ending in `default: assertNever(...)`, so adding a kind breaks compilation at every consumer until handled. Each arm stays its own object literal even where fields coincide today, leaving room for arm-specific fields later (a future `pdf` body's `pageCount`).
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* The decoded body of a fetched resource. A CLOSED discriminated union owned by
|
||||
@@ -112,8 +109,8 @@ interface WebFetchResult {
|
||||
* new kind is a coordinated change across known packages, not a plugin
|
||||
* extension. Consumers `switch` on `kind` ending in `default: assertNever(...)`
|
||||
* so adding a kind breaks compilation at every consumer until handled. Each arm
|
||||
* stays its own object literal even where fields coincide today, leaving room
|
||||
* for arm-specific fields later (a `pdf` body's `pageCount`).
|
||||
* stays its own object literal even where fields coincide, so an arm can gain
|
||||
* fields the others lack.
|
||||
*/
|
||||
type WebFetchBody =
|
||||
| { readonly kind: 'html'; readonly content: string }
|
||||
@@ -132,7 +129,7 @@ Selection never depends on registration, config, or HMR order: a capability has
|
||||
|
||||
## The service
|
||||
|
||||
`WebService` registers search and fetch providers, rejects duplicate ids with `WEB_DUPLICATE_PROVIDER`, and resolves providers at execution time with structured selection errors. The local fetch backend accepts only HTTP(S), rejects credentials, caps redirects, bytes, characters, and time, revalidates every same-origin redirect hop, and decodes the body; the tool owns presentation. Private-network blocking is deferred, so do not enable `web_fetch` where it can reach sensitive internal targets.
|
||||
`WebService` registers search and fetch providers, rejects duplicate ids with `WEB_DUPLICATE_PROVIDER`, and resolves providers at execution time with structured selection errors. The local fetch backend accepts only HTTP(S), rejects credentials, caps redirects, bytes, characters, and time, revalidates every same-origin redirect hop, and decodes the body; the tool owns presentation. The local backend does not block private-network targets; do not enable `web_fetch` where it can reach sensitive internal ones.
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ interface WebSearchRequest {
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Normalized search outcome. `content` is optional provider-generated answer
|
||||
* text or summary (Exa returns none; Perplexity returns a generated answer).
|
||||
* text or summary (Exa and DeepSeek return none; Perplexity returns a
|
||||
* generated answer).
|
||||
* `sources[]` is the portable citation surface. `truncated` is set by the seam
|
||||
* when it cut `sources[]` down to `maxResults`.
|
||||
*/
|
||||
@@ -50,8 +51,6 @@ interface WebSearchResult {
|
||||
}
|
||||
```
|
||||
|
||||
`content` 是提供方可选生成的回答文本(Exa 和 DeepSeek 不返回;Perplexity 返回生成式回答)。`sources[]` 是一套可跨提供方使用的引用数据结构。每个来源都必须有 `url`;`title`、`snippet` 和 `publishedAt` 为可选字段,因为并非每个提供方都会返回它们——Perplexity 的引用可能只有 URL,强迫适配器编造其余字段会让 seam 说谎。`dsh-tool-web` 渲染时使用 `title ?? hostname(url)`。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* One citeable source. A source always has a URL; `title`, `snippet`, and
|
||||
@@ -103,8 +102,6 @@ interface WebFetchResult {
|
||||
}
|
||||
```
|
||||
|
||||
`WebFetchBody` 是 `dsh-web` 拥有的**封闭**可辨识联合类型(不是可合并扩展的 map):提供方解码 kind,`dsh-tool-web` 渲染它,因此新增一个 kind 是已知包之间的协调变更,而非插件扩展。消费方对 `kind` 做 `switch` 并以 `default: assertNever(...)` 结尾,所以新增 kind 会在每个消费方处编译失败,直到被处理。即使各分支当前字段一致,每个分支仍保持独立的对象字面量,为将来分支特有字段留出空间(例如未来 `pdf` body 的 `pageCount`)。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* The decoded body of a fetched resource. A CLOSED discriminated union owned by
|
||||
@@ -112,8 +109,8 @@ interface WebFetchResult {
|
||||
* new kind is a coordinated change across known packages, not a plugin
|
||||
* extension. Consumers `switch` on `kind` ending in `default: assertNever(...)`
|
||||
* so adding a kind breaks compilation at every consumer until handled. Each arm
|
||||
* stays its own object literal even where fields coincide today, leaving room
|
||||
* for arm-specific fields later (a `pdf` body's `pageCount`).
|
||||
* stays its own object literal even where fields coincide, so an arm can gain
|
||||
* fields the others lack.
|
||||
*/
|
||||
type WebFetchBody =
|
||||
| { readonly kind: 'html'; readonly content: string }
|
||||
@@ -132,7 +129,7 @@ type WebFetchBody =
|
||||
|
||||
## 服务
|
||||
|
||||
`WebService` 注册搜索与抓取提供方,以 `WEB_DUPLICATE_PROVIDER` 拒绝重复 id,并在执行时以结构化的选择错误解析提供方。本地抓取后端仅接受 HTTP(S)、拒绝凭证、限制重定向次数、字节数、字符数和时间、对每一跳同源重定向重新校验,并解码正文;展示由工具负责。SSRF/私有网络防护尚未实现,因此在能够触及敏感内部目标的环境中,禁止启用 `web_fetch`。
|
||||
`WebService` 注册搜索与抓取提供方,以 `WEB_DUPLICATE_PROVIDER` 拒绝重复 id,并在执行时以结构化的选择错误解析提供方。本地抓取后端仅接受 HTTP(S)、拒绝凭证、限制重定向次数、字节数、字符数和时间、对每一跳同源重定向重新校验,并解码正文;展示由工具负责。本地后端不会拦截私有网络目标;在能够触及敏感内部目标的环境中,禁止启用 `web_fetch`。
|
||||
|
||||
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user