Merge branch 'stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring

# Conflicts:
#	apps/cli/tests/web-agent-presets.e2e.ts
#	packages/client/connection/README.i18n.yaml
#	packages/client/connection/README.md
#	packages/client/connection/README.zh.md
#	packages/client/ui-conversation/src/client/contract/slots.ts
#	packages/client/ui-primitives/tests/icons.spec.tsx
#	packages/client/ui-settings/src/client/contract/slots.ts
#	packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts
This commit is contained in:
Yichen Jiang
2026-08-09 23:02:37 +08:00
825 changed files with 2990 additions and 2066 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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`

View File

@@ -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`

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 -->

View File

@@ -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 -->

View File

@@ -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: 8325694d92aa1a3019aef8f6b0c1f99d45ad0df5
compaction.zh.md: 9f9014b8c1c1db12abc6581fbe6ebacfac3dd6fe
compaction.md: fdff28ac2ec83966a03050ba8d54fe0ee26c4fc6
compaction.zh.md: e4227320d9829cac695dc4ba0c5779086f77194b

View File

@@ -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 |
|---|---|---|

View File

@@ -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` 的决策依据
| 事件 | 载荷 | 作用 |
|---|---|---|

View File

@@ -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: f176ba49e952d0ed3c681ab488cab13f97777fa4
core.zh.md: 60530c111c177c148648c0f43d7441bbcf2866f8
core.md: 2a1e52016dd7eabe9f1110ca44fcf27ac9548390
core.zh.md: 71a395eefcd2259c81fff86c1a217aef3a55f812

View File

@@ -676,7 +676,7 @@ list(): Agent[]
roots(): Agent[]
```
Source: [`packages/core/agent/src/index.ts:254`](../../packages/core/agent/src/index.ts)
Source: [`packages/core/agent/src/index.ts:255`](../../packages/core/agent/src/index.ts)
<a id="agent-events"></a>

View File

@@ -684,7 +684,7 @@ list(): Agent[]
roots(): Agent[]
```
Source: [`packages/core/agent/src/index.ts:254`](../../packages/core/agent/src/index.ts)
Source: [`packages/core/agent/src/index.ts:255`](../../packages/core/agent/src/index.ts)
<a id="agent-events"></a>

View File

@@ -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: dd81cc27ad73bab026cd11da6ab184890bb6535d
filesystem.zh.md: 9589adfaf43c1d5a4bbc8571e70c878770d880b2
filesystem.md: e0edad514b0c4d9108b18cc0b472600b29024c51
filesystem.zh.md: 81c2d87b6a96740c7a99449a27c16331f5a0645a

View File

@@ -269,7 +269,7 @@ type FsErrorCode =
## 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

View File

@@ -269,7 +269,7 @@ type FsErrorCode =
## 文件 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` 停下,因此这里的截止时间会成为无法兑现承诺的旋钮,而且恰好落在"显式优于隐式"禁止隐式默认值的位置。两个参照 agentClaude 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 传播,在系统调用边界尽力中止。
## 服务与插件

View File

@@ -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

View File

@@ -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.

View File

@@ -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`(直接 fetchSSEServer-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)规定。

View File

@@ -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

View File

@@ -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'
```

View File

@@ -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'
```

View File

@@ -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: 39133c659953e3f444d4041072c646c4b3108f07
persistence.zh.md: 7b4b6ffcbead9c8f3530e7fb230069dfcbee5cbb
persistence.md: 8f6872b77be8c7ae273e0fc1887dca30dbe1eb37
persistence.zh.md: 25e72a69bd03cd5cc0715ae769055062466397dd

View File

@@ -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
@@ -208,7 +208,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.

View File

@@ -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 检查点
@@ -208,7 +208,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。

View File

@@ -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: 0d219425584832dbe1adeef2e0613ff9c909ef62
session-projection.zh.md: f7341049d63f67df0572416771a55bf28e889f7a
session-projection.md: 4cbe0babb22406f7a48f0c19e982bb4757b4f44d
session-projection.zh.md: 5eada67a6eed914021e284fc5eabf203125b4b83

View File

@@ -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)

View File

@@ -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);驱动、缓存与变更流约定:[packageREADME](../../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);驱动、缓存与变更流约定:[packageREADME](../../packages/session/session-projection/README.md)。
源码:[`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts)

View File

@@ -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: 6a4d647b1b8b6af55eca556748df000bdd41e701
session.zh.md: 356229a15e101081c1e377289e166922c14a2b4c
session.md: 6fb0cec4fd222ceafbd5b4111fe56f22505058ad
session.zh.md: d33a71e92e2bd9fd9fb7e9194255b7c1f5f0af77

View File

@@ -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'

View File

@@ -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'

View File

@@ -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: f222da732ba3800a214e236bb7a64d5709067cdb
skills.zh.md: f20c68596f5350ac33c2956dfb124ae6c8972882

View File

@@ -2,7 +2,7 @@
English | [中文](skills.zh.md)
The [skill capability family](../../packages/skill) includes the Service Definition ([dsh-skill](../../packages/skill/skill), `ctx.skills`), the local Service provider ([dsh-skill-local](../../packages/skill/skill-local)), the optional packaged badge provider ([dsh-skill-badge](../../packages/skill/skill-badge)), and the Consumer ([dsh-tool-skill](../../packages/skill/tool-skill)). The registry merges provider catalogs; providers contribute local or packaged skills; the Consumer owns the initial and replacement catalogs plus the model-facing `skill` tool. Skills are optional instructions, not session events, so their vocabulary lives here rather than in [core.md](core.md).
The [skill capability family](../../packages/skill) includes the Service Definition ([dsh-skill](../../packages/skill/skill), `ctx.skills`), the local Service provider ([dsh-skill-local](../../packages/skill/skill-local)), the optional packaged badge provider ([dsh-skill-badge](../../packages/skill/skill-badge)), and the Consumer ([dsh-tool-skill](../../packages/skill/tool-skill)). The registry merges provider catalogs across its host and per-scope layers; providers contribute local or packaged skills; the Consumer owns the initial and replacement catalogs plus the model-facing `skill` tool. Skills are optional instructions, not session events, so their vocabulary lives here rather than in [core.md](core.md).
Source: [`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts), [`packages/skill/skill-local/src/index.ts`](../../packages/skill/skill-local/src/index.ts), [`packages/skill/skill-badge/src/index.ts`](../../packages/skill/skill-badge/src/index.ts), and [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts).
@@ -10,7 +10,9 @@ Source: [`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/ind
`ctx.skills` combines local, embedded, remote, or other providers. Registration is synchronous; remote initialization and discovery belong in awaited `list()`. Provider objects, options, and candidates are borrowed readonly, while semantic fields are validated.
Duplicate names resolve by rank, provider order, then local order; summaries sort by name. A rejected `list()` is logged and omitted from an incomplete observation, while an explicit incomplete observation contributes usable candidates without making the result cacheable; malformed candidates fail fast. Each provider factory receives a registration-scoped control whose `invalidate()` clears completed catalogs only while that exact registration remains active and whose signal aborts on failed registration or disposal. An in-flight discovery retries once when its provider generation changes; a second change returns the latest candidates incomplete and uncached. Provider and runtime mutations emit the unfiltered `skills/change` invalidation event; it carries no diff, so consumers refetch `snapshot()` with their own lookup options.
The registry is host+per-scope layered, the shape the [tools registry](tools.md) established over [dsh-scope](../../packages/core/scope): a registration files into the layer of its calling context's scope, so host rows and repository plugins land in the global layer while a plugin mounted by an agent preset's standing composition lands in that preset's layer, and provider names are unique per layer rather than process-wide. A read merges the global layer with the viewing scope's chain — the nearest layer's entry wins a duplicate skill name outright, and the rank order below decides duplicates only within one layer. Discovery caches are keyed by the resolved scope chain, so re-parenting a scope (a blank-session recompose) is visible to the next read without a registry mutation.
Within one layer, duplicate names resolve by rank, provider order, then local order; summaries sort by name. A rejected `list()` is logged and omitted from an incomplete observation, while an explicit incomplete observation contributes usable candidates without making the result cacheable; malformed candidates fail fast. Each provider factory receives a registration-scoped control whose `invalidate()` clears completed catalogs only while that exact registration remains active and whose signal aborts on failed registration or disposal. An in-flight discovery retries once when its provider generation changes; a second change returns the latest candidates incomplete and uncached. Provider and runtime mutations emit the unfiltered `skills/change` invalidation event; it carries no diff, so consumers refetch `snapshot()` with their own lookup options.
An array returned by `SkillProvider.list()` is complete-discovery shorthand. `SkillProviderObservation` lets a provider expose candidates that remain directly loadable while reporting that the observation is not authoritative.
@@ -80,7 +82,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. */
@@ -187,7 +189,7 @@ type SkillRegistration = Omit<SkillDefinition, 'invocation' | 'provider'> & {
## Lookup and configuration
Skill lookup is cwd-sensitive because providers may expose workspace-local skills, and its optional signal cancels provider work for the caller. Providers receive the same readonly options object used for cache identity and loading. Cancellation is checked before and after catalog selection, including cache hits, and races both discovery and full-definition loading. If no git root is found, the local provider treats the supplied cwd itself as the project root.
Skill lookup is cwd-sensitive because providers may expose workspace-local skills, and its optional signal cancels provider work for the caller. Registry reads additionally take the viewing scope — consumers pass the calling agent, which is its own scope key — through `SkillViewOptions`; the registry consumes `scope` for layer selection, and providers read only their `SkillLookupOptions` contract from the same borrowed options object. Cancellation is checked before and after catalog selection, including cache hits, and races both discovery and full-definition loading. If no git root is found, the local provider treats the supplied cwd itself as the project root.
Full definitions are not cached by the registry. Each `get()` calls the winning provider with the selected candidate, so the local provider rereads the current body. A definition whose name no longer matches that candidate is rejected and invalidates the exact provider for rediscovery.
@@ -201,6 +203,19 @@ interface SkillLookupOptions {
}
```
```ts type-equiv
/**
* Registry read options: provider lookup context plus the viewing scope.
* The registry consumes `scope` to select layers; providers receive the same
* borrowed options object and read only their {@link SkillLookupOptions}
* contract from it.
*/
interface SkillViewOptions extends SkillLookupOptions {
/** Viewing scope (the calling agent); omitted reads the global layer alone. */
readonly scope?: ScopeKey | undefined
}
```
The registry owns only its discovery-cache bound. The local provider owns filesystem roots (`dshHome`, `agentsHome`, `customSkillDirs`, and optional `bundledSkillDir`/`DSH_BUNDLED_SKILL_DIR`) plus watcher enablement, polling, stability, symlink, and project-capacity controls. The consumer owns its catalog description bound. Exact defaults and validation are in the generated [config catalog](../config-catalog.md).
```ts type-equiv
@@ -231,13 +246,16 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
### `ctx.skills` — `SkillService`
Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted invocation-neutral summaries, and loads full skill bodies on demand.
Layered registry of skill providers, the host+per-scope shape the tools registry established. A registration files into the layer of its calling context's scope (scopeOf): host rows and repository plugins land in the global layer, while a plugin mounted by an agent preset's standing composition lands in that preset's layer. A read merges the global layer with the viewing scope's chain — the nearest layer's entry wins a duplicate name outright, and the rank order decides duplicates only within one layer. It exposes sorted invocation-neutral summaries and loads full skill bodies on demand.
```ts cordis-catalog
/**
* Register a borrowed same-process provider synchronously during plugin apply. Duplicate and
* reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters
* the provider and invalidates catalog caches.
* Register a borrowed same-process provider synchronously during plugin
* apply, into the calling context's layer: a scoped context (an agent
* preset's standing mount) registers for that scope alone, an unscoped
* context registers globally. Duplicate names within one layer and reserved
* names throw; remote initialization belongs in `list()`. Fiber disposal
* unregisters the provider and invalidates catalog caches.
* @param create - synchronous factory receiving this registration's lifecycle and invalidation control.
* @returns the exact Cordis effect disposer that unregisters this provider;
* composite effects may yield it directly to preserve teardown ordering.
@@ -245,9 +263,11 @@ Registry of skill providers. It merges provider catalogs with stable first-wins
registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => void
/**
* Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which
* outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and
* receives a no-op disposer so it cannot remove the winner.
* Register a borrowed readonly runtime skill into the calling context's
* layer. Project entries outrank runtime entries, which outrank user
* entries, within one layer. Same-name runtime entries in one layer are
* first-wins; a duplicate logs a warning and receives a no-op disposer so
* it cannot remove the winner.
* @param skill - the skill definition input; omitted invocation and provider fields receive defaults.
* @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
*/
@@ -258,32 +278,33 @@ register(skill: SkillRegistration): () => void
* model or user invocation policy at their operational boundary. Lookup
* options and provider candidates are readonly same-process values borrowed
* throughout discovery.
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
* @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
* @returns all sorted winning summaries.
*/
async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]>
async list(options: SkillViewOptions = {}): Promise<SkillSummary[]>
/**
* Observe the current invocation-neutral catalog and whether discovery completed within a stable revision.
* Incomplete observations are never cached, allowing consumers to retain last-good state and
* retry on their next request boundary.
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
* @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
* @returns sorted summaries plus discovery-completeness state.
*/
async snapshot(options: SkillLookupOptions = {}): Promise<SkillCatalogSnapshot>
async snapshot(options: SkillViewOptions = {}): Promise<SkillCatalogSnapshot>
/**
* Load and validate the winning candidate, passing its opaque discovery locator back to the
* provider. Cancellation is rechecked after selection, including cache hits, and raced against
* loading so an uncooperative provider cannot hang the caller.
* @param name - kebab-case skill name.
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
* @param options - view options; `scope` selects the viewing agent's layers,
* `cwd` selects workspace-sensitive skills, and `signal` cancels work.
* @returns the full skill, including body content, or `undefined`.
*/
async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined>
async get(name: string, options: SkillViewOptions = {}): Promise<SkillDefinition | undefined>
```
Source: [`packages/skill/skill/src/index.ts:305`](../../packages/skill/skill/src/index.ts)
Source: [`packages/skill/skill/src/index.ts:357`](../../packages/skill/skill/src/index.ts)
<a id="skills-events"></a>
@@ -306,5 +327,5 @@ A skill provider, runtime contribution, or provider-backed catalog may have chan
'skills/change'(): void
```
Source: [`packages/skill/skill/src/index.ts:284`](../../packages/skill/skill/src/index.ts)
Source: [`packages/skill/skill/src/index.ts:297`](../../packages/skill/skill/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -2,7 +2,7 @@
[English](skills.md) | 中文
[skill技能能力族](../../packages/skill) 包含 Service Definition[dsh-skill](../../packages/skill/skill)`ctx.skills`)、本地 Service provider[dsh-skill-local](../../packages/skill/skill-local))、可选的随包徽章提供方([dsh-skill-badge](../../packages/skill/skill-badge))和 Consumer[dsh-tool-skill](../../packages/skill/tool-skill))。注册表合并各提供方的目录;提供方贡献本地或随包 skillConsumer 拥有初始目录和替换目录,以及面向模型的 `skill` 工具。skill 是可选的指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.md)。
[skill技能能力族](../../packages/skill) 包含 Service Definition[dsh-skill](../../packages/skill/skill)`ctx.skills`)、本地 Service provider[dsh-skill-local](../../packages/skill/skill-local))、可选的随包徽章提供方([dsh-skill-badge](../../packages/skill/skill-badge))和 Consumer[dsh-tool-skill](../../packages/skill/tool-skill))。注册表在其宿主层与各 scope 层之间合并各提供方的目录;提供方贡献本地或随包 skillConsumer 拥有初始目录和替换目录,以及面向模型的 `skill` 工具。skill 是可选的指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.md)。
源码:[`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts)、[`packages/skill/skill-local/src/index.ts`](../../packages/skill/skill-local/src/index.ts)、[`packages/skill/skill-badge/src/index.ts`](../../packages/skill/skill-badge/src/index.ts) 与 [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts)。
@@ -10,7 +10,9 @@
`ctx.skills` 组合本地、内嵌、远程或其他提供方。注册是同步的;远程初始化与发现属于 `list()` 的 await 阶段。提供方对象、选项与候选项以只读方式借用,语义字段会被校验。
重名项依次按 rank、提供方顺序和本地顺序确定优先级摘要按名称排序。提供方的 `list()` 被拒绝时,系统会记录日志,并从不完整观测中省略该提供方的结果;显式的不完整观测会提供可用候选项,但不会使结果变得可缓存;格式错误的候选项快速失败。每个提供方工厂都会接收一项注册作用域内的控制能力;仅当该精确注册仍处于活动状态时,其 `invalidate()` 才会清除已完成目录;注册失败或释放时,其信号会中止。若提供方代次在发现进行期间发生变化,该发现会重试一次;若再次变化,则返回最新候选项,并将结果标为不完整且不予缓存。提供方和运行时变更会发出不带过滤条件的 `skills/change` 失效事件;该事件不携带 diff因此消费方会使用自身的查找选项重新获取 `snapshot()`
注册表采用宿主 + 按 scope 的分层结构,即[工具注册表](tools.md)在 [dsh-scope](../../packages/core/scope) 之上确立的形态:注册会落入调用方上下文 scope 对应的层——宿主行与 repository 插件落入全局层,由 agent preset 常驻组合挂载的插件落入该 preset 的层——提供方名称在每层内唯一,而非进程级唯一。读取时将全局层与观察 scope 的链合并:最近层的条目直接赢得重名 skill下文的 rank 顺序只在单层内裁决重名。发现缓存以解析后的 scope 链为键,因此重设 scope 父级(空会话重组)无需注册表变更即可被下一次读取看到
在单层内,重名项依次按 rank、提供方顺序和本地顺序确定优先级摘要按名称排序。提供方的 `list()` 被拒绝时,系统会记录日志,并从不完整观测中省略该提供方的结果;显式的不完整观测会提供可用候选项,但不会使结果变得可缓存;格式错误的候选项快速失败。每个提供方工厂都会接收一项注册作用域内的控制能力;仅当该精确注册仍处于活动状态时,其 `invalidate()` 才会清除已完成目录;注册失败或释放时,其信号会中止。若提供方代次在发现进行期间发生变化,该发现会重试一次;若再次变化,则返回最新候选项,并将结果标为不完整且不予缓存。提供方和运行时变更会发出不带过滤条件的 `skills/change` 失效事件;该事件不携带 diff因此消费方会使用自身的查找选项重新获取 `snapshot()`
`SkillProvider.list()` 返回的数组是完整发现的简写形式。`SkillProviderObservation` 允许提供方公开仍可直接加载的候选项,同时报告该观测不具权威性。
@@ -80,7 +82,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. */
@@ -187,7 +189,7 @@ type SkillRegistration = Omit<SkillDefinition, 'invocation' | 'provider'> & {
## 查找与配置
skill 查找对 cwd 敏感,因为提供方可能暴露工作区本地的 skill可选的 signal 为调用方取消提供方的工作。提供方接收用于缓存标识和加载的同一个只读选项对象。取消在目录选择前后(包括缓存命中时)都会检查,并与发现和完整定义加载竞争。如果找不到 git root本地提供方将所提供的 cwd 本身视为项目根目录。
skill 查找对 cwd 敏感,因为提供方可能暴露工作区本地的 skill可选的 signal 为调用方取消提供方的工作。注册表读取还通过 `SkillViewOptions` 携带观察 scope——消费方传入调用中的 agentagent 本身就是自己的 scope key注册表消费 `scope` 做层选择,提供方只从同一个借用的选项对象中读取其 `SkillLookupOptions` 契约。取消在目录选择前后(包括缓存命中时)都会检查,并与发现和完整定义加载竞争。如果找不到 git root本地提供方将所提供的 cwd 本身视为项目根目录。
注册表不缓存完整定义。每次调用 `get()` 都会携所选候选项调用胜出提供方,因此本地提供方会重新读取当前正文。名称与该候选项不再匹配的定义会被拒绝,并使该提供方实例失效以便重新发现。
@@ -201,6 +203,19 @@ interface SkillLookupOptions {
}
```
```ts type-equiv
/**
* Registry read options: provider lookup context plus the viewing scope.
* The registry consumes `scope` to select layers; providers receive the same
* borrowed options object and read only their {@link SkillLookupOptions}
* contract from it.
*/
interface SkillViewOptions extends SkillLookupOptions {
/** Viewing scope (the calling agent); omitted reads the global layer alone. */
readonly scope?: ScopeKey | undefined
}
```
注册表只拥有其发现缓存上限。本地提供方拥有文件系统根目录(`dshHome`、`agentsHome`、`customSkillDirs`,以及可选的 `bundledSkillDir`/`DSH_BUNDLED_SKILL_DIR`),以及 watcher 启用、轮询、稳定性、符号链接和项目容量控制。消费方拥有其目录描述上限。确切的默认值和校验规则见自动生成的[插件配置目录](../config-catalog.md)。
```ts type-equiv
@@ -231,13 +246,16 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
### `ctx.skills` — `SkillService`
Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted invocation-neutral summaries, and loads full skill bodies on demand.
Layered registry of skill providers, the host+per-scope shape the tools registry established. A registration files into the layer of its calling context's scope (scopeOf): host rows and repository plugins land in the global layer, while a plugin mounted by an agent preset's standing composition lands in that preset's layer. A read merges the global layer with the viewing scope's chain — the nearest layer's entry wins a duplicate name outright, and the rank order decides duplicates only within one layer. It exposes sorted invocation-neutral summaries and loads full skill bodies on demand.
```ts cordis-catalog
/**
* Register a borrowed same-process provider synchronously during plugin apply. Duplicate and
* reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters
* the provider and invalidates catalog caches.
* Register a borrowed same-process provider synchronously during plugin
* apply, into the calling context's layer: a scoped context (an agent
* preset's standing mount) registers for that scope alone, an unscoped
* context registers globally. Duplicate names within one layer and reserved
* names throw; remote initialization belongs in `list()`. Fiber disposal
* unregisters the provider and invalidates catalog caches.
* @param create - synchronous factory receiving this registration's lifecycle and invalidation control.
* @returns the exact Cordis effect disposer that unregisters this provider;
* composite effects may yield it directly to preserve teardown ordering.
@@ -245,9 +263,11 @@ Registry of skill providers. It merges provider catalogs with stable first-wins
registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => void
/**
* Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which
* outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and
* receives a no-op disposer so it cannot remove the winner.
* Register a borrowed readonly runtime skill into the calling context's
* layer. Project entries outrank runtime entries, which outrank user
* entries, within one layer. Same-name runtime entries in one layer are
* first-wins; a duplicate logs a warning and receives a no-op disposer so
* it cannot remove the winner.
* @param skill - the skill definition input; omitted invocation and provider fields receive defaults.
* @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
*/
@@ -258,32 +278,33 @@ register(skill: SkillRegistration): () => void
* model or user invocation policy at their operational boundary. Lookup
* options and provider candidates are readonly same-process values borrowed
* throughout discovery.
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
* @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
* @returns all sorted winning summaries.
*/
async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]>
async list(options: SkillViewOptions = {}): Promise<SkillSummary[]>
/**
* Observe the current invocation-neutral catalog and whether discovery completed within a stable revision.
* Incomplete observations are never cached, allowing consumers to retain last-good state and
* retry on their next request boundary.
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
* @param options - view options; `scope` selects the viewing agent's layers, `cwd` selects project roots, and `signal` cancels discovery.
* @returns sorted summaries plus discovery-completeness state.
*/
async snapshot(options: SkillLookupOptions = {}): Promise<SkillCatalogSnapshot>
async snapshot(options: SkillViewOptions = {}): Promise<SkillCatalogSnapshot>
/**
* Load and validate the winning candidate, passing its opaque discovery locator back to the
* provider. Cancellation is rechecked after selection, including cache hits, and raced against
* loading so an uncooperative provider cannot hang the caller.
* @param name - kebab-case skill name.
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
* @param options - view options; `scope` selects the viewing agent's layers,
* `cwd` selects workspace-sensitive skills, and `signal` cancels work.
* @returns the full skill, including body content, or `undefined`.
*/
async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined>
async get(name: string, options: SkillViewOptions = {}): Promise<SkillDefinition | undefined>
```
Source: [`packages/skill/skill/src/index.ts:305`](../../packages/skill/skill/src/index.ts)
Source: [`packages/skill/skill/src/index.ts:357`](../../packages/skill/skill/src/index.ts)
<a id="skills-events"></a>
@@ -306,5 +327,5 @@ A skill provider, runtime contribution, or provider-backed catalog may have chan
'skills/change'(): void
```
Source: [`packages/skill/skill/src/index.ts:284`](../../packages/skill/skill/src/index.ts)
Source: [`packages/skill/skill/src/index.ts:297`](../../packages/skill/skill/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -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

View File

@@ -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 -->

View File

@@ -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 -->

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 -->

View File

@@ -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 -->