docs: generate each subsystem's cordis surface into its own page; delete the flat catalogs

Rebuild of the region machinery (PR3) on the post-#904 Typert projection:
renderPageRegion/renderInheritedPage live in dsh-typert-generator beside the
projection; scripts/gen-cordis-catalog.ts owns the curated SERVICE_PAGE /
EVENT_SCOPE_PAGE / SERVICE_WALK_EXEMPTIONS / LINK_MAP partition (fail-loud in
both directions, with the independent Context-merge scan backstopping the
projection's blind spot), spliceRegion, and the guarded pair auto-record.
docs/cordis-catalog/ is deleted: the flat events/services catalogs dissolve
into per-page regions and docs/cordis-catalog/core moves to docs/cordis-api/
with the inherited tier as its own generated page. The partition absorbs the
post-regrouping surface: ctx.typert → invariants.md, ctx.directoryPicker →
workspace.md, skills/* events → skills.md, and the four launcher-provided tui
accessor values join the named exemptions.
This commit is contained in:
Tianyi Cui
2026-07-30 21:40:58 +08:00
parent 44b035c711
commit f7323354bb
214 changed files with 10434 additions and 4447 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: 8f17f98101950e6413d67fb4cce6befc5aede069
approval.zh.md: c19285572b667512427b71d4af214acbc2baf334
approval.md: 2045f2073b72fa265728cbf77b673053a2eaf371
approval.zh.md: c021db14c4925acaeef5d984d9dfa88ad55bd8dd

View File

@@ -87,3 +87,85 @@ interface ApprovalRequest {
`ctx.approval.request(req)` requires the requesting session to be inside an open turn. It appends `approval/asked`, obtains one outcome, appends the matching `approval/decided`, and resolves with that outcome. The `never` policy is enforced inside the service before waterfall dispatch, so even an answerer registered later with `prepend` cannot bypass it. Answerers return an outcome when they own the request or call `next()` to delegate; the first answer occupies the single decision slot.
The audit events are log-only and do not enter the model transcript. Model-visible behavior is the caller's derived tool result plus the current runtime-context snapshot. Service disposal removes its context contribution; answerer listeners are independently effect-bound to their owning plugins.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxapproval--approvalservice"></a>
### `ctx.approval` — `ApprovalService`
Approval service that applies session policy before answerers and logs every ask/outcome pair to the requesting session. It exposes deterministic policy changes to the model through the runtime-context snapshot and switch notices.
```ts cordis-catalog
/**
* Switch one live agent's policy and queue the transition for its next model
* step. Session initialization uses {@link setApprovalPolicy} directly
* because there is no previously visible policy to change.
* @param agent - the live agent whose policy is changing.
* @param policy - the new effective policy.
*/
setPolicy(agent: Agent, policy: ApprovalPolicy): void
/**
* Ask the composed answerers to decide one readonly same-process request.
* The service borrows the request, agent, session, and live signal directly.
* The request requires an open turn because the audit pair must be enclosed
* by the durable log's commit/replay boundary; an idle ask rejects before
* appending anything. The answerer phase always produces an outcome: an
* aborted signal yields `'cancelled'`, a missing or throwing answerer yields
* `'unavailable'` (fail closed), and a rogue non-vocabulary return value is
* normalized to `'unavailable'`. A failure that prevents either audit append
* from committing still rejects because returning an unlogged decision would
* violate the pair. Session contains post-commit observer failures, so an
* authoritative append cannot reject the request or suppress its matching
* audit event.
* @param req - the pending decision (agent, tool identity, reason, signal).
* @returns the closed outcome; `'allowed-once'` is the only grant.
* @throws when no turn is open or either audit event fails before the session
* append commit point.
*/
async request(req: ApprovalRequest): Promise<ApprovalOutcome>
/**
* Read the session override without applying the configured default.
* @param session - session whose log supplies the override.
* @returns the last logged policy, or `undefined` without one.
*/
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)
<a id="approval-events"></a>
### `approval/*` events
<a id="approvalrequest--waterfall"></a>
#### `approval/request` — waterfall
Ask composed answerers for one decision. Return an outcome to claim the request or call `next()`; failure yields the fail-closed default. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
```ts cordis-catalog
/**
* Ask composed answerers for one decision. Return an outcome to claim the
* request or call `next()`; failure yields the fail-closed default.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @param req - the pending decision (agent, tool identity, reason, signal).
* @mode waterfall
*/
'approval/request'(this: Scoped<ApprovalService>, req: ApprovalRequest, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome>
```
Types: [Scoped](scope.md)
Source: [`packages/interaction/user-approval/src/index.ts:30`](../../packages/interaction/user-approval/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -87,3 +87,85 @@ interface ApprovalRequest {
`ctx.approval.request(req)` 要求发起请求的会话处于一个尚未结束的轮次内。它追加 `approval/asked`,获取一个结果,追加对应的 `approval/decided`,然后以该结果完成。`never` 策略在服务内部、waterfall 分发之前强制执行,因此即使后来以 `prepend` 注册的应答者也无法绕过它。应答者在拥有该请求时返回结果,否则调用 `next()` 委托;第一个应答占据唯一的决策槽位。
审计事件仅写入日志,不进入模型 transcript文本记录。模型可见的行为是调用方派生的工具结果与当前运行时上下文快照。服务 dispose资源释放时会移除其上下文贡献应答者监听器独立地通过 effect 绑定到其所属插件。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxapproval--approvalservice"></a>
### `ctx.approval` — `ApprovalService`
Approval service that applies session policy before answerers and logs every ask/outcome pair to the requesting session. It exposes deterministic policy changes to the model through the runtime-context snapshot and switch notices.
```ts cordis-catalog
/**
* Switch one live agent's policy and queue the transition for its next model
* step. Session initialization uses {@link setApprovalPolicy} directly
* because there is no previously visible policy to change.
* @param agent - the live agent whose policy is changing.
* @param policy - the new effective policy.
*/
setPolicy(agent: Agent, policy: ApprovalPolicy): void
/**
* Ask the composed answerers to decide one readonly same-process request.
* The service borrows the request, agent, session, and live signal directly.
* The request requires an open turn because the audit pair must be enclosed
* by the durable log's commit/replay boundary; an idle ask rejects before
* appending anything. The answerer phase always produces an outcome: an
* aborted signal yields `'cancelled'`, a missing or throwing answerer yields
* `'unavailable'` (fail closed), and a rogue non-vocabulary return value is
* normalized to `'unavailable'`. A failure that prevents either audit append
* from committing still rejects because returning an unlogged decision would
* violate the pair. Session contains post-commit observer failures, so an
* authoritative append cannot reject the request or suppress its matching
* audit event.
* @param req - the pending decision (agent, tool identity, reason, signal).
* @returns the closed outcome; `'allowed-once'` is the only grant.
* @throws when no turn is open or either audit event fails before the session
* append commit point.
*/
async request(req: ApprovalRequest): Promise<ApprovalOutcome>
/**
* Read the session override without applying the configured default.
* @param session - session whose log supplies the override.
* @returns the last logged policy, or `undefined` without one.
*/
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)
<a id="approval-events"></a>
### `approval/*` events
<a id="approvalrequest--waterfall"></a>
#### `approval/request` — waterfall
Ask composed answerers for one decision. Return an outcome to claim the request or call `next()`; failure yields the fail-closed default. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
```ts cordis-catalog
/**
* Ask composed answerers for one decision. Return an outcome to claim the
* request or call `next()`; failure yields the fail-closed default.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @param req - the pending decision (agent, tool identity, reason, signal).
* @mode waterfall
*/
'approval/request'(this: Scoped<ApprovalService>, req: ApprovalRequest, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome>
```
Types: [Scoped](scope.md)
Source: [`packages/interaction/user-approval/src/index.ts:30`](../../packages/interaction/user-approval/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/bash.md
bash.md: f83a133c8e049bc111cfabd2b59def18a1b2d607
bash.zh.md: 27ca9f6e5a8e05cd4ffa86e72d433b6f22005f09
# pnpm run verify-translation-pairing --write docs/subsystems/bash.md
bash.md: dd7649ce46bea5642270fa553eb3d82f2dc33e9b
bash.zh.md: bba9ece2955cee66e941aa878299cff6c06a97a0

View File

@@ -219,3 +219,85 @@ interface BashProcessRead {
## The service
`BashExecutor` owns `resolve`, foreground `run`, background-process `start`, and the `sandboxMode` capability fact. `dsh-bash-local` owns command defaulting, timeout/abort classification, the terminal environment, and the background read merge; process groups, bounded collectors, spill files, credential scrubbing, and disposal quiescence are the [subprocess service](subprocess.md)'s. `dsh-tool-bash` owns model-facing rendering and adapts background handles into the [generic task runtime](tasks.md). `dsh-bash` owns the shell tools' shared exit-status contract: the exported `parseExitStatus`/`ParsedExitStatus` inverts the `[exit code: N]` / `[killed by signal: X]` markers `dsh-tool-bash`'s `renderResult` and `dsh-tool-pwsh`'s `renderPwshResult` append, and both tools' `presentResult` use it to split the rendered text into the terminal card's output body and its exit-status pill.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxbash--bashexecutor-abstract-seam"></a>
### `ctx.bash` — `BashExecutor` (abstract seam)
Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.bash` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
Implementations must honor these semantics:
- run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a BashRunResult.
- start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr.
- BashProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files.
- A still-running background process is stopped and awaited when its owning composition tears down. With the subprocess seam that boundary is `ctx.subprocess` disposal, so a background process survives an executor-only reload.
```ts cordis-catalog
/**
* Apply implementation-owned defaults and caps to a request before execution.
* @param request - the caller's request; omitted fields get this
* implementation's defaults, capped fields are clamped.
* @returns the fully-specified spec to hand to {@link run}/{@link start}.
*/
abstract resolve(request: BashExecRequest): BashExecSpec
/**
* Run a command in the foreground; resolves when it finishes.
* @param spec - a resolved spec from {@link resolve}, never a raw request.
* @returns the outcome; nonzero exits, timeout kills, and abort kills
* resolve with a descriptive result rather than reject.
*/
abstract run(spec: BashExecSpec): Promise<BashRunResult>
/**
* Start a background process and return its handle immediately.
* @param spec - a resolved spec from {@link resolve}, never a raw request.
* @returns the live process handle (reads, kill, quiescence promise).
*/
abstract start(spec: BashExecSpec): BashProcess
```
Source: [`packages/bash/bash/src/index.ts:53`](../../packages/bash/bash/src/index.ts)
<a id="ctxbashenv--bashenvregistry"></a>
### `ctx.bashEnv` — `BashEnvRegistry`
Registry (`ctx.bashEnv`) for trusted, per-execution `DSH_*` variables. The namespace is rebuilt for every model shell call: ambient `DSH_*` values are discarded by the executor, then the registry's current snapshot is injected. Built-in shell facts remain owned by the registry itself while plugins can register additional, enumerable facts with effect-scoped disposal.
```ts cordis-catalog
/**
* Register one environment contributor. Names and keys are unique; built-in
* keys are reserved. Registration is disposed with the calling plugin fiber.
* @param contributor - declared key ownership and per-execution resolver.
* @returns the disposer that unregisters the contribution.
*/
register(contributor: BashEnvContributor): () => void
/**
* Build the trusted `DSH_*` snapshot for one shell tool execution.
* @param execution - the current tool execution.
* @returns an immutable environment overlay containing built-ins and current contributions.
*/
collect(execution: ToolExecution): DshEnvironment
/**
* Enumerate plugin-contributed variables without executing their resolvers.
* @returns declarations sorted by environment variable name.
*/
list(): BashEnvVariableInfo[]
```
Types: [DshEnvironment](subprocess.md) · [ToolExecution](tools.md)
Source: [`packages/bash/bash-env/src/index.ts:89`](../../packages/bash/bash-env/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -219,3 +219,85 @@ interface BashProcessRead {
## 服务
`BashExecutor` 拥有 `resolve`、前台 `run`、后台进程 `start` 以及 `sandboxMode` 能力事实。`dsh-bash-local` 拥有命令默认值补全、超时/中止分类、终端环境以及后台读取合并进程组、有界收集器、spill 文件、凭据清除与 dispose资源释放后完全停稳归[进程管理器](subprocess.md)所有。`dsh-tool-bash` 拥有面向模型的渲染,并将后台句柄适配到[通用任务运行时](tasks.md)。`dsh-bash` 拥有 shell 工具共享的退出状态契约:导出的 `parseExitStatus`/`ParsedExitStatus` 是 `dsh-tool-bash` 的 `renderResult` 与 `dsh-tool-pwsh` 的 `renderPwshResult` 所追加的 `[exit code: N]` / `[killed by signal: X]` 标记的逆解析,两个工具的 `presentResult` 都用它把渲染文本拆分为 terminal 卡的输出正文与退出状态 pill。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxbash--bashexecutor-abstract-seam"></a>
### `ctx.bash` — `BashExecutor` (abstract seam)
Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.bash` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
Implementations must honor these semantics:
- run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a BashRunResult.
- start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr.
- BashProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files.
- A still-running background process is stopped and awaited when its owning composition tears down. With the subprocess seam that boundary is `ctx.subprocess` disposal, so a background process survives an executor-only reload.
```ts cordis-catalog
/**
* Apply implementation-owned defaults and caps to a request before execution.
* @param request - the caller's request; omitted fields get this
* implementation's defaults, capped fields are clamped.
* @returns the fully-specified spec to hand to {@link run}/{@link start}.
*/
abstract resolve(request: BashExecRequest): BashExecSpec
/**
* Run a command in the foreground; resolves when it finishes.
* @param spec - a resolved spec from {@link resolve}, never a raw request.
* @returns the outcome; nonzero exits, timeout kills, and abort kills
* resolve with a descriptive result rather than reject.
*/
abstract run(spec: BashExecSpec): Promise<BashRunResult>
/**
* Start a background process and return its handle immediately.
* @param spec - a resolved spec from {@link resolve}, never a raw request.
* @returns the live process handle (reads, kill, quiescence promise).
*/
abstract start(spec: BashExecSpec): BashProcess
```
Source: [`packages/bash/bash/src/index.ts:53`](../../packages/bash/bash/src/index.ts)
<a id="ctxbashenv--bashenvregistry"></a>
### `ctx.bashEnv` — `BashEnvRegistry`
Registry (`ctx.bashEnv`) for trusted, per-execution `DSH_*` variables. The namespace is rebuilt for every model shell call: ambient `DSH_*` values are discarded by the executor, then the registry's current snapshot is injected. Built-in shell facts remain owned by the registry itself while plugins can register additional, enumerable facts with effect-scoped disposal.
```ts cordis-catalog
/**
* Register one environment contributor. Names and keys are unique; built-in
* keys are reserved. Registration is disposed with the calling plugin fiber.
* @param contributor - declared key ownership and per-execution resolver.
* @returns the disposer that unregisters the contribution.
*/
register(contributor: BashEnvContributor): () => void
/**
* Build the trusted `DSH_*` snapshot for one shell tool execution.
* @param execution - the current tool execution.
* @returns an immutable environment overlay containing built-ins and current contributions.
*/
collect(execution: ToolExecution): DshEnvironment
/**
* Enumerate plugin-contributed variables without executing their resolvers.
* @returns declarations sorted by environment variable name.
*/
list(): BashEnvVariableInfo[]
```
Types: [DshEnvironment](subprocess.md) · [ToolExecution](tools.md)
Source: [`packages/bash/bash-env/src/index.ts:89`](../../packages/bash/bash-env/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/client-modules.md
client-modules.md: 5dc89a87db7e79e4a297002fa059fdeffcceda20
client-modules.zh.md: b3aa9c2aecf8e051a41b7b05f05d364b9ff8d2a6
client-modules.md: 19bcc6304e9d917c3950fd9857e7b2c2b3c16e73
client-modules.zh.md: b0ad293cad2f67de1982ef271308b2a7f06ebaf4

View File

@@ -58,6 +58,61 @@ Package metadata — including the negative "not a client package" verdict — i
## The service
`ClientModuleHostService` (`ctx.clientModuleHost`, defined in [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts)) exposes reads and the rebuild face; signatures are in the generated [service catalog](../cordis-catalog/services.md#ctxclientmodulehost--clientmodulehostservice). `graph()` returns the current composed graph (a stable object between changes) and `clientPath(id)` the bundle's absolute path. `rebuilt(id)` is the only entry point through which bundle content reaches the graph: it re-hashes the file, and only a real rev change recomposes the graph and notifies. `onRebuilt` fires per changed bundle with the new rev; `onGraphChanged` fires after any flush that recomposed the graph (row added or removed, or a rebuilt rev change) and is pull-model — listeners re-read `graph()`. Both notification paths contain listener exceptions so one throwing subscriber cannot skip later subscribers or kill whatever triggered the flush.
`ClientModuleHostService` (`ctx.clientModuleHost`, defined in [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts)) exposes reads and the rebuild face; signatures are in the generated [service catalog](#ctxclientmodulehost--clientmodulehostservice). `graph()` returns the current composed graph (a stable object between changes) and `clientPath(id)` the bundle's absolute path. `rebuilt(id)` is the only entry point through which bundle content reaches the graph: it re-hashes the file, and only a real rev change recomposes the graph and notifies. `onRebuilt` fires per changed bundle with the new rev; `onGraphChanged` fires after any flush that recomposed the graph (row added or removed, or a rebuilt rev change) and is pull-model — listeners re-read `graph()`. Both notification paths contain listener exceptions so one throwing subscriber cannot skip later subscribers or kill whatever triggered the flush.
In development, [dsh-client-hmr](../../packages/client/hmr/README.md) is the registry's watch driver: its node half stat-polls every graph row's bundle from a synchronously captured baseline, calls `rebuilt(id)` on change, resyncs its watch set through `onGraphChanged`, and broadcasts rev changes to the browser half over SSE. Production graphs omit the HMR row entirely; the module host itself never watches files.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxclientmodulehost--clientmodulehostservice"></a>
### `ctx.clientModuleHost` — `ClientModuleHostService`
The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
```ts cordis-catalog
/**
* Current composed entry graph (stable object between changes).
* @returns the graph served as `window.__DSH_BOOT__`.
*/
graph(): WebBootGraph
/**
* Absolute path of an entry's client bundle.
* @param id - entry id (package name).
* @returns the path, or undefined for an unknown id.
*/
clientPath(id: string): string | undefined
/**
* Re-hash one bundle (the HMR watch's registration hook — the only entry
* point through which bundle content changes reach the graph).
* @param id - entry id (package name).
* @returns the new rev, or undefined for an unknown id.
*/
rebuilt(id: string): string | undefined
/**
* Subscribe to bundle rebuilds; fires only when the re-hash changed the rev.
* @param listener - receives the entry id and its new bundle rev.
* @returns the unsubscriber.
*/
onRebuilt(listener: (id: string, rev: string) => void): () => void
/**
* Fires after any flush that recomposed the graph (row added/removed, or a
* rebuilt rev change). Pull model: listeners re-read {@link graph}.
* @param listener - notified with no payload.
* @returns the unsubscriber.
*/
onGraphChanged(listener: () => void): () => void
```
Source: [`packages/client/modules/src/index.ts:184`](../../packages/client/modules/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -58,6 +58,61 @@ interface WebBootGraph {
## 服务
`ClientModuleHostService``ctx.clientModuleHost`,定义于 [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts))暴露读取面与重建面;签名见生成的[服务目录](../cordis-catalog/services.md#ctxclientmodulehost--clientmodulehostservice)。`graph()` 返回当前组合出的图(两次变更之间是同一个稳定对象),`clientPath(id)` 返回该 bundle 的绝对路径。`rebuilt(id)` 是 bundle 内容到达图的唯一入口:它对文件重新哈希,只有 rev 真正变化才会重新组合图并发出通知。`onRebuilt` 按发生变化的 bundle 逐个触发并携带新 rev`onGraphChanged` 在任何一次重新组合了图的 flush 之后触发(行的增删,或 rebuilt 带来的 rev 变化),并采用拉取模型——监听器自行重读 `graph()`。两条通知路径都会兜住监听器异常,因此一个抛错的订阅者既不能让后续订阅者被跳过,也不能杀死触发这次 flush 的一方。
`ClientModuleHostService``ctx.clientModuleHost`,定义于 [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts))暴露读取面与重建面;签名见生成的[服务目录](#ctxclientmodulehost--clientmodulehostservice)。`graph()` 返回当前组合出的图(两次变更之间是同一个稳定对象),`clientPath(id)` 返回该 bundle 的绝对路径。`rebuilt(id)` 是 bundle 内容到达图的唯一入口:它对文件重新哈希,只有 rev 真正变化才会重新组合图并发出通知。`onRebuilt` 按发生变化的 bundle 逐个触发并携带新 rev`onGraphChanged` 在任何一次重新组合了图的 flush 之后触发(行的增删,或 rebuilt 带来的 rev 变化),并采用拉取模型——监听器自行重读 `graph()`。两条通知路径都会兜住监听器异常,因此一个抛错的订阅者既不能让后续订阅者被跳过,也不能杀死触发这次 flush 的一方。
开发环境下,[dsh-client-hmr](../../packages/client/hmr/README.md) 是注册表的监视驱动:它的 Node 半从同步取得的基线出发,对图中每一行的 bundle 做 stat 轮询,变化时调用 `rebuilt(id)`,经 `onGraphChanged` 重新同步监视集合,并通过 SSEServer-Sent Events把 rev 变化广播给浏览器半。生产环境的图完全不含 HMR热模块替换模块宿主自身从不监视文件。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxclientmodulehost--clientmodulehostservice"></a>
### `ctx.clientModuleHost` — `ClientModuleHostService`
The web plugin table service: incremental dshClient scan + wire composition + bundle route + index tap. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
```ts cordis-catalog
/**
* Current composed entry graph (stable object between changes).
* @returns the graph served as `window.__DSH_BOOT__`.
*/
graph(): WebBootGraph
/**
* Absolute path of an entry's client bundle.
* @param id - entry id (package name).
* @returns the path, or undefined for an unknown id.
*/
clientPath(id: string): string | undefined
/**
* Re-hash one bundle (the HMR watch's registration hook — the only entry
* point through which bundle content changes reach the graph).
* @param id - entry id (package name).
* @returns the new rev, or undefined for an unknown id.
*/
rebuilt(id: string): string | undefined
/**
* Subscribe to bundle rebuilds; fires only when the re-hash changed the rev.
* @param listener - receives the entry id and its new bundle rev.
* @returns the unsubscriber.
*/
onRebuilt(listener: (id: string, rev: string) => void): () => void
/**
* Fires after any flush that recomposed the graph (row added/removed, or a
* rebuilt rev change). Pull model: listeners re-read {@link graph}.
* @param listener - notified with no payload.
* @returns the unsubscriber.
*/
onGraphChanged(listener: () => void): () => void
```
Source: [`packages/client/modules/src/index.ts:184`](../../packages/client/modules/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/code-runtime.md
code-runtime.md: fbce7d812b7609716fb43ae01610253008e0a92c
code-runtime.zh.md: 700146cfaa9cfab37ec4d85e550020acf1d9f294
# pnpm run verify-translation-pairing --write docs/subsystems/code-runtime.md
code-runtime.md: 12779f1aafc8930410ae0d695e3f617caafcf607
code-runtime.zh.md: de563dd3430348f5cf3675d4bf124adeba170559

View File

@@ -159,3 +159,33 @@ interface CodeRunFailure {
## The service
`CodeRuntime` (`ctx.codeRuntime`, abstract — defined in [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts)) is `run(request)` plus two readonly descriptors: `language` (what the program must be written in — `'typescript'` and `'python'` are the well-known values, those `dsh-tools` presents, and only `'typescript'` has a published backend; a consumer generating language-specific presentation switches on it and fails loud on one it cannot present) and `isolation` (the execution substrate — `'worker-thread'`, `'process'`, `'container'`; a diagnostic label, **not a security claim**). Implementations must keep runs isolated from each other (no cross-run state) and dispose to quiescence: in-flight runs are terminated and awaited before teardown completes.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxcoderuntime--coderuntime-abstract-seam"></a>
### `ctx.codeRuntime` — `CodeRuntime` (abstract seam)
Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and substrate failures resolve in CodeRunResult; only seam misuse rejects. Implementations bridge structured-cloneable bindings, materialize each declared namespace rejection class, treat programs as hostile peers, isolate runs from one another, and terminate and await in-flight runs during disposal.
```ts cordis-catalog
/**
* Execute one program against the request's bindings and capture what it
* emitted. See the class doc for the resolution contract (error is a result
* field; rejection means seam misuse only).
* @param request - the program, its bindings, and the abort signal; the
* request carries everything the runtime acts on, with no hidden defaults.
* @returns the run's outcome: completion value (when transferable), the
* ordered log capture, and the failure (if any).
*/
abstract run(request: CodeRunRequest): Promise<CodeRunResult>
```
Source: [`packages/code-runtime/code-runtime/src/index.ts:104`](../../packages/code-runtime/code-runtime/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -159,3 +159,33 @@ interface CodeRunFailure {
## 服务
`CodeRuntime``ctx.codeRuntime`,抽象服务,定义于 [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts))由 `run(request)` 加两个只读描述符组成:`language`(程序必须使用的语言,已知值为 `'typescript'` 与 `'python'`,即 `dsh-tools` 能呈现的那些,其中只有 `'typescript'` 有已发布的后端;生成语言相关展示的消费方据此切换,遇到无法展示的语言时应显式报错)和 `isolation`(执行基底,`'worker-thread'`、`'process'`、`'container'`;仅为诊断标签,**不构成安全承诺**)。实现必须保证各次运行彼此隔离(无跨运行状态),并在 dispose资源释放时等待系统完全停稳teardown 要等到所有进行中的运行均已终止并结算后才完成。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxcoderuntime--coderuntime-abstract-seam"></a>
### `ctx.codeRuntime` — `CodeRuntime` (abstract seam)
Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and substrate failures resolve in CodeRunResult; only seam misuse rejects. Implementations bridge structured-cloneable bindings, materialize each declared namespace rejection class, treat programs as hostile peers, isolate runs from one another, and terminate and await in-flight runs during disposal.
```ts cordis-catalog
/**
* Execute one program against the request's bindings and capture what it
* emitted. See the class doc for the resolution contract (error is a result
* field; rejection means seam misuse only).
* @param request - the program, its bindings, and the abort signal; the
* request carries everything the runtime acts on, with no hidden defaults.
* @returns the run's outcome: completion value (when transferable), the
* ordered log capture, and the failure (if any).
*/
abstract run(request: CodeRunRequest): Promise<CodeRunResult>
```
Source: [`packages/code-runtime/code-runtime/src/index.ts:104`](../../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/commands.md
commands.md: 83ce9c2498df58110d9a9c2e7d5163a9df813e1c
commands.zh.md: c486f480177c7ff8b35cc846beafac009cea42bb
commands.md: c1ab6d9782f515321104d0cf4d9e037c6a68a18d
commands.zh.md: 3b3d8def042b107c5098e76c511424f023975e77

View File

@@ -97,3 +97,89 @@ interface ParsedCommand {
readonly rawInput: string
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxcommands--commandservice"></a>
### `ctx.commands` — `CommandService`
Human-command registry. Plain-context definitions are global; definitions registered through a command-injected child of an agent context shadow globals for that agent.
```ts cordis-catalog
/**
* Register a global or calling-agent-scoped command.
* @param definition - discovery metadata and direct UI handler.
* @returns the exact effect disposer that unregisters this definition.
*/
register(definition: CommandDefinition): () => void
/**
* List the effective immutable command descriptors for one agent.
* @param agent - exact receiving agent and scoped-layer key.
* @returns name-sorted descriptors after scoped shadowing.
*/
list(agent: Agent): readonly CommandDescriptor[]
/**
* Resolve one effective command definition.
* @param agent - exact receiving agent and scoped-layer key.
* @param name - command name without a slash.
* @returns the scoped shadow or global definition.
*/
find(agent: Agent, name: string): CommandDefinition | undefined
/**
* Parse and execute a known command without sending it to the model.
*
* A resolved command's lifecycle is logged: `command/run` is appended
* before the handler is invoked and `command/done` after settlement (a
* thrown or aborted handler settles as `kind: 'error'`). Both are direct
* log-only appends — no turn wraps them, and persistence drains them at
* ordinary checkpoints. Admission misses (syntax or unknown name) log
* nothing — they never entered a handler. A `command/run` append failure
* fails the execution loud; a `command/done` append failure on the
* handler-failure path is contained so the handler's own error stays the
* reported failure.
*
* @param agent - exact receiving agent.
* @param line - complete slash-command line.
* @param signal - cancellation signal owned by the UI request.
* @returns the settled execution (result + lifecycle pairing id), or
* `undefined` when syntax or name does not resolve.
*/
async execute( agent: Agent, line: string, signal: AbortSignal, ): Promise<CommandExecution | undefined>
```
Types: [Agent](core.md)
Source: [`packages/interaction/commands/src/index.ts:305`](../../packages/interaction/commands/src/index.ts)
<a id="commands-events"></a>
### `commands/*` events
<a id="commandschange--emit"></a>
#### `commands/change` — emit
A command was registered or unregistered. This is an unfiltered registry notification because a global or scoped change may affect any UI view. Observer failures are contained and cannot veto the registry mutation.
```ts cordis-catalog
/**
* A command was registered or unregistered. This is an unfiltered registry
* notification because a global or scoped change may affect any UI view.
* Observer failures are contained and cannot veto the registry mutation.
* @mode emit
*/
'commands/change'(): void
```
Source: [`packages/interaction/commands/src/index.ts:172`](../../packages/interaction/commands/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -97,3 +97,89 @@ interface ParsedCommand {
readonly rawInput: string
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxcommands--commandservice"></a>
### `ctx.commands` — `CommandService`
Human-command registry. Plain-context definitions are global; definitions registered through a command-injected child of an agent context shadow globals for that agent.
```ts cordis-catalog
/**
* Register a global or calling-agent-scoped command.
* @param definition - discovery metadata and direct UI handler.
* @returns the exact effect disposer that unregisters this definition.
*/
register(definition: CommandDefinition): () => void
/**
* List the effective immutable command descriptors for one agent.
* @param agent - exact receiving agent and scoped-layer key.
* @returns name-sorted descriptors after scoped shadowing.
*/
list(agent: Agent): readonly CommandDescriptor[]
/**
* Resolve one effective command definition.
* @param agent - exact receiving agent and scoped-layer key.
* @param name - command name without a slash.
* @returns the scoped shadow or global definition.
*/
find(agent: Agent, name: string): CommandDefinition | undefined
/**
* Parse and execute a known command without sending it to the model.
*
* A resolved command's lifecycle is logged: `command/run` is appended
* before the handler is invoked and `command/done` after settlement (a
* thrown or aborted handler settles as `kind: 'error'`). Both are direct
* log-only appends — no turn wraps them, and persistence drains them at
* ordinary checkpoints. Admission misses (syntax or unknown name) log
* nothing — they never entered a handler. A `command/run` append failure
* fails the execution loud; a `command/done` append failure on the
* handler-failure path is contained so the handler's own error stays the
* reported failure.
*
* @param agent - exact receiving agent.
* @param line - complete slash-command line.
* @param signal - cancellation signal owned by the UI request.
* @returns the settled execution (result + lifecycle pairing id), or
* `undefined` when syntax or name does not resolve.
*/
async execute( agent: Agent, line: string, signal: AbortSignal, ): Promise<CommandExecution | undefined>
```
Types: [Agent](core.md)
Source: [`packages/interaction/commands/src/index.ts:305`](../../packages/interaction/commands/src/index.ts)
<a id="commands-events"></a>
### `commands/*` events
<a id="commandschange--emit"></a>
#### `commands/change` — emit
A command was registered or unregistered. This is an unfiltered registry notification because a global or scoped change may affect any UI view. Observer failures are contained and cannot veto the registry mutation.
```ts cordis-catalog
/**
* A command was registered or unregistered. This is an unfiltered registry
* notification because a global or scoped change may affect any UI view.
* Observer failures are contained and cannot veto the registry mutation.
* @mode emit
*/
'commands/change'(): void
```
Source: [`packages/interaction/commands/src/index.ts:172`](../../packages/interaction/commands/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: f1df5b83bd43136af60988dabd9dc68fe32467a2
compaction.zh.md: 52540250d466f81f51cf7c681bb6f1436e29a12d
compaction.md: 126c6c7c7df125cf991ea074e0165481c09455dc
compaction.zh.md: 9fb01a41a92367b9bb3f4586f35b5d0baf613bbc

View File

@@ -112,3 +112,119 @@ interface PruneResult {
readonly charsRemoved: number
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxcompact--compactservice-abstract-seam"></a>
### `ctx.compact` — `CompactService` (abstract seam)
Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses COMPACT_CHECKPOINT_SOURCE so consumers recognize it independently of the backend. Load one implementation per context as `ctx.compact`.
```ts cordis-catalog
/**
* Consider automatic compaction for one explicit trigger. Pressure policy
* uses the latest durable routed request, while context-overflow policy may
* force a useful balanced reduction even below the normal threshold. Return
* `null` when no safe range can be compacted. A single oversized retained
* unit or request envelope cannot be repaired through surface compaction.
*
* @param agent - agent context owning the session surface and routing options.
* @param trigger - normal pressure or provider-confirmed context overflow.
* @param signal - cancellation signal; model-backed implementations must forward it.
* @returns the compaction result, or `null` if no compaction was needed.
*/
abstract compactIfNeeded( agent: CompactAgentContext, trigger: CompactionTrigger, signal: AbortSignal, ): Promise<CompactionResult | null>
/**
* Explicitly compact useful history even below automatic pressure thresholds.
* Implementations synchronously start an idle task before any asynchronous
* work, select a useful range without writing on a no-op, then
* append a standalone `compact/start` before summarization. That durable
* marker is the compaction lock until one `compact/end` attempt. Later waking
* prompts remain accepted in FIFO order and start only after the optional
* durability checkpoint and idle-task settlement. Context injected while the
* summary runs may sit between the marker pair; only the selected span must
* remain stable.
*
* @param agent - idle agent whose durable history should be compacted.
* @param signal - cancellation scoped to this compaction request.
* @returns the compaction result, or `null` when no safe useful range exists.
* @throws {@link ManualCompactionError} for expected busy, agent-cancellation,
* changed-span, summarization/shrink, commit-stage, or persistence failures;
* an aborted request preserves its exact abort reason. Failed attempts remain
* visible in the log.
*/
abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): Promise<CompactionResult | null>
/**
* Forcibly compact a range of surface nodes into a single summary node.
* `start` and `end` name an inclusive span by surface position, not numeric seq
* order; replacements can make visible seqs non-monotonic. Both edges must be
* balanced so assistant tool calls remain paired with their results. A model-
* backed implementation forwards cancellation and rejects active, missing,
* reversed, or unbalanced ranges. The target session is `agent.session`.
* Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}.
* Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}
* for the edge checks.
*
* @param start - first surface seq, inclusive.
* @param end - last surface seq, inclusive.
* @param agent - context whose session is mutated and whose routing options guide summarization.
* @param signal - optional cancellation; model-backed implementations must forward it.
* @throws when compaction is active or the range is missing, reversed, or unbalanced.
* @returns the appended event seqs, summary, replaced range, and token accounting.
*/
abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
```
Source: [`packages/compact/compact/src/index.ts:93`](../../packages/compact/compact/src/index.ts)
<a id="ctxtoolresultprune--toolresultpruneservice"></a>
### `ctx.toolResultPrune` — `ToolResultPruneService`
Deterministic head/middle/tail pruning for current tool-result surface nodes.
```ts cordis-catalog
/**
* Measure text content in Unicode code points; non-text blocks cost zero.
* @param blocks - tool-result content to measure.
* @returns total Unicode code points across text blocks.
*/
measureContent(blocks: readonly ContentBlock[]): number
/**
* Replace an over-budget text middle while retaining rich-block order.
* Text slicing is by Unicode code point, not UTF-16 code unit, so a retained
* boundary cannot split a surrogate pair. Grapheme clusters may still split.
* @param blocks - original tool-result content.
* @returns pruned content, or `null` when the text is within budget.
*/
pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null
/**
* Prune every over-budget tool result from one stable current-surface snapshot.
* Each replacement preserves the complete event data except for `content`,
* points at the shadowed node for durable provenance and replay, and is
* immediately preceded by a `compact/prune` shadow-price event pricing the
* shadowed node through the injected token meter, so pure consumers can
* subtract it without per-node state.
* @param session - session whose current surface is rewritten.
* @returns landed replacements and aggregate Unicode-code-point savings.
* @throws when the session rejects a replacement; replacements committed
* earlier in the pass remain durable.
*/
pruneSession(session: Session): PruneResult
```
Types: [ContentBlock](core.md) · [Session](session.md)
Source: [`packages/compact/compact-tool-result-prune/src/index.ts:44`](../../packages/compact/compact-tool-result-prune/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -112,3 +112,119 @@ interface PruneResult {
readonly charsRemoved: number
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxcompact--compactservice-abstract-seam"></a>
### `ctx.compact` — `CompactService` (abstract seam)
Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. The replacement user message uses COMPACT_CHECKPOINT_SOURCE so consumers recognize it independently of the backend. Load one implementation per context as `ctx.compact`.
```ts cordis-catalog
/**
* Consider automatic compaction for one explicit trigger. Pressure policy
* uses the latest durable routed request, while context-overflow policy may
* force a useful balanced reduction even below the normal threshold. Return
* `null` when no safe range can be compacted. A single oversized retained
* unit or request envelope cannot be repaired through surface compaction.
*
* @param agent - agent context owning the session surface and routing options.
* @param trigger - normal pressure or provider-confirmed context overflow.
* @param signal - cancellation signal; model-backed implementations must forward it.
* @returns the compaction result, or `null` if no compaction was needed.
*/
abstract compactIfNeeded( agent: CompactAgentContext, trigger: CompactionTrigger, signal: AbortSignal, ): Promise<CompactionResult | null>
/**
* Explicitly compact useful history even below automatic pressure thresholds.
* Implementations synchronously start an idle task before any asynchronous
* work, select a useful range without writing on a no-op, then
* append a standalone `compact/start` before summarization. That durable
* marker is the compaction lock until one `compact/end` attempt. Later waking
* prompts remain accepted in FIFO order and start only after the optional
* durability checkpoint and idle-task settlement. Context injected while the
* summary runs may sit between the marker pair; only the selected span must
* remain stable.
*
* @param agent - idle agent whose durable history should be compacted.
* @param signal - cancellation scoped to this compaction request.
* @returns the compaction result, or `null` when no safe useful range exists.
* @throws {@link ManualCompactionError} for expected busy, agent-cancellation,
* changed-span, summarization/shrink, commit-stage, or persistence failures;
* an aborted request preserves its exact abort reason. Failed attempts remain
* visible in the log.
*/
abstract compactNow( agent: ManualCompactAgentContext, signal: AbortSignal, ): Promise<CompactionResult | null>
/**
* Forcibly compact a range of surface nodes into a single summary node.
* `start` and `end` name an inclusive span by surface position, not numeric seq
* order; replacements can make visible seqs non-monotonic. Both edges must be
* balanced so assistant tool calls remain paired with their results. A model-
* backed implementation forwards cancellation and rejects active, missing,
* reversed, or unbalanced ranges. The target session is `agent.session`.
* Its replacement user message must use {@link COMPACT_CHECKPOINT_SOURCE}.
* Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter}
* for the edge checks.
*
* @param start - first surface seq, inclusive.
* @param end - last surface seq, inclusive.
* @param agent - context whose session is mutated and whose routing options guide summarization.
* @param signal - optional cancellation; model-backed implementations must forward it.
* @throws when compaction is active or the range is missing, reversed, or unbalanced.
* @returns the appended event seqs, summary, replaced range, and token accounting.
*/
abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise<CompactionResult>
```
Source: [`packages/compact/compact/src/index.ts:93`](../../packages/compact/compact/src/index.ts)
<a id="ctxtoolresultprune--toolresultpruneservice"></a>
### `ctx.toolResultPrune` — `ToolResultPruneService`
Deterministic head/middle/tail pruning for current tool-result surface nodes.
```ts cordis-catalog
/**
* Measure text content in Unicode code points; non-text blocks cost zero.
* @param blocks - tool-result content to measure.
* @returns total Unicode code points across text blocks.
*/
measureContent(blocks: readonly ContentBlock[]): number
/**
* Replace an over-budget text middle while retaining rich-block order.
* Text slicing is by Unicode code point, not UTF-16 code unit, so a retained
* boundary cannot split a surrogate pair. Grapheme clusters may still split.
* @param blocks - original tool-result content.
* @returns pruned content, or `null` when the text is within budget.
*/
pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null
/**
* Prune every over-budget tool result from one stable current-surface snapshot.
* Each replacement preserves the complete event data except for `content`,
* points at the shadowed node for durable provenance and replay, and is
* immediately preceded by a `compact/prune` shadow-price event pricing the
* shadowed node through the injected token meter, so pure consumers can
* subtract it without per-node state.
* @param session - session whose current surface is rewritten.
* @returns landed replacements and aggregate Unicode-code-point savings.
* @throws when the session rejects a replacement; replacements committed
* earlier in the pass remain durable.
*/
pruneSession(session: Session): PruneResult
```
Types: [ContentBlock](core.md) · [Session](session.md)
Source: [`packages/compact/compact-tool-result-prune/src/index.ts:44`](../../packages/compact/compact-tool-result-prune/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/core.md
core.md: 3a8ccbef4163eabba1cd287284eda68135f2db13
core.zh.md: 0cd6a57971ee98c75b52dd45b6f648632dab8a90
core.md: 7e62de88519e5c0bf3a1a0b94de93635767f9f7b
core.zh.md: ef5b70ae336d55fd25c2251372705912473d07f7

View File

@@ -53,9 +53,10 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
| [storage.md](storage.md) | the storage subsystem: the backend seam (`StorageBackend`), `StorageForms`, `DomainSpec`/`Domain`, `domain/changed` |
| [workspace.md](workspace.md) | the workspace registry: `Workspace`/`WorkspaceId`, registration and resolution, the session `cwd` relationship |
| [client-modules.md](client-modules.md) | the web plugin table: `dshClient` declarations, `WebBootGraph` wire composition, the bundle route and index tap |
| [session-projection.md](session-projection.md) | the projection seam: `SessionProjectionMap`, the pure `ProjectionDefinition` unit, `ProjectionSnapshot`'s consistent cut, the change feed |
| [telemetry.md](telemetry.md) | the outbound reporting seam: `TelemetryRecord`/`TelemetrySeverity`, the `TelemetryBackend` contract, the `telemetry/record` redact waterfall |
> Type declarations and their JSDoc on these pages are source-equivalent and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Ordinary blocks preserve complete declarations; `public-api` blocks preserve body-stripped public class declarations. Cordis services use the generated [service catalog](../cordis-catalog/services.md).
> Type declarations and their JSDoc on these pages are source-equivalent and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Ordinary blocks preserve complete declarations; `public-api` blocks preserve body-stripped public class declarations. Cordis services use the generated [service catalog](#cordis-surface).
## The `…Map → derived-union` pattern
@@ -780,3 +781,546 @@ type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
The one pipeline-authoring type that is core: what every registered tool *is* — a model-facing `ToolSchema` plus an `execute` function and optional final-content and UI callbacks. A tool author rarely constructs it by hand (the `defineTool` DSL builds it with typed args), but it is the contract the registry holds and the loop dispatches through.
Its full fields, the `defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` typed schema DSL, the `ToolExecution`/`ToolExecutionResult` waterfall shapes, and the tool-presentation UI vocabulary are on **[tools.md](tools.md)**.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxagentloop--agentloop"></a>
### `ctx.agentLoop` — `AgentLoop`
Concrete agent factory and driver service.
```ts cordis-catalog
/**
* Create an agent and session under one caller-supplied identity, owned by
* the accessing fiber. Constructor-driven config calls mint a fresh combined
* id before entering this boundary.
* @param id - shared agent/session identity.
* @param options - concrete loop options.
* @param meta - optional fresh-session workspace metadata.
* @returns the published running agent.
*/
create(id: SessionId, options: AgentOptions = {}, meta: Pick<SessionHeader, 'cwd'> = {}): Agent
/**
* Create an owned agent on a caller-supplied session id.
* @param ownerCtx - caller context that structurally owns the lifecycle.
* @param options - identities, session seed/metadata, loop options, setup, and cancellation.
* @returns the published handle.
*/
async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>
/**
* Resume an owned agent from the configured persistence service.
* @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
* @param options - persisted identity, loop options, setup, and cancellation.
* @returns the published handle.
*/
async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
```
Types: [SessionHeader](persistence.md)
Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts)
<a id="ctxagents--agentregistry"></a>
### `ctx.agents` — `AgentRegistry`
Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory.
Initiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain.
```ts cordis-catalog
/**
* Read the Agent that initiated the inherited asynchronous driver chain.
* Use this optional form for logging, tracing, metrics, or host attribution
* that also supports agentless calls. When a parent creates a child, setup
* reports the causal parent while `agentCtx.agent` identifies the child.
* @returns the inherited Agent, or `undefined` outside an initiator boundary
* and inside an explicit clearing boundary.
* @throws when this service instance has been disposed.
*/
currentInitiator(): Agent | undefined
/**
* Read the initiating Agent and fail when no initiator boundary is active.
* Use this for private helpers contractually below a driver, or for a
* deployment-owned outbound request whose contract forbids agentless calls.
* Generic or direct-call seams use optional lookup or explicit request fields.
* @returns the inherited Agent.
* @throws when no initiator is active or this service instance has been disposed.
*/
requireInitiator(): Agent
/**
* Run an operation with one exact Agent as its process-local initiator. The
* exact synchronous value or Promise returned by the operation is preserved.
* Custom drivers and test harnesses wrap their complete returned foreground
* lifetime.
* A queue or wire receiver may establish this boundary only after validating
* explicit identity and resolving the exact live Agent; this method does neither.
* Detached work remains owned by the subsystem that starts it.
* @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization.
* @param operation - synchronous or asynchronous operation to invoke.
* @returns the exact value returned by `operation`.
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
*/
withInitiator<T>(agent: Agent, operation: () => T): T
/**
* Run an operation inside a boundary that hides any inherited initiating
* Agent. The exact synchronous value or Promise is preserved.
* Use this while creating lazy shared timers, queue pumps, pool maintenance,
* watchers, or exporters so they do not inherit the first Agent that happens
* to initialize them. It clears only initiator attribution, not explicit
* fields, and does not own or drain detached resources.
* @param operation - synchronous or asynchronous operation to invoke without an initiator.
* @returns the exact value returned by `operation`.
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
*/
withoutInitiator<T>(operation: () => T): T
/**
* Register the agent-creation factory (the loop calls this on construction,
* effect-scoped). A traced Cordis service is canonicalized to its concrete
* target; each create/resume call is then traced through that caller's
* context so ownership follows the caller without stacking proxy layers.
* Throws if a factory is already registered. Returns the disposer; on
* dispose the factory slot is cleared.
* @param factory - the loop-owned factory {@link create}/{@link resume} delegate to.
* @returns the disposer that clears the factory slot. The exact
* Cordis effect disposer (single-shot): composite (generator) effects may
* yield it directly — exact identity nests the teardown in order.
*/
setFactory(factory: AgentFactory): () => void
/**
* Create and publish a new agent through the registered factory.
* Distinct from {@link register} (which records an already-constructed
* agent): this constructs the agent and its session. Rejects if no factory is
* registered or creation/setup fails. The resolved {@link AgentHandle} lets
* the owner tear down exactly this agent.
* @param options - shared identity, session seed/metadata, and agent options.
* @returns the handle after setup, rollback-covered publication, and loop start complete.
*/
async create(options: CreateAgentOptions): Promise<AgentHandle>
/**
* Load a persisted session and resume an agent on it through the registered
* factory. Rejects if no factory is registered; the factory rejects if
* session persistence is not configured or persistence/setup fails.
* @param options - persisted identity, configuration, and optional setup.
* @returns the handle after setup, rollback-covered publication, and loop start complete.
*/
async resume(options: ResumeAgentOptions): Promise<AgentHandle>
/**
* Register a live agent. Throws if an agent with the same id is already
* registered. Emits `agent/created` on registration and `agent/disposed`
* when the calling fiber is disposed — both with the agent's scope carrier
* (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
* emits are scope-filtered regardless of which context invoked `register`
* (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
* requires passing the carrier). Returns the disposer.
* @param agent - the already-constructed agent to record in the store.
* @returns the EXACT Cordis effect disposer (single-shot; a repeat call
* returns undefined without awaiting an in-flight teardown). Exact
* identity is load-bearing: a composite (generator) effect that owns a
* teardown ORDER — the agent factory's lifecycle chain — must yield THIS
* function so Cordis nests the unregistration at that yield position;
* yielding a wrapper would leave it disposing as a concurrent sibling on
* owner unload, unregistering the agent (and emitting `agent/disposed`)
* while its final turn is still draining.
*/
register(agent: Agent): () => void
/**
* Insert an already-constructed agent without announcing it. This is the
* advanced ordered-lifecycle primitive used by the async agent factory: it
* first completes setup while the agent is unpublished, then assigns the
* returned detach closure into its pre-installed composite teardown before
* calling {@link announce}. Ordinary callers use {@link register}.
* @param agent - the prepared, unpublished agent.
* @param owner - live agent whose scoped context created this agent, or
* undefined for a top-level runtime root. This is runtime ownership, not
* the resumed session's durable parent lineage.
* @returns an idempotent closure that removes this exact entry and emits
* `agent/disposed` with listener failures contained. When called from a
* synchronous `agent/created` listener, removal and disposal wait until
* that creation dispatch unwinds.
*/
enter(agent: Agent, owner: Agent | undefined): () => void
/**
* Announce an agent previously inserted with {@link enter}.
* @param agent - the live inserted agent to announce.
* @throws if `agent` is not the exact live registry entry for its id, or its
* creation announcement already began (including a reentrant call from a
* creation listener).
*/
announce(agent: Agent): void
/**
* Look up a live agent.
* @param id - the shared agent/session id to look up.
* @returns the agent, or undefined when no live agent has that id.
*/
get(id: SessionId): Agent | undefined
/**
* Test whether a live agent was created through one exact parent agent's
* scoped context. Runtime ownership is independent of durable session
* lineage and remains unambiguous when unrelated providers reuse an id.
* @param id - the candidate child agent's shared agent/session id.
* @param owner - the expected runtime creator agent.
* @returns true only while the exact child entry is live under that owner.
*/
isOwnedBy(id: SessionId, owner: Agent): boolean
/**
* All live agents, in registration order.
* @returns a fresh array; mutating it does not affect the registry.
*/
list(): Agent[]
/**
* All live top-level agents in registration order. A top-level agent was
* created without an owning agent context; durable session lineage does not
* affect this runtime relation, so a resumed fork may still be a root.
* @returns a fresh array; mutating it does not affect the registry.
*/
roots(): Agent[]
```
Source: [`packages/core/agent/src/index.ts:253`](../../packages/core/agent/src/index.ts)
<a id="agent-events"></a>
### `agent/*` events
<a id="agentcreated--emit"></a>
#### `agent/created` — emit
A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving seam. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry.
```ts cordis-catalog
/**
* A fully configured agent and live session were published. Setup is
* composition-only; `agent/session-start` is the first startup-driving seam.
* Synchronous listener failure vetoes publication, while returned-promise
* rejection is reported. Detach requested during dispatch waits until every
* creation listener has observed the stable entry.
* @param payload.agent - the newly registered agent with its live session and completed setup.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/created'(this: Scoped<Agent>, payload: { agent: Agent }): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:158`](../../packages/core/agent/src/types.ts)
<a id="agentdisposed--emit"></a>
#### `agent/disposed` — emit
An agent left the registry; AgentLoop emits this after driver quiescence and scoped-registration unwind, but before session detachment. Custom registry users own their driver-ordering contract.
```ts cordis-catalog
/**
* An agent left the registry; AgentLoop emits this after driver quiescence
* and scoped-registration unwind, but before session detachment. Custom
* registry users own their driver-ordering contract.
* @param payload.agent - the exact agent removed from the registry.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/disposed'(this: Scoped<Agent>, payload: { agent: Agent }): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/types.ts)
<a id="agenterror--emit"></a>
#### `agent/error` — emit
A step or turn errored. The machine reports a failure here even when the error has no in-turn position for a durable record.
```ts cordis-catalog
/**
* A step or turn errored. The machine reports a failure here even when
* the error has no in-turn position for a durable record.
* @param payload.agent - the agent whose turn errored.
* @param payload.turn - the turn in which the failure surfaced.
* @param payload.step - the step at which the failure surfaced.
* @param payload.error - the failure, verbatim.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; error: unknown }): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:289`](../../packages/core/agent/src/types.ts)
<a id="agentinboxclaimed--emit"></a>
#### `agent/inbox/claimed` — emit
One message left the inbox inside its open turn. If the proposed step is rejected, the claimed message ends here: it is neither discarded nor re-emitted as a user/message, and the turn closes without a step.
```ts cordis-catalog
/**
* One message left the inbox inside its open turn. If the proposed step
* is rejected, the claimed message ends here: it is neither discarded nor
* re-emitted as a user/message, and the turn closes without a step.
* @param payload.agent - the agent whose inbox changed.
* @param payload.message - the claimed message.
* @param payload.turn - the owning turn.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/claimed'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage; turn: number }): void
```
Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent/src/types.ts:196`](../../packages/core/agent/src/types.ts)
<a id="agentinboxdiscarded--emit"></a>
#### `agent/inbox/discarded` — emit
One message was discarded from the live inbox.
```ts cordis-catalog
/**
* One message was discarded from the live inbox.
* @param payload.agent - the agent whose inbox changed.
* @param payload.message - the discarded message.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/discarded'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage }): void
```
Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/types.ts)
<a id="agentinboxinserted--emit"></a>
#### `agent/inbox/inserted` — emit
One message entered the live inbox.
```ts cordis-catalog
/**
* One message entered the live inbox.
* @param payload.agent - the agent whose inbox changed.
* @param payload.message - the inserted message.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/inserted'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage }): void
```
Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent/src/types.ts:185`](../../packages/core/agent/src/types.ts)
<a id="agentpre-step--waterfall"></a>
#### `agent/pre-step` — waterfall
Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages.
```ts cordis-catalog
/**
* Reject a proposed step or replace the messages that enter it. Calling
* `next()` preserves the current messages.
* @param payload.agent - the agent proposing the step.
* @param payload.messages - messages removed from the inbox for this step.
* @param payload.turn - the turn that will own the step.
* @param payload.step - the step proposed by the loop.
* @param payload.signal - the current turn's cancellation signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/pre-step'(this: Scoped<Agent>, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
```
Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent/src/types.ts:230`](../../packages/core/agent/src/types.ts)
<a id="agentrequest--waterfall"></a>
#### `agent/request` — waterfall
Replace the frozen call configuration. `await next()` yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. Model-visible content must use logged channels; this seam cannot mutate messages.
```ts cordis-catalog
/**
* Replace the frozen call configuration. `await next()` yields the config
* the machine would use (agent options on the first request, the logged
* header afterwards); return a replacement to switch. Model-visible
* content must use logged channels; this seam cannot mutate messages.
* @param payload.agent - the agent making the model call.
* @param payload.turn - the open turn number.
* @param payload.step - the step whose request this is.
* @param payload.signal - the current turn's explicit abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/request'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/types.ts)
<a id="agentrequest-error--waterfall"></a>
#### `agent/request-error` — waterfall
Handle one failed model-request attempt before the loop retries or closes its step. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns recovery, or calls `next()` to delegate. The default `undefined` leaves the failure terminal.
```ts cordis-catalog
/**
* Handle one failed model-request attempt before the loop retries or closes
* its step. A listener returns `{ kind: 'retry' }` without calling `next()`
* when it owns recovery, or calls `next()` to delegate. The default
* `undefined` leaves the failure terminal.
* @param payload.agent - the agent whose request failed.
* @param payload.turn - the turn containing the failed request.
* @param payload.step - the step containing the failed request attempt.
* @param payload.provider - the provider selected for the failed request.
* @param payload.failure - serializable facts normalized at the final adapter boundary.
* @param payload.retryPolicy - the policy of the adapter registration that served the failed request.
* @param payload.signal - the turn abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/request-error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; provider: string; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined; signal: AbortSignal }, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
```
Types: [LlmFailure](llm-streaming.md) · [ResolvedRetryPolicy](llm-streaming.md) · [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts)
<a id="agentsession-start--emit"></a>
#### `agent/session-start` — emit
The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts.
```ts cordis-catalog
/**
* The session lifecycle began, once before the first turn. Use
* `agent.inject()` to seed model-facing context. This is a notification, not
* a veto; disposal requested by a lifecycle owner is rechecked before the
* driver starts.
* @param payload.agent - the agent whose session lifecycle began.
* @param payload.source - why the session started (fresh startup, resume, …).
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/session-start'(this: Scoped<Agent>, payload: { agent: Agent; source: SessionStartSource }): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:216`](../../packages/core/agent/src/types.ts)
<a id="agentstatus--emit"></a>
#### `agent/status` — emit
Agent status changed (`idle` ⇄ `running`). A waking delivery enters `running` synchronously after reserving cancellation; `idle` means no driver remains scheduled or active.
```ts cordis-catalog
/**
* Agent status changed (`idle` ⇄ `running`). A waking delivery enters
* `running` synchronously after reserving cancellation; `idle` means no
* driver remains scheduled or active.
* @param payload.agent - the agent whose status flipped.
* @param payload.status - the status just entered (the transition's destination).
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/status'(this: Scoped<Agent>, payload: { agent: Agent; status: AgentStatus }): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:177`](../../packages/core/agent/src/types.ts)
<a id="agentturn-stopping--serial"></a>
#### `agent/turn-stopping` — serial
The turn is about to close: the model owes no response (no live tool calls, no fresh steering). Awaited before the boundary commits — a listener that objects steers (`agent.steer(...)`) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. Data decides, so listener order cannot change the outcome. The inverse control (stop a tool loop early) is data too: a tool result carrying `concludesTurn` ends the turn at its step. The conclusion never short-circuits already-submitted next-step work: same-step `additionalContexts` or racing steering still runs, and the turn closes only when that inbox drains.
```ts cordis-catalog
/**
* The turn is about to close: the model owes no response (no live tool
* calls, no fresh steering). Awaited before the boundary commits — a
* listener that objects steers (`agent.steer(...)`) and the machine
* re-reads its inbox: fresh steering runs another step, none closes the
* turn. Data decides, so listener order cannot change the outcome. The
* inverse control (stop a tool loop early) is data too: a tool result
* carrying `concludesTurn` ends the turn at its step. The conclusion
* never short-circuits already-submitted next-step work: same-step
* `additionalContexts` or racing steering still runs, and the turn
* closes only when that inbox drains.
* @param payload.agent - the agent whose turn is at its stop boundary.
* @param payload.turn - the turn about to close.
* @param payload.signal - the current turn's explicit abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode serial
*/
'agent/turn-stopping'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; signal: AbortSignal }): Promise<void> | void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:277`](../../packages/core/agent/src/types.ts)
<a id="agent-loop-events"></a>
### `agent-loop/*` events
<a id="agent-loopconfig-start-failed--emit"></a>
#### `agent-loop/config-start-failed` — emit
A declarative agent entry failed before it could publish a live agent. Consumers that buffer work for the configured identity use this transient signal to reject that work instead of waiting forever. Normal factory teardown suppresses failures from the cancelled startup attempt.
```ts cordis-catalog
/**
* A declarative agent entry failed before it could publish a live agent.
* Consumers that buffer work for the configured identity use this
* transient signal to reject that work instead of waiting forever. Normal
* factory teardown suppresses failures from the cancelled startup attempt.
* @param payload.sessionId - exact shared agent/session identity that failed startup.
* @param payload.error - persistence, setup, or publication failure.
* @mode emit
*/
'agent-loop/config-start-failed'(payload: { sessionId: SessionId; error: unknown }): void
```
Source: [`packages/core/agent-loop/src/index.ts:182`](../../packages/core/agent-loop/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -53,9 +53,10 @@ harness 是一个微内核:一个极小的核心加上众多插件。大多数
| [storage.md](storage.md) | 存储子系统:后端 seam`StorageBackend`)、`StorageForms``DomainSpec`/`Domain``domain/changed` |
| [workspace.md](workspace.md) | 工作区注册表:`Workspace`/`WorkspaceId`、注册与解析、与会话 `cwd` 的关系 |
| [client-modules.md](client-modules.md) | Web 插件表:`dshClient` 声明、`WebBootGraph` 线上组合、bundle 路由与 index 转换 |
| [session-projection.md](session-projection.md) | 投影 seam`SessionProjectionMap`、纯函数 `ProjectionDefinition` 单元、`ProjectionSnapshot` 的一致切面、变更馈送 |
| [telemetry.md](telemetry.md) | 对外上报 seam`TelemetryRecord`/`TelemetrySeverity``TelemetryBackend` 契约、`telemetry/record` 脱敏 waterfall |
> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务使用生成的[服务目录](../cordis-catalog/services.md)。
> 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务使用生成的[服务目录](#cordis-surface)。
<a id="the-map--derived-union-pattern"></a>
@@ -788,3 +789,546 @@ type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
唯一属于核心的流水线编写类型:每个已注册工具*是什么*——一个面向模型的 `ToolSchema` 加上一个 `execute` 函数,以及可选的最终内容回调与 UI 回调。工具作者很少手动构造它(`defineTool` DSL 会用类型化参数构建),但它是注册表持有、循环分发所经过的契约。
其完整字段、`defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` 类型化 schema DSL、`ToolExecution`/`ToolExecutionResult` waterfall 形状,以及工具展示 UI 词汇在 **[tools.md](tools.md)** 中。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxagentloop--agentloop"></a>
### `ctx.agentLoop` — `AgentLoop`
Concrete agent factory and driver service.
```ts cordis-catalog
/**
* Create an agent and session under one caller-supplied identity, owned by
* the accessing fiber. Constructor-driven config calls mint a fresh combined
* id before entering this boundary.
* @param id - shared agent/session identity.
* @param options - concrete loop options.
* @param meta - optional fresh-session workspace metadata.
* @returns the published running agent.
*/
create(id: SessionId, options: AgentOptions = {}, meta: Pick<SessionHeader, 'cwd'> = {}): Agent
/**
* Create an owned agent on a caller-supplied session id.
* @param ownerCtx - caller context that structurally owns the lifecycle.
* @param options - identities, session seed/metadata, loop options, setup, and cancellation.
* @returns the published handle.
*/
async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>
/**
* Resume an owned agent from the configured persistence service.
* @param ownerCtx - caller context that owns load, setup, and the live lifecycle.
* @param options - persisted identity, loop options, setup, and cancellation.
* @returns the published handle.
*/
async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>
```
Types: [SessionHeader](persistence.md)
Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent-loop/src/index.ts)
<a id="ctxagents--agentregistry"></a>
### `ctx.agents` — `AgentRegistry`
Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory.
Initiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain.
```ts cordis-catalog
/**
* Read the Agent that initiated the inherited asynchronous driver chain.
* Use this optional form for logging, tracing, metrics, or host attribution
* that also supports agentless calls. When a parent creates a child, setup
* reports the causal parent while `agentCtx.agent` identifies the child.
* @returns the inherited Agent, or `undefined` outside an initiator boundary
* and inside an explicit clearing boundary.
* @throws when this service instance has been disposed.
*/
currentInitiator(): Agent | undefined
/**
* Read the initiating Agent and fail when no initiator boundary is active.
* Use this for private helpers contractually below a driver, or for a
* deployment-owned outbound request whose contract forbids agentless calls.
* Generic or direct-call seams use optional lookup or explicit request fields.
* @returns the inherited Agent.
* @throws when no initiator is active or this service instance has been disposed.
*/
requireInitiator(): Agent
/**
* Run an operation with one exact Agent as its process-local initiator. The
* exact synchronous value or Promise returned by the operation is preserved.
* Custom drivers and test harnesses wrap their complete returned foreground
* lifetime.
* A queue or wire receiver may establish this boundary only after validating
* explicit identity and resolving the exact live Agent; this method does neither.
* Detached work remains owned by the subsystem that starts it.
* @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization.
* @param operation - synchronous or asynchronous operation to invoke.
* @returns the exact value returned by `operation`.
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
*/
withInitiator<T>(agent: Agent, operation: () => T): T
/**
* Run an operation inside a boundary that hides any inherited initiating
* Agent. The exact synchronous value or Promise is preserved.
* Use this while creating lazy shared timers, queue pumps, pool maintenance,
* watchers, or exporters so they do not inherit the first Agent that happens
* to initialize them. It clears only initiator attribution, not explicit
* fields, and does not own or drain detached resources.
* @param operation - synchronous or asynchronous operation to invoke without an initiator.
* @returns the exact value returned by `operation`.
* @throws when the initiator scope is closing/disposed, or when `operation` throws.
*/
withoutInitiator<T>(operation: () => T): T
/**
* Register the agent-creation factory (the loop calls this on construction,
* effect-scoped). A traced Cordis service is canonicalized to its concrete
* target; each create/resume call is then traced through that caller's
* context so ownership follows the caller without stacking proxy layers.
* Throws if a factory is already registered. Returns the disposer; on
* dispose the factory slot is cleared.
* @param factory - the loop-owned factory {@link create}/{@link resume} delegate to.
* @returns the disposer that clears the factory slot. The exact
* Cordis effect disposer (single-shot): composite (generator) effects may
* yield it directly — exact identity nests the teardown in order.
*/
setFactory(factory: AgentFactory): () => void
/**
* Create and publish a new agent through the registered factory.
* Distinct from {@link register} (which records an already-constructed
* agent): this constructs the agent and its session. Rejects if no factory is
* registered or creation/setup fails. The resolved {@link AgentHandle} lets
* the owner tear down exactly this agent.
* @param options - shared identity, session seed/metadata, and agent options.
* @returns the handle after setup, rollback-covered publication, and loop start complete.
*/
async create(options: CreateAgentOptions): Promise<AgentHandle>
/**
* Load a persisted session and resume an agent on it through the registered
* factory. Rejects if no factory is registered; the factory rejects if
* session persistence is not configured or persistence/setup fails.
* @param options - persisted identity, configuration, and optional setup.
* @returns the handle after setup, rollback-covered publication, and loop start complete.
*/
async resume(options: ResumeAgentOptions): Promise<AgentHandle>
/**
* Register a live agent. Throws if an agent with the same id is already
* registered. Emits `agent/created` on registration and `agent/disposed`
* when the calling fiber is disposed — both with the agent's scope carrier
* (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the
* emits are scope-filtered regardless of which context invoked `register`
* (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always
* requires passing the carrier). Returns the disposer.
* @param agent - the already-constructed agent to record in the store.
* @returns the EXACT Cordis effect disposer (single-shot; a repeat call
* returns undefined without awaiting an in-flight teardown). Exact
* identity is load-bearing: a composite (generator) effect that owns a
* teardown ORDER — the agent factory's lifecycle chain — must yield THIS
* function so Cordis nests the unregistration at that yield position;
* yielding a wrapper would leave it disposing as a concurrent sibling on
* owner unload, unregistering the agent (and emitting `agent/disposed`)
* while its final turn is still draining.
*/
register(agent: Agent): () => void
/**
* Insert an already-constructed agent without announcing it. This is the
* advanced ordered-lifecycle primitive used by the async agent factory: it
* first completes setup while the agent is unpublished, then assigns the
* returned detach closure into its pre-installed composite teardown before
* calling {@link announce}. Ordinary callers use {@link register}.
* @param agent - the prepared, unpublished agent.
* @param owner - live agent whose scoped context created this agent, or
* undefined for a top-level runtime root. This is runtime ownership, not
* the resumed session's durable parent lineage.
* @returns an idempotent closure that removes this exact entry and emits
* `agent/disposed` with listener failures contained. When called from a
* synchronous `agent/created` listener, removal and disposal wait until
* that creation dispatch unwinds.
*/
enter(agent: Agent, owner: Agent | undefined): () => void
/**
* Announce an agent previously inserted with {@link enter}.
* @param agent - the live inserted agent to announce.
* @throws if `agent` is not the exact live registry entry for its id, or its
* creation announcement already began (including a reentrant call from a
* creation listener).
*/
announce(agent: Agent): void
/**
* Look up a live agent.
* @param id - the shared agent/session id to look up.
* @returns the agent, or undefined when no live agent has that id.
*/
get(id: SessionId): Agent | undefined
/**
* Test whether a live agent was created through one exact parent agent's
* scoped context. Runtime ownership is independent of durable session
* lineage and remains unambiguous when unrelated providers reuse an id.
* @param id - the candidate child agent's shared agent/session id.
* @param owner - the expected runtime creator agent.
* @returns true only while the exact child entry is live under that owner.
*/
isOwnedBy(id: SessionId, owner: Agent): boolean
/**
* All live agents, in registration order.
* @returns a fresh array; mutating it does not affect the registry.
*/
list(): Agent[]
/**
* All live top-level agents in registration order. A top-level agent was
* created without an owning agent context; durable session lineage does not
* affect this runtime relation, so a resumed fork may still be a root.
* @returns a fresh array; mutating it does not affect the registry.
*/
roots(): Agent[]
```
Source: [`packages/core/agent/src/index.ts:253`](../../packages/core/agent/src/index.ts)
<a id="agent-events"></a>
### `agent/*` events
<a id="agentcreated--emit"></a>
#### `agent/created` — emit
A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving seam. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry.
```ts cordis-catalog
/**
* A fully configured agent and live session were published. Setup is
* composition-only; `agent/session-start` is the first startup-driving seam.
* Synchronous listener failure vetoes publication, while returned-promise
* rejection is reported. Detach requested during dispatch waits until every
* creation listener has observed the stable entry.
* @param payload.agent - the newly registered agent with its live session and completed setup.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/created'(this: Scoped<Agent>, payload: { agent: Agent }): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:158`](../../packages/core/agent/src/types.ts)
<a id="agentdisposed--emit"></a>
#### `agent/disposed` — emit
An agent left the registry; AgentLoop emits this after driver quiescence and scoped-registration unwind, but before session detachment. Custom registry users own their driver-ordering contract.
```ts cordis-catalog
/**
* An agent left the registry; AgentLoop emits this after driver quiescence
* and scoped-registration unwind, but before session detachment. Custom
* registry users own their driver-ordering contract.
* @param payload.agent - the exact agent removed from the registry.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/disposed'(this: Scoped<Agent>, payload: { agent: Agent }): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:167`](../../packages/core/agent/src/types.ts)
<a id="agenterror--emit"></a>
#### `agent/error` — emit
A step or turn errored. The machine reports a failure here even when the error has no in-turn position for a durable record.
```ts cordis-catalog
/**
* A step or turn errored. The machine reports a failure here even when
* the error has no in-turn position for a durable record.
* @param payload.agent - the agent whose turn errored.
* @param payload.turn - the turn in which the failure surfaced.
* @param payload.step - the step at which the failure surfaced.
* @param payload.error - the failure, verbatim.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; error: unknown }): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:289`](../../packages/core/agent/src/types.ts)
<a id="agentinboxclaimed--emit"></a>
#### `agent/inbox/claimed` — emit
One message left the inbox inside its open turn. If the proposed step is rejected, the claimed message ends here: it is neither discarded nor re-emitted as a user/message, and the turn closes without a step.
```ts cordis-catalog
/**
* One message left the inbox inside its open turn. If the proposed step
* is rejected, the claimed message ends here: it is neither discarded nor
* re-emitted as a user/message, and the turn closes without a step.
* @param payload.agent - the agent whose inbox changed.
* @param payload.message - the claimed message.
* @param payload.turn - the owning turn.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/claimed'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage; turn: number }): void
```
Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent/src/types.ts:196`](../../packages/core/agent/src/types.ts)
<a id="agentinboxdiscarded--emit"></a>
#### `agent/inbox/discarded` — emit
One message was discarded from the live inbox.
```ts cordis-catalog
/**
* One message was discarded from the live inbox.
* @param payload.agent - the agent whose inbox changed.
* @param payload.message - the discarded message.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/discarded'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage }): void
```
Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/types.ts)
<a id="agentinboxinserted--emit"></a>
#### `agent/inbox/inserted` — emit
One message entered the live inbox.
```ts cordis-catalog
/**
* One message entered the live inbox.
* @param payload.agent - the agent whose inbox changed.
* @param payload.message - the inserted message.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/inbox/inserted'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage }): void
```
Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent/src/types.ts:185`](../../packages/core/agent/src/types.ts)
<a id="agentpre-step--waterfall"></a>
#### `agent/pre-step` — waterfall
Reject a proposed step or replace the messages that enter it. Calling `next()` preserves the current messages.
```ts cordis-catalog
/**
* Reject a proposed step or replace the messages that enter it. Calling
* `next()` preserves the current messages.
* @param payload.agent - the agent proposing the step.
* @param payload.messages - messages removed from the inbox for this step.
* @param payload.turn - the turn that will own the step.
* @param payload.step - the step proposed by the loop.
* @param payload.signal - the current turn's cancellation signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/pre-step'(this: Scoped<Agent>, payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
```
Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent/src/types.ts:230`](../../packages/core/agent/src/types.ts)
<a id="agentrequest--waterfall"></a>
#### `agent/request` — waterfall
Replace the frozen call configuration. `await next()` yields the config the machine would use (agent options on the first request, the logged header afterwards); return a replacement to switch. Model-visible content must use logged channels; this seam cannot mutate messages.
```ts cordis-catalog
/**
* Replace the frozen call configuration. `await next()` yields the config
* the machine would use (agent options on the first request, the logged
* header afterwards); return a replacement to switch. Model-visible
* content must use logged channels; this seam cannot mutate messages.
* @param payload.agent - the agent making the model call.
* @param payload.turn - the open turn number.
* @param payload.step - the step whose request this is.
* @param payload.signal - the current turn's explicit abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/request'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; signal: AbortSignal }, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/types.ts)
<a id="agentrequest-error--waterfall"></a>
#### `agent/request-error` — waterfall
Handle one failed model-request attempt before the loop retries or closes its step. A listener returns `{ kind: 'retry' }` without calling `next()` when it owns recovery, or calls `next()` to delegate. The default `undefined` leaves the failure terminal.
```ts cordis-catalog
/**
* Handle one failed model-request attempt before the loop retries or closes
* its step. A listener returns `{ kind: 'retry' }` without calling `next()`
* when it owns recovery, or calls `next()` to delegate. The default
* `undefined` leaves the failure terminal.
* @param payload.agent - the agent whose request failed.
* @param payload.turn - the turn containing the failed request.
* @param payload.step - the step containing the failed request attempt.
* @param payload.provider - the provider selected for the failed request.
* @param payload.failure - serializable facts normalized at the final adapter boundary.
* @param payload.retryPolicy - the policy of the adapter registration that served the failed request.
* @param payload.signal - the turn abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode waterfall
*/
'agent/request-error'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; step: number; provider: string; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined; signal: AbortSignal }, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
```
Types: [LlmFailure](llm-streaming.md) · [ResolvedRetryPolicy](llm-streaming.md) · [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts)
<a id="agentsession-start--emit"></a>
#### `agent/session-start` — emit
The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts.
```ts cordis-catalog
/**
* The session lifecycle began, once before the first turn. Use
* `agent.inject()` to seed model-facing context. This is a notification, not
* a veto; disposal requested by a lifecycle owner is rechecked before the
* driver starts.
* @param payload.agent - the agent whose session lifecycle began.
* @param payload.source - why the session started (fresh startup, resume, …).
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/session-start'(this: Scoped<Agent>, payload: { agent: Agent; source: SessionStartSource }): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:216`](../../packages/core/agent/src/types.ts)
<a id="agentstatus--emit"></a>
#### `agent/status` — emit
Agent status changed (`idle` ⇄ `running`). A waking delivery enters `running` synchronously after reserving cancellation; `idle` means no driver remains scheduled or active.
```ts cordis-catalog
/**
* Agent status changed (`idle` ⇄ `running`). A waking delivery enters
* `running` synchronously after reserving cancellation; `idle` means no
* driver remains scheduled or active.
* @param payload.agent - the agent whose status flipped.
* @param payload.status - the status just entered (the transition's destination).
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode emit
*/
'agent/status'(this: Scoped<Agent>, payload: { agent: Agent; status: AgentStatus }): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:177`](../../packages/core/agent/src/types.ts)
<a id="agentturn-stopping--serial"></a>
#### `agent/turn-stopping` — serial
The turn is about to close: the model owes no response (no live tool calls, no fresh steering). Awaited before the boundary commits — a listener that objects steers (`agent.steer(...)`) and the machine re-reads its inbox: fresh steering runs another step, none closes the turn. Data decides, so listener order cannot change the outcome. The inverse control (stop a tool loop early) is data too: a tool result carrying `concludesTurn` ends the turn at its step. The conclusion never short-circuits already-submitted next-step work: same-step `additionalContexts` or racing steering still runs, and the turn closes only when that inbox drains.
```ts cordis-catalog
/**
* The turn is about to close: the model owes no response (no live tool
* calls, no fresh steering). Awaited before the boundary commits — a
* listener that objects steers (`agent.steer(...)`) and the machine
* re-reads its inbox: fresh steering runs another step, none closes the
* turn. Data decides, so listener order cannot change the outcome. The
* inverse control (stop a tool loop early) is data too: a tool result
* carrying `concludesTurn` ends the turn at its step. The conclusion
* never short-circuits already-submitted next-step work: same-step
* `additionalContexts` or racing steering still runs, and the turn
* closes only when that inbox drains.
* @param payload.agent - the agent whose turn is at its stop boundary.
* @param payload.turn - the turn about to close.
* @param payload.signal - the current turn's explicit abort signal.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @mode serial
*/
'agent/turn-stopping'(this: Scoped<Agent>, payload: { agent: Agent; turn: number; signal: AbortSignal }): Promise<void> | void
```
Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/types.ts:277`](../../packages/core/agent/src/types.ts)
<a id="agent-loop-events"></a>
### `agent-loop/*` events
<a id="agent-loopconfig-start-failed--emit"></a>
#### `agent-loop/config-start-failed` — emit
A declarative agent entry failed before it could publish a live agent. Consumers that buffer work for the configured identity use this transient signal to reject that work instead of waiting forever. Normal factory teardown suppresses failures from the cancelled startup attempt.
```ts cordis-catalog
/**
* A declarative agent entry failed before it could publish a live agent.
* Consumers that buffer work for the configured identity use this
* transient signal to reject that work instead of waiting forever. Normal
* factory teardown suppresses failures from the cancelled startup attempt.
* @param payload.sessionId - exact shared agent/session identity that failed startup.
* @param payload.error - persistence, setup, or publication failure.
* @mode emit
*/
'agent-loop/config-start-failed'(payload: { sessionId: SessionId; error: unknown }): void
```
Source: [`packages/core/agent-loop/src/index.ts:182`](../../packages/core/agent-loop/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/credentials.md
credentials.md: ef74ddeb4346e18f8d5d33488657e5d50f1d754e
credentials.zh.md: 09cf374a2346fd93aa834e3372321e6eeece6ed8
credentials.md: 0bc2224ac039addc795d3806e8c85004f2bb84a7
credentials.zh.md: 6f5ccd1572885434257d39eb0f659fcc7371ad1d

View File

@@ -48,3 +48,86 @@ interface CredentialInfo {
## Change commits
`credentials/updated (ref)` fires after a committed change to a provider-managed source — a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Consumers do not need the event (they re-resolve per operation); it exists for configuration surfaces refreshing a "configured" badge.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxcredentials--credentials-abstract-seam"></a>
### `ctx.credentials` — `Credentials` (abstract seam)
Abstract credential service. Providers implement the four operations over their source layers; one seam-wide rule binds them all: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret.
```ts cordis-catalog
/**
* Resolve one reference to its current value. Resolution is per call:
* consumers re-resolve at each operation and must not cache across
* operations — that per-operation read is what makes a changed credential
* reach the next operation without a restart.
* @param ref - the reference to resolve.
* @returns the value and its source, or `undefined` while unconfigured.
*/
abstract resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined>
/**
* Describe one reference for configuration surfaces without exposing the
* value.
* @param ref - the reference to describe.
* @returns configured state, supplying source, and writability.
*/
abstract describe(ref: CredentialRef): Promise<CredentialInfo>
/**
* Durably store one value in the provider-managed writable source. Rejects
* while a read-only source shadows the reference — the write would appear
* to succeed while resolution keeps returning the shadowing value — and
* rejects an empty value (use {@link unset}).
* @param ref - the reference to store.
* @param value - the non-empty secret value.
*/
abstract set(ref: CredentialRef, value: string): Promise<void>
/**
* Remove one reference from the provider-managed writable source; removing
* an absent reference is a no-op. Rejects while a read-only source shadows
* the reference, like {@link set}.
* @param ref - the reference to remove.
*/
abstract unset(ref: CredentialRef): Promise<void>
```
Source: [`packages/credentials/credentials/src/index.ts:77`](../../packages/credentials/credentials/src/index.ts)
<a id="credentials-events"></a>
### `credentials/*` events
<a id="credentialsupdated--emit"></a>
#### `credentials/updated` — emit
Committed change to a provider-managed credential source: a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Listener failures are contained and logged — a sync throw and an async rejection alike — without changing the committed operation's outcome, except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
```ts cordis-catalog
/**
* Committed change to a provider-managed credential source: a `set`, an
* `unset`, or an external edit observed in storage. Ambient
* process-environment changes are not observable and never emit. Listener
* failures are contained and logged — a sync throw and an async rejection
* alike — without changing the committed operation's outcome, except
* `INVARIANT`-coded failures, which rethrow after every listener ran;
* that rethrow reaches the emitter only from synchronous listeners, so
* invariant checks on this event must not be async functions.
* @param ref - the reference whose stored value changed.
* @mode emit
*/
'credentials/updated'(ref: CredentialRef): void
```
Source: [`packages/credentials/credentials/src/index.ts:67`](../../packages/credentials/credentials/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -48,3 +48,86 @@ interface CredentialInfo {
## 变更提交
`credentials/updated (ref)` 在 provider 管理的来源发生已提交变更后触发——`set`、`unset` 或在存储中观察到的外部编辑。进程环境自身的变化不可观测,永不发出事件。消费方不需要该事件(它们按操作重新解析);它服务于配置界面刷新「已配置」徽标。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxcredentials--credentials-abstract-seam"></a>
### `ctx.credentials` — `Credentials` (abstract seam)
Abstract credential service. Providers implement the four operations over their source layers; one seam-wide rule binds them all: an empty stored value is absent everywhere — `resolve` skips it, `describe` reports it unconfigured — so a blank never masquerades as a configured secret.
```ts cordis-catalog
/**
* Resolve one reference to its current value. Resolution is per call:
* consumers re-resolve at each operation and must not cache across
* operations — that per-operation read is what makes a changed credential
* reach the next operation without a restart.
* @param ref - the reference to resolve.
* @returns the value and its source, or `undefined` while unconfigured.
*/
abstract resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined>
/**
* Describe one reference for configuration surfaces without exposing the
* value.
* @param ref - the reference to describe.
* @returns configured state, supplying source, and writability.
*/
abstract describe(ref: CredentialRef): Promise<CredentialInfo>
/**
* Durably store one value in the provider-managed writable source. Rejects
* while a read-only source shadows the reference — the write would appear
* to succeed while resolution keeps returning the shadowing value — and
* rejects an empty value (use {@link unset}).
* @param ref - the reference to store.
* @param value - the non-empty secret value.
*/
abstract set(ref: CredentialRef, value: string): Promise<void>
/**
* Remove one reference from the provider-managed writable source; removing
* an absent reference is a no-op. Rejects while a read-only source shadows
* the reference, like {@link set}.
* @param ref - the reference to remove.
*/
abstract unset(ref: CredentialRef): Promise<void>
```
Source: [`packages/credentials/credentials/src/index.ts:77`](../../packages/credentials/credentials/src/index.ts)
<a id="credentials-events"></a>
### `credentials/*` events
<a id="credentialsupdated--emit"></a>
#### `credentials/updated` — emit
Committed change to a provider-managed credential source: a `set`, an `unset`, or an external edit observed in storage. Ambient process-environment changes are not observable and never emit. Listener failures are contained and logged — a sync throw and an async rejection alike — without changing the committed operation's outcome, except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
```ts cordis-catalog
/**
* Committed change to a provider-managed credential source: a `set`, an
* `unset`, or an external edit observed in storage. Ambient
* process-environment changes are not observable and never emit. Listener
* failures are contained and logged — a sync throw and an async rejection
* alike — without changing the committed operation's outcome, except
* `INVARIANT`-coded failures, which rethrow after every listener ran;
* that rethrow reaches the emitter only from synchronous listeners, so
* invariant checks on this event must not be async functions.
* @param ref - the reference whose stored value changed.
* @mode emit
*/
'credentials/updated'(ref: CredentialRef): void
```
Source: [`packages/credentials/credentials/src/index.ts:67`](../../packages/credentials/credentials/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/filesystem.md
filesystem.md: addded9f673ed435e95109d4fb772967514c0b87
filesystem.zh.md: 1e378928ed570b97dbec73b836a7e6ff18726ff8
# pnpm run verify-translation-pairing --write docs/subsystems/filesystem.md
filesystem.md: 1999399254fa5d72ab222a42a2fa8f4b890da50a
filesystem.zh.md: 044a1c5be91ed8bec5d0442cc6f7edfde6870785

View File

@@ -181,7 +181,7 @@ interface FsEditOutcome {
`dsh-fs` owns three events the tool dispatches and the policy plugin listens for, so the emitter (`dsh-tool-fs`) and the listener (`dsh-fs-policy`) share a vocabulary without the emitter depending on the policy plugin. They carry only `dsh-fs` vocabulary plus an opaque `object` actor — no model-facing concepts and no agent/session owner structure.
`fs/write-intent` and `fs/edit-intent` are **single-slot decision waterfalls**: the tool dispatches each with a default thunk returning `undefined` (the bare provider), and a listener fully decides without calling `next()`. The slot is first-wins by registration order — the policy plugin owning it is a deployment convention, not an enforced invariant. `fs/observed` is a fire-and-forget recording event dispatched with a plain `ctx.emit`; its listener MUST be synchronous and side-effect-only, because the tool does NOT guard the emit — a throwing listener would surface as the tool's `isError` result for a mutation that already succeeded. The generated catalog shows the exact signatures on [events.md](../cordis-catalog/events.md).
`fs/write-intent` and `fs/edit-intent` are **single-slot decision waterfalls**: the tool dispatches each with a default thunk returning `undefined` (the bare provider), and a listener fully decides without calling `next()`. The slot is first-wins by registration order — the policy plugin owning it is a deployment convention, not an enforced invariant. `fs/observed` is a fire-and-forget recording event dispatched with a plain `ctx.emit`; its listener MUST be synchronous and side-effect-only, because the tool does NOT guard the emit — a throwing listener would surface as the tool's `isError` result for a mutation that already succeeded. The generated [cordis surface](#cordis-surface) below shows the exact signatures.
## Execution context (policy plugin)
@@ -258,4 +258,208 @@ type FsErrorCode =
## The service and the plugin
`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls (supplying `createIfAbsent`/`replaceIfVersion`/`{ version }` or throwing `FS_NOT_OBSERVED`) and records on `fs/observed`. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated wiring catalog shows the exact `ctx.fs` signatures on [services.md](../cordis-catalog/services.md#ctxfs--filesystem-abstract-seam).
`FileSystem` (`ctx.fs`, abstract) owns the provider primitives: `resolve`, `processPath`, `fileUrl`, `contains`, `stat`, `lstat`, `readText`, `streamText`, `listDir`, `writeText`, and `editText`. `dsh-fs-policy` registers **no service** — it is a plugin that adds policy through the `fs/*` event gate: it decides the write/edit intent waterfalls (supplying `createIfAbsent`/`replaceIfVersion`/`{ version }` or throwing `FS_NOT_OBSERVED`) and records on `fs/observed`. The executor is `dsh-tool-fs`: it reads/writes/edits through `ctx.fs`, dispatches the waterfalls, and emits the recording event. The generated [`ctx.fs` section](#ctxfs--filesystem-abstract-seam) below shows the exact signatures.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxfs--filesystem-abstract-seam"></a>
### `ctx.fs` — `FileSystem` (abstract seam)
Abstract filesystem provider. Targets must preserve identity across aliases; reads expose regular UTF-8 text or typed errors, listings are stable and content-free, and mutations are atomic. Optional guards add stale protection without changing the unguarded provider contract.
```ts cordis-catalog
/**
* Resolve a model/plugin-supplied path into a stable {@link FsTarget}. May perform I/O (a
* remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence
* async even though the local backend only normalizes + realpaths.
*
* @param path - the path to resolve; relative paths resolve against `opts.cwd`.
* @param opts - optional cwd override and cancellation signal.
* @returns the stable target; the same file yields the same `targetKey`.
*/
abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget>
/**
* Return the canonical absolute path a subprocess in this filesystem's
* execution world can open. The path is deliberately separate from
* {@link FsTarget.targetKey}: consumers may pass this value to another OS
* capability, but must continue treating the target key as opaque.
* @param target - the resolved target whose process path is required.
* @returns an absolute path in the backend's execution world.
*/
abstract processPath(target: FsTarget): string
/**
* Return the canonical `file:` URI for a target in this filesystem's
* execution world. Backends own URI encoding because the host platform may
* differ from the execution platform.
* @param target - the resolved target to encode.
* @returns the target's canonical file URI.
*/
abstract fileUrl(target: FsTarget): string
/**
* Test canonical containment without exposing or parsing backend target
* keys. Both targets must come from this provider.
* @param parent - canonical directory target.
* @param child - canonical candidate target.
* @returns true when `child` is `parent` or a descendant of it.
*/
abstract contains(parent: FsTarget, child: FsTarget): boolean
/**
* Return target metadata, or `undefined` when the target does not exist.
* @param target - the resolved target to stat.
* @param signal - aborts the metadata round-trip.
* @returns metadata only, never content; undefined for an absent target.
*/
abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>
/**
* Return path metadata without following the final path component when it is a
* symbolic link. This is intentionally path-shaped, not target-shaped:
* {@link resolve} follows symlinks to produce the stable identity used by
* normal reads/writes, while `lstat` lets a consumer reject the path itself
* before that follow happens.
*
* `opts.cwd` follows {@link resolve}'s cwd rules. `undefined` means the path is
* absent.
* @param path - the path to inspect; relative paths resolve against `opts.cwd`.
* @param opts - `cwd` overrides the backend's default base for relative paths.
* @param signal - aborts the metadata round-trip.
* @returns metadata only, never content; undefined for an absent path.
*/
abstract lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined>
/**
* Read the whole regular text file as a single decoded string.
* @param target - the resolved target to read.
* @param signal - aborts the read.
* @returns the full decoded UTF-8 content.
*/
abstract readText(target: FsTarget, signal?: AbortSignal): Promise<string>
/**
* Stream the whole regular text file as decoded text chunks (same text
* semantics as {@link readText}, for large files). The backend owns
* cross-chunk UTF-8 decoding and binary rejection so the policy layer never
* touches raw bytes.
* @param target - the resolved target to read.
* @param signal - aborts the stream, including between chunks.
* @returns the chunk iterable, decoded and validated like {@link readText}.
*/
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>
/**
* List direct children of a directory in stable name order. Returns resolved
* child targets plus cheap metadata only; never reads file contents.
* @param target - the resolved directory target.
* @param signal - aborts the listing.
* @returns one entry per direct child, in stable name order.
*/
abstract listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>
/**
* Atomically create or replace UTF-8 text. `expected` guards intent and
* staleness; omission allows unconditional overwrite.
* @param target - the resolved target to write.
* @param content - the full new file content.
* @param expected - the write intent guarding the write; omit for unconditional.
* @param signal - aborts before the atomic rename takes effect.
* @param sandboxPolicy - the per-call mode and workspace root this write
* runs under; a sandboxing backend fences the write by it, the bare backend
* ignores it. Omit to leave the backend its own default.
* @returns the outcome, including the version the write produced.
*/
abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise<FsWriteOutcome>
/**
* Atomically edit literal text. When supplied, the version guard is checked
* before matching so stale content reports `FS_STALE_VERSION`; omission edits
* the current content without a freshness precondition.
* @param target - the resolved target to edit.
* @param edit - the literal search/replace request.
* @param expected - the version guard; omit for an unconditional edit.
* @param signal - aborts before the atomic rename takes effect.
* @param sandboxPolicy - the per-call mode and workspace root this edit runs
* under; a sandboxing backend fences the edit by it, the bare backend
* ignores it. Omit to leave the backend its own default.
* @returns the outcome, including the version the edit produced.
*/
abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise<FsEditOutcome>
```
Types: [SandboxExecutionPolicy](sandbox.md)
Source: [`packages/fs/fs/src/index.ts:83`](../../packages/fs/fs/src/index.ts)
<a id="fs-events"></a>
### `fs/*` events
<a id="fsedit-intent--waterfall"></a>
#### `fs/edit-intent` — waterfall
Single-slot decision for the next FileSystem.editText. Calling `next()` yields an unconditional edit; the first returned guard wins.
```ts cordis-catalog
/**
* Single-slot decision for the next {@link FileSystem.editText}. Calling
* `next()` yields an unconditional edit; the first returned guard wins.
* @param target - the resolved target about to be edited.
* @param actor - the opaque tool-execution context the decider keys off.
* @mode waterfall
*/
'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>
```
Source: [`packages/fs/fs/src/index.ts:64`](../../packages/fs/fs/src/index.ts)
<a id="fsobserved--emit"></a>
#### `fs/observed` — emit
Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited.
```ts cordis-catalog
/**
* Record a successful observation. Listeners must be synchronous recorders:
* throws fail the tool call and returned promises are not awaited.
* @param target - the target that was read/written/edited.
* @param version - the version the actor now holds as its observation.
* @param actor - the observing tool-execution context; undefined records nothing useful.
* @mode emit
*/
'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void
```
Source: [`packages/fs/fs/src/index.ts:73`](../../packages/fs/fs/src/index.ts)
<a id="fswrite-intent--waterfall"></a>
#### `fs/write-intent` — waterfall
Single-slot decision for the next FileSystem.writeText. Calling `next()` yields the bare provider's unconditional write; the first listener that returns an intent owns the decision rather than composing with peers.
```ts cordis-catalog
/**
* Single-slot decision for the next {@link FileSystem.writeText}. Calling
* `next()` yields the bare provider's unconditional write; the first listener
* that returns an intent owns the decision rather than composing with peers.
* @param target - the resolved target about to be written.
* @param actor - the opaque tool-execution context the decider keys off.
* @mode waterfall
*/
'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>
```
Source: [`packages/fs/fs/src/index.ts:56`](../../packages/fs/fs/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -181,7 +181,7 @@ interface FsEditOutcome {
`dsh-fs` 拥有三个事件,由工具分发、策略插件监听,使发射方(`dsh-tool-fs`)与监听方(`dsh-fs-policy`)共享词汇,而发射方无需依赖策略插件。它们只携带 `dsh-fs` 词汇加一个不透明的 `object` actor不含面向模型的概念也不含 agent/会话所有者结构。
`fs/write-intent` 与 `fs/edit-intent` 是**单槽决策 waterfall**:工具分发时附带一个默认 thunk返回 `undefined`,即裸提供方),监听方完全决策而不调用 `next()`。该槽按注册顺序先到先得——由策略插件占据是部署约定,而非强制不变式。`fs/observed` 是一个即发即弃的记录事件,通过普通 `ctx.emit` 分发;其监听方必须是同步的、仅产生副作用,因为工具不会捕获该 emit 抛出的异常——抛出异常的监听方会导致工具为一次已经成功的变更返回 `isError` 结果。生成的目录在 [events.md](../cordis-catalog/events.md) 展示确切签名。
`fs/write-intent` 与 `fs/edit-intent` 是**单槽决策 waterfall**:工具分发时附带一个默认 thunk返回 `undefined`,即裸提供方),监听方完全决策而不调用 `next()`。该槽按注册顺序先到先得——由策略插件占据是部署约定,而非强制不变式。`fs/observed` 是一个即发即弃的记录事件,通过普通 `ctx.emit` 分发;其监听方必须是同步的、仅产生副作用,因为工具不会捕获该 emit 抛出的异常——抛出异常的监听方会导致工具为一次已经成功的变更返回 `isError` 结果。下方生成的 [cordis surface](#cordis-surface) 展示确切签名。
## 执行上下文(策略插件)
@@ -258,4 +258,208 @@ type FsErrorCode =
## 服务与插件
`FileSystem``ctx.fs`abstract拥有提供方原语`resolve`、`processPath`、`fileUrl`、`contains`、`stat`、`lstat`、`readText`、`streamText`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:对写入/编辑意图 waterfall 作出决策(提供 `createIfAbsent`/`replaceIfVersion`/`{ version }`,或抛出 `FS_NOT_OBSERVED`),并在 `fs/observed` 上记录。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall并 emit 记录事件。生成的 wiring 目录在 [services.md](../cordis-catalog/services.md#ctxfs--filesystem-abstract-seam) 展示确切的 `ctx.fs` 签名。
`FileSystem``ctx.fs`abstract拥有提供方原语`resolve`、`processPath`、`fileUrl`、`contains`、`stat`、`lstat`、`readText`、`streamText`、`listDir`、`writeText` 与 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门禁添加策略的插件:对写入/编辑意图 waterfall 作出决策(提供 `createIfAbsent`/`replaceIfVersion`/`{ version }`,或抛出 `FS_NOT_OBSERVED`),并在 `fs/observed` 上记录。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall并 emit 记录事件。下方生成的 [`ctx.fs` 小节](#ctxfs--filesystem-abstract-seam) 展示确切的 `ctx.fs` 签名。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxfs--filesystem-abstract-seam"></a>
### `ctx.fs` — `FileSystem` (abstract seam)
Abstract filesystem provider. Targets must preserve identity across aliases; reads expose regular UTF-8 text or typed errors, listings are stable and content-free, and mutations are atomic. Optional guards add stale protection without changing the unguarded provider contract.
```ts cordis-catalog
/**
* Resolve a model/plugin-supplied path into a stable {@link FsTarget}. May perform I/O (a
* remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence
* async even though the local backend only normalizes + realpaths.
*
* @param path - the path to resolve; relative paths resolve against `opts.cwd`.
* @param opts - optional cwd override and cancellation signal.
* @returns the stable target; the same file yields the same `targetKey`.
*/
abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget>
/**
* Return the canonical absolute path a subprocess in this filesystem's
* execution world can open. The path is deliberately separate from
* {@link FsTarget.targetKey}: consumers may pass this value to another OS
* capability, but must continue treating the target key as opaque.
* @param target - the resolved target whose process path is required.
* @returns an absolute path in the backend's execution world.
*/
abstract processPath(target: FsTarget): string
/**
* Return the canonical `file:` URI for a target in this filesystem's
* execution world. Backends own URI encoding because the host platform may
* differ from the execution platform.
* @param target - the resolved target to encode.
* @returns the target's canonical file URI.
*/
abstract fileUrl(target: FsTarget): string
/**
* Test canonical containment without exposing or parsing backend target
* keys. Both targets must come from this provider.
* @param parent - canonical directory target.
* @param child - canonical candidate target.
* @returns true when `child` is `parent` or a descendant of it.
*/
abstract contains(parent: FsTarget, child: FsTarget): boolean
/**
* Return target metadata, or `undefined` when the target does not exist.
* @param target - the resolved target to stat.
* @param signal - aborts the metadata round-trip.
* @returns metadata only, never content; undefined for an absent target.
*/
abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>
/**
* Return path metadata without following the final path component when it is a
* symbolic link. This is intentionally path-shaped, not target-shaped:
* {@link resolve} follows symlinks to produce the stable identity used by
* normal reads/writes, while `lstat` lets a consumer reject the path itself
* before that follow happens.
*
* `opts.cwd` follows {@link resolve}'s cwd rules. `undefined` means the path is
* absent.
* @param path - the path to inspect; relative paths resolve against `opts.cwd`.
* @param opts - `cwd` overrides the backend's default base for relative paths.
* @param signal - aborts the metadata round-trip.
* @returns metadata only, never content; undefined for an absent path.
*/
abstract lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined>
/**
* Read the whole regular text file as a single decoded string.
* @param target - the resolved target to read.
* @param signal - aborts the read.
* @returns the full decoded UTF-8 content.
*/
abstract readText(target: FsTarget, signal?: AbortSignal): Promise<string>
/**
* Stream the whole regular text file as decoded text chunks (same text
* semantics as {@link readText}, for large files). The backend owns
* cross-chunk UTF-8 decoding and binary rejection so the policy layer never
* touches raw bytes.
* @param target - the resolved target to read.
* @param signal - aborts the stream, including between chunks.
* @returns the chunk iterable, decoded and validated like {@link readText}.
*/
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>
/**
* List direct children of a directory in stable name order. Returns resolved
* child targets plus cheap metadata only; never reads file contents.
* @param target - the resolved directory target.
* @param signal - aborts the listing.
* @returns one entry per direct child, in stable name order.
*/
abstract listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>
/**
* Atomically create or replace UTF-8 text. `expected` guards intent and
* staleness; omission allows unconditional overwrite.
* @param target - the resolved target to write.
* @param content - the full new file content.
* @param expected - the write intent guarding the write; omit for unconditional.
* @param signal - aborts before the atomic rename takes effect.
* @param sandboxPolicy - the per-call mode and workspace root this write
* runs under; a sandboxing backend fences the write by it, the bare backend
* ignores it. Omit to leave the backend its own default.
* @returns the outcome, including the version the write produced.
*/
abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise<FsWriteOutcome>
/**
* Atomically edit literal text. When supplied, the version guard is checked
* before matching so stale content reports `FS_STALE_VERSION`; omission edits
* the current content without a freshness precondition.
* @param target - the resolved target to edit.
* @param edit - the literal search/replace request.
* @param expected - the version guard; omit for an unconditional edit.
* @param signal - aborts before the atomic rename takes effect.
* @param sandboxPolicy - the per-call mode and workspace root this edit runs
* under; a sandboxing backend fences the edit by it, the bare backend
* ignores it. Omit to leave the backend its own default.
* @returns the outcome, including the version the edit produced.
*/
abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise<FsEditOutcome>
```
Types: [SandboxExecutionPolicy](sandbox.md)
Source: [`packages/fs/fs/src/index.ts:83`](../../packages/fs/fs/src/index.ts)
<a id="fs-events"></a>
### `fs/*` events
<a id="fsedit-intent--waterfall"></a>
#### `fs/edit-intent` — waterfall
Single-slot decision for the next FileSystem.editText. Calling `next()` yields an unconditional edit; the first returned guard wins.
```ts cordis-catalog
/**
* Single-slot decision for the next {@link FileSystem.editText}. Calling
* `next()` yields an unconditional edit; the first returned guard wins.
* @param target - the resolved target about to be edited.
* @param actor - the opaque tool-execution context the decider keys off.
* @mode waterfall
*/
'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>
```
Source: [`packages/fs/fs/src/index.ts:64`](../../packages/fs/fs/src/index.ts)
<a id="fsobserved--emit"></a>
#### `fs/observed` — emit
Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited.
```ts cordis-catalog
/**
* Record a successful observation. Listeners must be synchronous recorders:
* throws fail the tool call and returned promises are not awaited.
* @param target - the target that was read/written/edited.
* @param version - the version the actor now holds as its observation.
* @param actor - the observing tool-execution context; undefined records nothing useful.
* @mode emit
*/
'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void
```
Source: [`packages/fs/fs/src/index.ts:73`](../../packages/fs/fs/src/index.ts)
<a id="fswrite-intent--waterfall"></a>
#### `fs/write-intent` — waterfall
Single-slot decision for the next FileSystem.writeText. Calling `next()` yields the bare provider's unconditional write; the first listener that returns an intent owns the decision rather than composing with peers.
```ts cordis-catalog
/**
* Single-slot decision for the next {@link FileSystem.writeText}. Calling
* `next()` yields the bare provider's unconditional write; the first listener
* that returns an intent owns the decision rather than composing with peers.
* @param target - the resolved target about to be written.
* @param actor - the opaque tool-execution context the decider keys off.
* @mode waterfall
*/
'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>
```
Source: [`packages/fs/fs/src/index.ts:56`](../../packages/fs/fs/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/goal.md
goal.md: fc6a7e63e58fc7cd4bc524be1e66515593680d95
goal.zh.md: c584cb375bd55c13c158e7ee22d7721040dffa05
# pnpm run verify-translation-pairing --write docs/subsystems/goal.md
goal.md: 6f54a5261cb44c3fda389e37cb689a00061ab241
goal.zh.md: 043263f3231b4c727f21ca0b8fc07d8adffa0559

View File

@@ -143,3 +143,135 @@ interface GoalChanged {
## Service behavior
[`GoalService`](../../packages/goal/goal/src/index.ts) resolves creation defaults, folds strict replay from durable `goal/change` events, enforces exact-live-agent identity and compare-and-set mutations, and emits contained `goal/changed` notifications. The package [README](../../packages/goal/goal/README.md) owns the callable and model-visible contract.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxgoals--goalservice"></a>
### `ctx.goals` — `GoalService`
Goal service (`ctx.goals`) backed exclusively by the owning session log.
```ts cordis-catalog
/**
* Read the current goal for one exact live agent.
* @param agent - owning live agent.
* @returns a fresh view or `undefined` when no goal is current.
* @throws {@link GoalError} when the agent is not the registry's live instance.
*/
get(agent: Agent): GoalView | undefined
/**
* Remove process-local continuation authority without changing durable goal
* phase or revision. Lifecycle owners use this before unloading a driver;
* a later human-authorized {@link resume} records the new activation edge.
* @param agent - owning live agent.
* @returns a fresh disarmed view, or `undefined` when no goal is current.
*/
disarm(agent: Agent): GoalView | undefined
/**
* Create and arm a goal. A completed goal may be replaced; every other
* current phase must be cleared or resumed instead.
* @param agent - owning live agent.
* @param request - objective and optional round cap.
* @returns the created live view.
*/
create(agent: Agent, request: CreateGoalRequest): GoalView
/**
* Edit objective and/or round cap without changing phase.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @param request - at least one replacement field.
* @returns the edited view.
*/
@Remote('edit') edit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView
/**
* Pause an active goal and disarm automatic continuation.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @returns the paused view.
*/
@Remote('pause') pause(agent: Agent, ref: GoalRef): GoalView
/**
* Resume and arm a stopped goal, or rearm an active goal after a
* session-start edge, while its round budget still has capacity.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @returns the active view.
*/
@Remote('resume') resume(agent: Agent, ref: GoalRef): GoalView
/**
* Mark a current non-complete goal complete and disarm it.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @returns the completed view.
*/
@Remote('complete') complete(agent: Agent, ref: GoalRef): GoalView
/**
* Mark an active goal blocked and disarm it.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @param reason - policy-owned stable code and human-readable explanation.
* @returns the blocked view with its durable reason.
*/
block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView
/**
* Clear the current goal while retaining a durable tombstone and history.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @returns the tombstone ref whose revision is one past the cleared snapshot.
*/
@Remote('clear') clear(agent: Agent, ref: GoalRef): GoalRef
/**
* Create one Goal through the remote boundary.
* @param agent - exact live Agent resolved from the wire identity.
* @param request - objective and optional round cap.
* @returns the created Goal identity.
*/
@Remote('create') remoteExportCreate(agent: Agent, request: CreateGoalRequest): CreateGoalResult
```
Types: [Agent](core.md)
Source: [`packages/goal/goal/src/index.ts:183`](../../packages/goal/goal/src/index.ts)
<a id="goal-events"></a>
### `goal/*` events
<a id="goalchanged--emit"></a>
#### `goal/changed` — emit
Goal mutation accepted by one live agent. The matching `goal/change` session event has already committed. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
```ts cordis-catalog
/**
* Goal mutation accepted by one live agent. The matching `goal/change`
* session event has already committed. Listener failures are contained.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @param payload.agent - agent whose session owns the goal.
* @param payload.change - fresh current projection or clear tombstone.
* @mode emit
*/
'goal/changed'(this: import('@deepseek-ai/dsh-scope').Scoped<Agent>, payload: { agent: Agent; change: GoalChanged }): void
```
Types: [Agent](core.md) · [Scoped](scope.md)
Source: [`packages/goal/goal/src/domain.ts:114`](../../packages/goal/goal/src/domain.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -143,3 +143,135 @@ interface GoalChanged {
## 服务行为
[`GoalService`](../../packages/goal/goal/src/index.ts) 解析创建默认值、从持久 `goal/change` 事件执行严格回放折叠、校验确切的活跃 agent 身份、以比较并设置方式执行变更,并发出 `goal/changed` 通知;监听器故障会被隔离。包 [README](../../packages/goal/goal/README.md) 负责记录可调用契约和面向模型的契约。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxgoals--goalservice"></a>
### `ctx.goals` — `GoalService`
Goal service (`ctx.goals`) backed exclusively by the owning session log.
```ts cordis-catalog
/**
* Read the current goal for one exact live agent.
* @param agent - owning live agent.
* @returns a fresh view or `undefined` when no goal is current.
* @throws {@link GoalError} when the agent is not the registry's live instance.
*/
get(agent: Agent): GoalView | undefined
/**
* Remove process-local continuation authority without changing durable goal
* phase or revision. Lifecycle owners use this before unloading a driver;
* a later human-authorized {@link resume} records the new activation edge.
* @param agent - owning live agent.
* @returns a fresh disarmed view, or `undefined` when no goal is current.
*/
disarm(agent: Agent): GoalView | undefined
/**
* Create and arm a goal. A completed goal may be replaced; every other
* current phase must be cleared or resumed instead.
* @param agent - owning live agent.
* @param request - objective and optional round cap.
* @returns the created live view.
*/
create(agent: Agent, request: CreateGoalRequest): GoalView
/**
* Edit objective and/or round cap without changing phase.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @param request - at least one replacement field.
* @returns the edited view.
*/
@Remote('edit') edit(agent: Agent, ref: GoalRef, request: EditGoalRequest): GoalView
/**
* Pause an active goal and disarm automatic continuation.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @returns the paused view.
*/
@Remote('pause') pause(agent: Agent, ref: GoalRef): GoalView
/**
* Resume and arm a stopped goal, or rearm an active goal after a
* session-start edge, while its round budget still has capacity.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @returns the active view.
*/
@Remote('resume') resume(agent: Agent, ref: GoalRef): GoalView
/**
* Mark a current non-complete goal complete and disarm it.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @returns the completed view.
*/
@Remote('complete') complete(agent: Agent, ref: GoalRef): GoalView
/**
* Mark an active goal blocked and disarm it.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @param reason - policy-owned stable code and human-readable explanation.
* @returns the blocked view with its durable reason.
*/
block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView
/**
* Clear the current goal while retaining a durable tombstone and history.
* @param agent - owning live agent.
* @param ref - expected current revision.
* @returns the tombstone ref whose revision is one past the cleared snapshot.
*/
@Remote('clear') clear(agent: Agent, ref: GoalRef): GoalRef
/**
* Create one Goal through the remote boundary.
* @param agent - exact live Agent resolved from the wire identity.
* @param request - objective and optional round cap.
* @returns the created Goal identity.
*/
@Remote('create') remoteExportCreate(agent: Agent, request: CreateGoalRequest): CreateGoalResult
```
Types: [Agent](core.md)
Source: [`packages/goal/goal/src/index.ts:183`](../../packages/goal/goal/src/index.ts)
<a id="goal-events"></a>
### `goal/*` events
<a id="goalchanged--emit"></a>
#### `goal/changed` — emit
Goal mutation accepted by one live agent. The matching `goal/change` session event has already committed. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
```ts cordis-catalog
/**
* Goal mutation accepted by one live agent. The matching `goal/change`
* session event has already committed. Listener failures are contained.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
* @param payload.agent - agent whose session owns the goal.
* @param payload.change - fresh current projection or clear tombstone.
* @mode emit
*/
'goal/changed'(this: import('@deepseek-ai/dsh-scope').Scoped<Agent>, payload: { agent: Agent; change: GoalChanged }): void
```
Types: [Agent](core.md) · [Scoped](scope.md)
Source: [`packages/goal/goal/src/domain.ts:114`](../../packages/goal/goal/src/domain.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/http-server.md
http-server.md: 200d3eed683f203b65824ea3f62cf56ce3df912f
http-server.zh.md: 9a12d3769a93270810927075be5f236875688b91
http-server.md: b9795fe98b432b6ef5f7d01a4d3e115c809fe642
http-server.zh.md: c3ba02dce5c58f491921f1fb31f999f9da808bc5

View File

@@ -45,3 +45,64 @@ interface Config {
`HttpServerService` (`ctx.httpServer`) listens immediately on activation; a listen failure (EADDRINUSE…) throws out of init — a FAILED fiber the boot's fail-loud sweep reports. `register(route)` adds one named route and returns its disposer; a duplicate `(kind, path)` throws, because route patterns are a composition-level contract and a collision is a misconfiguration. `tapIndex(transform)` adds a pure html-to-html transform applied to every index response — `/` and each SPA fallback — in registration order; [dsh-client-modules](../../packages/client/modules) uses it to inject the boot manifest. `port` reads the listening port, the OS-assigned value when `config.port` is 0.
A request whose handling throws (a malformed %-escape hitting `decodeURIComponent`, a client dropping mid-body) is logged as a warning and answered 400 — or the socket destroyed when headers are already out — never a process exit. Disposal pairs `close()` with `closeAllConnections()` because a handler may hold its response open (SSE) and such connections never end on their own; without the force-close, teardown would hang. The package never prints: the URL line belongs to the shell. Per-package operational detail, including the dev-mode bundle watch pipeline, stays in the [README](../../packages/host/webserver/README.md).
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxhttpserver--httpserverservice"></a>
### `ctx.httpServer` — `HttpServerService`
The web-shape HTTP carrier service. Activation listens immediately (route registration order carries no request-facing semantics: named routes are composed to be disjoint, and the fallback seat answers anything not yet claimed during the boot window — 404 until its owner registers). A listen failure throws out of init — a FAILED fiber the boot's fail-loud sweep reports.
```ts cordis-catalog
/**
* Register a named route. Duplicate (kind, path) throws — route patterns are
* a composition-level contract, so a collision is a misconfiguration.
* @param route - kind, path, and the owning handler.
* @returns the disposer removing the route.
*/
register(route: WebRoute): () => void
/**
* Register an exact-path HTTP upgrade route. Duplicate paths throw because
* one socket can have only one protocol owner.
* @param route - pathname and handler owning negotiation plus socket use.
* @returns the disposer removing the route.
*/
registerUpgrade(route: WebUpgradeRoute): () => void
/**
* Claim the fallback seat: the handler answering every request no named
* route matches (the SPA dist server in the shipped Web composition). One
* owner only — a second registration throws, because two fallbacks cannot
* compose.
* @param handler - owns the full response lifecycle of unmatched requests.
* @returns the disposer releasing the seat.
*/
registerFallback(handler: WebRoute['handler']): () => void
/**
* Register an index.html transform, applied by the fallback owner to every
* index response ({@link applyIndexTaps}) in registration order.
* @param transform - pure html-to-html function.
* @returns the disposer removing the transform.
*/
tapIndex(transform: (html: string) => string): () => void
/**
* Run an index.html body through the registered taps in registration order
* — called by the fallback owner on every index response it renders.
* @param html - the raw index.html body.
* @returns the transformed body.
*/
applyIndexTaps(html: string): string
```
Source: [`packages/host/webserver/src/index.ts:60`](../../packages/host/webserver/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -45,3 +45,64 @@ interface Config {
`HttpServerService``ctx.httpServer`在激活时立即监听监听失败EADDRINUSE 等)会从 init 抛出,形成一个 FAILED fiber由启动的大声失败 sweep 上报。`register(route)` 添加一条具名路由并返回其 disposer重复的 `(kind, path)` 抛出异常,因为路由模式是组合层契约,冲突即配置错误。`tapIndex(transform)` 添加一个纯的 html 到 html 转换,按注册顺序应用于每个 index 响应(`/` 和每次 SPA 回退);[dsh-client-modules](../../packages/client/modules) 用它注入启动 manifest元数据清单。`port` 读取监听端口,`config.port` 为 0 时读到的是操作系统分配的值。
处理过程中抛出异常的请求(畸形的 % 转义撞上 `decodeURIComponent`、客户端在请求体中途断开)会记录为警告并应答 400响应头已发出时则销毁 socket绝不导致进程退出。dispose资源释放把 `close()` 与 `closeAllConnections()` 配对使用,因为处理器可能像 SSEServer-Sent Events那样保持响应打开而这类连接永远不会自行结束没有强制关闭拆卸就会挂起。该包package从不打印输出URL 行归 shell 所有。逐包运维细节(含开发模式的 bundle 监视流水线)留在 [README](../../packages/host/webserver/README.md) 中。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxhttpserver--httpserverservice"></a>
### `ctx.httpServer` — `HttpServerService`
The web-shape HTTP carrier service. Activation listens immediately (route registration order carries no request-facing semantics: named routes are composed to be disjoint, and the fallback seat answers anything not yet claimed during the boot window — 404 until its owner registers). A listen failure throws out of init — a FAILED fiber the boot's fail-loud sweep reports.
```ts cordis-catalog
/**
* Register a named route. Duplicate (kind, path) throws — route patterns are
* a composition-level contract, so a collision is a misconfiguration.
* @param route - kind, path, and the owning handler.
* @returns the disposer removing the route.
*/
register(route: WebRoute): () => void
/**
* Register an exact-path HTTP upgrade route. Duplicate paths throw because
* one socket can have only one protocol owner.
* @param route - pathname and handler owning negotiation plus socket use.
* @returns the disposer removing the route.
*/
registerUpgrade(route: WebUpgradeRoute): () => void
/**
* Claim the fallback seat: the handler answering every request no named
* route matches (the SPA dist server in the shipped Web composition). One
* owner only — a second registration throws, because two fallbacks cannot
* compose.
* @param handler - owns the full response lifecycle of unmatched requests.
* @returns the disposer releasing the seat.
*/
registerFallback(handler: WebRoute['handler']): () => void
/**
* Register an index.html transform, applied by the fallback owner to every
* index response ({@link applyIndexTaps}) in registration order.
* @param transform - pure html-to-html function.
* @returns the disposer removing the transform.
*/
tapIndex(transform: (html: string) => string): () => void
/**
* Run an index.html body through the registered taps in registration order
* — called by the fallback owner on every index response it renders.
* @param html - the raw index.html body.
* @returns the transformed body.
*/
applyIndexTaps(html: string): string
```
Source: [`packages/host/webserver/src/index.ts:60`](../../packages/host/webserver/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/invariants.md
invariants.md: 143a7feabbab18556f595a78c7aef17caa6d3028
invariants.zh.md: fb497857c02202544edf44ccfe3a2935a988e30e
invariants.md: 184d982bb27ab3ff98f715f0751c8bfcc1d08bea
invariants.zh.md: bd2f83a018642832fe50ba5cca6dd82e2bd7d59b

View File

@@ -57,3 +57,32 @@ An enabled installer runs in a dedicated child Cordis fiber; `installer.inject`
## The companion contract
Every workspace package owns a `./invariant` companion ([package contract](../../packages/AGENTS.md)); publication and registration are exhaustive, but assertions are deliberately not synthetic. A companion installs a check only when its package owns an observable event or mutable-data relationship; otherwise it exports an empty installer whose leading comment starts `No runtime invariant:` and explains, package-specifically, why nothing is checkable. `pnpm run verify-package-invariants` mechanically rejects generated markers, unexplained empty installers, non-empty installers that omit or ignore the reporter, incorrect registration names, and incomplete export, publication, dependency, or bundle wiring ([mechanical-rule Agent Note](../../.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md)). The catalog of executable companions and the standard composition live in the [package README](../../packages/support/invariants/README.md).
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxinvariants--invariantservice"></a>
### `ctx.invariants` — `InvariantService`
Package-owned invariant registry with global and regex-based selection.
```ts cordis-catalog
/**
* Register one package's invariant installer. The package name is reserved
* even when filtering disables its checks. Enabled installers run in a child
* fiber; failure disposes that fiber and releases the reservation.
* @param packageName - full npm package name that owns the contribution.
* @param installer - listener or startup-check installer for the child context.
* @returns an effect-scoped disposer for the registration.
*/
register(packageName: string, installer: InvariantInstaller): () => void
```
Source: [`packages/support/invariants/src/index.ts:94`](../../packages/support/invariants/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -57,3 +57,32 @@ interface InvariantInstaller {
## 配套插件契约
每个工作区包都拥有一个 `./invariant` 配套插件([包契约](../../packages/AGENTS.md));发布与注册是穷尽式的,但刻意不合成断言。只有当包对某个可观察事件或可变数据拥有所有权关系时,配套插件才安装检查;否则它导出一个空安装器,其起始注释以 `No runtime invariant:` 开头,针对该包具体解释为什么没有可检查项。`pnpm run verify-package-invariants` 机械地拒绝「生成文件」标记、无解释的空安装器、遗漏或忽略报告器的非空安装器、错误的注册名称,以及不完整的导出、发布、依赖或打包接线([机械规则 Agent Note](../../.agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md))。可执行配套插件的目录与标准组合方式见[包 README](../../packages/support/invariants/README.md)。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxinvariants--invariantservice"></a>
### `ctx.invariants` — `InvariantService`
Package-owned invariant registry with global and regex-based selection.
```ts cordis-catalog
/**
* Register one package's invariant installer. The package name is reserved
* even when filtering disables its checks. Enabled installers run in a child
* fiber; failure disposes that fiber and releases the reservation.
* @param packageName - full npm package name that owns the contribution.
* @param installer - listener or startup-check installer for the child context.
* @returns an effect-scoped disposer for the registration.
*/
register(packageName: string, installer: InvariantInstaller): () => void
```
Source: [`packages/support/invariants/src/index.ts:94`](../../packages/support/invariants/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/llm-streaming.md
llm-streaming.md: 5c90b3ce4ac65a99997f6ba7ad5deac494b7f772
llm-streaming.zh.md: 7fd0043234cb40d6b21cec6ff101993164785a6e
llm-streaming.md: fe4a831660dd81b33a1cd7dc9a9d7026f3831991
llm-streaming.zh.md: a10b696ec0887e139785fc7e95d8641e6496de79

View File

@@ -255,3 +255,191 @@ interface ContentBlockMap {
```
See [core.md § Content blocks and messages](core.md#content-blocks-and-messages) for the block interfaces.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxllm--llmservice"></a>
### `ctx.llm` — `LlmService`
The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall.
```ts cordis-catalog
/**
* Register an adapter for the given provider routes. Throws `LlmError` with code
* `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
* Disposed with the fiber.
* @param providers - every provider route this adapter should serve.
* @param adapter - the adapter that streams calls for those providers.
* @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
*/
registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle
/**
* Describe provider routes with a registered adapter.
* @returns detached provider metadata in registration order.
*/
listProviders(): LlmProviderInfo[]
/**
* Declare provider routes an adapter plugin can activate through
* configuration. Registration is all-or-nothing: an empty list, invalid
* entry, or a provider already declared by any registration throws
* `LlmError` without registering the rest. Disposed with the fiber.
* @param entries - every configurable provider this plugin owns.
* @returns a handle that withdraws all of them, and can atomically replace them.
*/
registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle
/**
* List every declared configurable provider, registered or dormant.
* @returns detached directory entries in declaration order.
*/
listConfigurableProviders(): LlmConfigurableProvider[]
/**
* Offer to interrogate provider endpoints on behalf of the settings
* namespace this plugin owns. The namespace is the key because that is what
* a configuration surface already holds from the configurable-provider
* directory, and because a provider being *added* has no route to name yet.
* Disposed with the fiber.
* @param settingsNs - the namespace whose profiles this discovery serves.
* @param discover - interrogates one endpoint; must honor `request.signal`.
* @returns the disposer that withdraws the offer.
*/
registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise<readonly LlmDiscoveredModel[]>, ): () => void
/**
* Interrogate one provider endpoint for the models it advertises. The
* request describes a draft, not a stored route, so nothing here reads or
* writes settings or credentials — the caller owns both, and the reply is
* candidate metadata a surface may offer for adoption.
* @param settingsNs - namespace whose registered discovery serves this draft.
* @param request - the endpoint, protocol, and one-shot credential to use.
* @returns the advertised models, deduplicated in endpoint order.
*/
async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, ): Promise<LlmDiscoveredModel[]>
/**
* Resolve the retry policy captured when one provider route was registered.
* @param provider - registered provider route to inspect.
* @returns the provider-owned policy, with normal defaults already resolved.
*/
providerRetryPolicy(provider: string): ResolvedRetryPolicy
/**
* Discover models advertised by one registered provider. Catalog membership
* is advisory and never changes routing or request validation.
* @param provider - registered provider route to inspect.
* @returns detached model metadata in adapter-preferred order.
*/
async listModels(provider: string): Promise<LlmModelInfo[]>
/**
* Resolve and validate all metadata from the adapter that owns one exact
* route. The result is detached from adapter-owned objects; catalog
* membership remains advisory and does not control request routing.
* @param provider - registered provider route to inspect.
* @param model - exact model id passed to the adapter.
* @param signal - optional cancellation for adapter-owned asynchronous lookup.
* @returns exact model identity plus available context and reasoning metadata.
*/
async resolveModelInfo( provider: string, model: string, signal?: AbortSignal, ): Promise<LlmResolvedModelInfo>
/**
* Validate a conversation call config against its exact model capability and
* materialize adapter-configured defaults. Unsupported explicit efforts
* reject before provider I/O; no clamping or aliasing is performed. This
* standalone query does not bind a later dispatch; use {@link prepareCall}
* when logging and streaming must share one adapter registration.
* @param config - provider/model route and optional request controls.
* @param signal - optional cancellation for adapter-owned capability lookup.
* @returns a detached config only when a default must be materialized.
*/
async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>
/**
* Resolve one call under its current adapter registration. The returned
* one-shot handle keeps that registration across header logging and dispatch,
* so HMR cannot combine one adapter's capability result with another adapter.
* @param config - provider/model route and optional request controls.
* @param signal - optional cancellation for adapter-owned capability lookup.
* @returns a prepared config and its registration-bound stream entry point.
*/
async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>
/**
* Stream one model call as raw chunks (token-level deltas). Replay state is
* retained only when the same adapter instance owns its historical provider
* and the target provider. Final adapter selection remains fixed through
* asynchronous exact-model resolution and dispatch. Adapter selection,
* dispatch, and iteration failures become terminal `error` or `aborted`
* finish chunks; middleware, nested-call, cleanup, and consumer failures
* remain thrown.
* @param options - the full request; `options.provider` selects the adapter.
* @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
*/
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
```
Types: [AdapterRegistrationHandle](core.md) · [DirectoryRegistrationHandle](core.md) · [GenerateOptions](core.md) · [LlmCallConfig](core.md) · [LlmConfigurableProvider](core.md) · [LlmDiscoveredModel](core.md) · [LlmModelDiscoveryRequest](core.md) · [LlmModelInfo](core.md) · [LlmProviderInfo](core.md) · [LlmResolvedModelInfo](core.md)
Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts)
<a id="llm-events"></a>
### `llm/*` events
<a id="llmadapters-updated--emit"></a>
#### `llm/adapters-updated` — emit
The provider topology changed: an adapter registered or unregistered routes, or the configurable-provider directory gained or lost entries. This is a payload-free registry notification fired at each commit point (including registration disposal); consumers re-read `listProviders()`, `listModels()`, or `listConfigurableProviders()` for the new state. Observer failures are contained and cannot veto the registry mutation.
```ts cordis-catalog
/**
* The provider topology changed: an adapter registered or unregistered
* routes, or the configurable-provider directory gained or lost entries.
* This is a payload-free registry notification fired at each commit point
* (including registration disposal); consumers re-read `listProviders()`,
* `listModels()`, or `listConfigurableProviders()` for the new state.
* Observer failures are contained and cannot veto the registry mutation.
* @mode emit
*/
'llm/adapters-updated'(): void
```
Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts)
<a id="llmstream--waterfall"></a>
#### `llm/stream` — waterfall
Waterfall around every streaming model call (retry, replay, routing). Bound to the LlmService; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit.
```ts cordis-catalog
/**
* Waterfall around every streaming model call (retry, replay, routing).
* Bound to the {@link LlmService}; call `next()` to reach the resolved
* adapter's stream, or yield your own chunks to short-circuit.
* @param options - the full request. A LOOP-built request carries the
* process-local {@link markAgentLoopRequest} identity and arrives deep-frozen
* (mutation throws): its content is a pure function of the session log (the
* reconstructability Agent Note), so listeners read it, never rewrite it.
* Hand-built calls do not carry that marker; their messages already obey
* the immutable creation contract.
* @mode waterfall
*/
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
```
Types: [GenerateOptions](core.md)
Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -255,3 +255,191 @@ interface ContentBlockMap {
```
块接口详见 [core.md § Content blocks and messages](core.md#content-blocks-and-messages)。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxllm--llmservice"></a>
### `ctx.llm` — `LlmService`
The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall.
```ts cordis-catalog
/**
* Register an adapter for the given provider routes. Throws `LlmError` with code
* `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
* Disposed with the fiber.
* @param providers - every provider route this adapter should serve.
* @param adapter - the adapter that streams calls for those providers.
* @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
*/
registerAdapter(providers: string[], adapter: LlmAdapter): AdapterRegistrationHandle
/**
* Describe provider routes with a registered adapter.
* @returns detached provider metadata in registration order.
*/
listProviders(): LlmProviderInfo[]
/**
* Declare provider routes an adapter plugin can activate through
* configuration. Registration is all-or-nothing: an empty list, invalid
* entry, or a provider already declared by any registration throws
* `LlmError` without registering the rest. Disposed with the fiber.
* @param entries - every configurable provider this plugin owns.
* @returns a handle that withdraws all of them, and can atomically replace them.
*/
registerConfigurableProviders(entries: readonly LlmConfigurableProvider[]): DirectoryRegistrationHandle
/**
* List every declared configurable provider, registered or dormant.
* @returns detached directory entries in declaration order.
*/
listConfigurableProviders(): LlmConfigurableProvider[]
/**
* Offer to interrogate provider endpoints on behalf of the settings
* namespace this plugin owns. The namespace is the key because that is what
* a configuration surface already holds from the configurable-provider
* directory, and because a provider being *added* has no route to name yet.
* Disposed with the fiber.
* @param settingsNs - the namespace whose profiles this discovery serves.
* @param discover - interrogates one endpoint; must honor `request.signal`.
* @returns the disposer that withdraws the offer.
*/
registerModelDiscovery( settingsNs: string, discover: (request: LlmModelDiscoveryRequest) => Promise<readonly LlmDiscoveredModel[]>, ): () => void
/**
* Interrogate one provider endpoint for the models it advertises. The
* request describes a draft, not a stored route, so nothing here reads or
* writes settings or credentials — the caller owns both, and the reply is
* candidate metadata a surface may offer for adoption.
* @param settingsNs - namespace whose registered discovery serves this draft.
* @param request - the endpoint, protocol, and one-shot credential to use.
* @returns the advertised models, deduplicated in endpoint order.
*/
async discoverModels( settingsNs: string, request: LlmModelDiscoveryRequest, ): Promise<LlmDiscoveredModel[]>
/**
* Resolve the retry policy captured when one provider route was registered.
* @param provider - registered provider route to inspect.
* @returns the provider-owned policy, with normal defaults already resolved.
*/
providerRetryPolicy(provider: string): ResolvedRetryPolicy
/**
* Discover models advertised by one registered provider. Catalog membership
* is advisory and never changes routing or request validation.
* @param provider - registered provider route to inspect.
* @returns detached model metadata in adapter-preferred order.
*/
async listModels(provider: string): Promise<LlmModelInfo[]>
/**
* Resolve and validate all metadata from the adapter that owns one exact
* route. The result is detached from adapter-owned objects; catalog
* membership remains advisory and does not control request routing.
* @param provider - registered provider route to inspect.
* @param model - exact model id passed to the adapter.
* @param signal - optional cancellation for adapter-owned asynchronous lookup.
* @returns exact model identity plus available context and reasoning metadata.
*/
async resolveModelInfo( provider: string, model: string, signal?: AbortSignal, ): Promise<LlmResolvedModelInfo>
/**
* Validate a conversation call config against its exact model capability and
* materialize adapter-configured defaults. Unsupported explicit efforts
* reject before provider I/O; no clamping or aliasing is performed. This
* standalone query does not bind a later dispatch; use {@link prepareCall}
* when logging and streaming must share one adapter registration.
* @param config - provider/model route and optional request controls.
* @param signal - optional cancellation for adapter-owned capability lookup.
* @returns a detached config only when a default must be materialized.
*/
async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>
/**
* Resolve one call under its current adapter registration. The returned
* one-shot handle keeps that registration across header logging and dispatch,
* so HMR cannot combine one adapter's capability result with another adapter.
* @param config - provider/model route and optional request controls.
* @param signal - optional cancellation for adapter-owned capability lookup.
* @returns a prepared config and its registration-bound stream entry point.
*/
async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>
/**
* Stream one model call as raw chunks (token-level deltas). Replay state is
* retained only when the same adapter instance owns its historical provider
* and the target provider. Final adapter selection remains fixed through
* asynchronous exact-model resolution and dispatch. Adapter selection,
* dispatch, and iteration failures become terminal `error` or `aborted`
* finish chunks; middleware, nested-call, cleanup, and consumer failures
* remain thrown.
* @param options - the full request; `options.provider` selects the adapter.
* @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
*/
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
```
Types: [AdapterRegistrationHandle](core.md) · [DirectoryRegistrationHandle](core.md) · [GenerateOptions](core.md) · [LlmCallConfig](core.md) · [LlmConfigurableProvider](core.md) · [LlmDiscoveredModel](core.md) · [LlmModelDiscoveryRequest](core.md) · [LlmModelInfo](core.md) · [LlmProviderInfo](core.md) · [LlmResolvedModelInfo](core.md)
Source: [`packages/llm/llm/src/index.ts:292`](../../packages/llm/llm/src/index.ts)
<a id="llm-events"></a>
### `llm/*` events
<a id="llmadapters-updated--emit"></a>
#### `llm/adapters-updated` — emit
The provider topology changed: an adapter registered or unregistered routes, or the configurable-provider directory gained or lost entries. This is a payload-free registry notification fired at each commit point (including registration disposal); consumers re-read `listProviders()`, `listModels()`, or `listConfigurableProviders()` for the new state. Observer failures are contained and cannot veto the registry mutation.
```ts cordis-catalog
/**
* The provider topology changed: an adapter registered or unregistered
* routes, or the configurable-provider directory gained or lost entries.
* This is a payload-free registry notification fired at each commit point
* (including registration disposal); consumers re-read `listProviders()`,
* `listModels()`, or `listConfigurableProviders()` for the new state.
* Observer failures are contained and cannot veto the registry mutation.
* @mode emit
*/
'llm/adapters-updated'(): void
```
Source: [`packages/llm/llm/src/index.ts:73`](../../packages/llm/llm/src/index.ts)
<a id="llmstream--waterfall"></a>
#### `llm/stream` — waterfall
Waterfall around every streaming model call (retry, replay, routing). Bound to the LlmService; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit.
```ts cordis-catalog
/**
* Waterfall around every streaming model call (retry, replay, routing).
* Bound to the {@link LlmService}; call `next()` to reach the resolved
* adapter's stream, or yield your own chunks to short-circuit.
* @param options - the full request. A LOOP-built request carries the
* process-local {@link markAgentLoopRequest} identity and arrives deep-frozen
* (mutation throws): its content is a pure function of the session log (the
* reconstructability Agent Note), so listeners read it, never rewrite it.
* Hand-built calls do not carry that marker; their messages already obey
* the immutable creation contract.
* @mode waterfall
*/
'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable<StreamChunk>): AsyncIterable<StreamChunk>
```
Types: [GenerateOptions](core.md)
Source: [`packages/llm/llm/src/index.ts:62`](../../packages/llm/llm/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/permission.md
permission.md: ec8cd4a923b617934f1612c31284103266aea4ef
permission.zh.md: 4534c33e2715e4a500344f547c7895128877f4fb
permission.md: 60ed88a79d0e51922f4cdffafa8b547669b49f5d
permission.zh.md: c06714fc6a111f56af680c3dea1764c90f09998d

View File

@@ -65,4 +65,67 @@ interface PresetOption {
`set(session, name)` resolves the preset (unknown names throw), appends a log-only `permission/preset` event unless `name` is already the effective preset, then writes each knob through its own setter — `setSandboxMode` from [dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) and `setApprovalPolicy` from [dsh-user-approval](../../packages/interaction/user-approval) — only when that knob's effective value changes. The selection event precedes the knob events in the same turn, and re-selecting the effective preset appends nothing at all.
`permission/preset` is durable, log-only user intent: it stays out of the model transcript (the knob events own the model-visible consequences through their consumers), and it exists so `current()` can preserve WHICH preset the user chose when two presets share a bundle; `effectivePermissionPreset(events)` folds the last one, and replay needs no catch-up state. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md); the method signatures are in the generated [service catalog](../cordis-catalog/services.md#ctxpermission--permissionservice).
`permission/preset` is durable, log-only user intent: it stays out of the model transcript (the knob events own the model-visible consequences through their consumers), and it exists so `current()` can preserve WHICH preset the user chose when two presets share a bundle; `effectivePermissionPreset(events)` folds the last one, and replay needs no catch-up state. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md); the method signatures are in the generated [service catalog](#ctxpermission--permissionservice).
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxpermission--permissionservice"></a>
### `ctx.permission` — `PermissionService`
Owns the deployment's permission presets and their write path. Requires a confining `ctx.bash` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error.
```ts cordis-catalog
/**
* Resolve the preset matching the effective knob values. A still-matching
* last selection wins shared-bundle ties; otherwise the first table match
* wins, or {@link CUSTOM_PRESET} when no entry matches.
* @param events - the session's events in log order.
* @returns the effective preset name, or `custom` when nothing matches.
*/
current(events: readonly SessionEvent[]): string
/**
* Build the whole select value for one folded knob state: every table
* option in declaration order, `custom` appended exactly while derived.
* @param state - the folded knob overrides.
* @returns the `permissions` projection payload.
*/
selectFor(state: KnobState): PermissionSelect
/**
* Resolve a preset's knob bundle.
* @param name - the preset name to resolve.
* @returns the configured bundle.
* @throws when `name` is not in the table.
*/
resolve(name: string): PresetSpec
/**
* Build the client option for a table entry or {@link CUSTOM_PRESET}. A
* missing label falls back to the table key.
* @param name - a table key, or `custom`.
* @returns the option a client renders.
* @throws when `name` is neither a table key nor `custom`.
*/
optionOf(name: string): PresetOption
/**
* Record a changed preset, then update each changed knob through its own
* setter. Selecting the effective preset again appends nothing.
* @param session - the session the switch belongs to.
* @param name - the preset to switch to; unknown names throw.
*/
set(session: Session, name: string): void
```
Types: [Session](session.md) · [SessionEvent](core.md)
Source: [`packages/interaction/permission/src/index.ts:159`](../../packages/interaction/permission/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -65,4 +65,67 @@ interface PresetOption {
`set(session, name)` 解析预设(未知名称抛出异常),在 `name` 尚不是生效预设时追加一条仅记日志的 `permission/preset` 事件,然后通过各旋钮自己的 setter[dsh-sandbox-policy](../../packages/sandbox/sandbox-policy) 的 `setSandboxMode` 与 [dsh-user-approval](../../packages/interaction/user-approval) 的 `setApprovalPolicy`)写入,且仅当该旋钮的生效值发生变化时才写。同一轮次内,选择事件先于旋钮事件出现;重新选择当前生效的预设则什么都不追加。
`permission/preset` 是持久、仅记日志的用户意图:它不进入模型 transcript文本记录模型可见的后果由旋钮事件经各自消费方承担它存在是为了在两个预设共享同一个旋钮组合时让 `current()` 仍能保住用户选择的究竟是哪一个预设;`effectivePermissionPreset(events)` 折叠最后一条,回放不需要任何追赶状态。完整事件声明见[持久化日志事件目录](../persistence-catalog.md);方法签名见生成的[服务目录](../cordis-catalog/services.md#ctxpermission--permissionservice)。
`permission/preset` 是持久、仅记日志的用户意图:它不进入模型 transcript文本记录模型可见的后果由旋钮事件经各自消费方承担它存在是为了在两个预设共享同一个旋钮组合时让 `current()` 仍能保住用户选择的究竟是哪一个预设;`effectivePermissionPreset(events)` 折叠最后一条,回放不需要任何追赶状态。完整事件声明见[持久化日志事件目录](../persistence-catalog.md);方法签名见生成的[服务目录](#ctxpermission--permissionservice)。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxpermission--permissionservice"></a>
### `ctx.permission` — `PermissionService`
Owns the deployment's permission presets and their write path. Requires a confining `ctx.bash` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error.
```ts cordis-catalog
/**
* Resolve the preset matching the effective knob values. A still-matching
* last selection wins shared-bundle ties; otherwise the first table match
* wins, or {@link CUSTOM_PRESET} when no entry matches.
* @param events - the session's events in log order.
* @returns the effective preset name, or `custom` when nothing matches.
*/
current(events: readonly SessionEvent[]): string
/**
* Build the whole select value for one folded knob state: every table
* option in declaration order, `custom` appended exactly while derived.
* @param state - the folded knob overrides.
* @returns the `permissions` projection payload.
*/
selectFor(state: KnobState): PermissionSelect
/**
* Resolve a preset's knob bundle.
* @param name - the preset name to resolve.
* @returns the configured bundle.
* @throws when `name` is not in the table.
*/
resolve(name: string): PresetSpec
/**
* Build the client option for a table entry or {@link CUSTOM_PRESET}. A
* missing label falls back to the table key.
* @param name - a table key, or `custom`.
* @returns the option a client renders.
* @throws when `name` is neither a table key nor `custom`.
*/
optionOf(name: string): PresetOption
/**
* Record a changed preset, then update each changed knob through its own
* setter. Selecting the effective preset again appends nothing.
* @param session - the session the switch belongs to.
* @param name - the preset to switch to; unknown names throw.
*/
set(session: Session, name: string): void
```
Types: [Session](session.md) · [SessionEvent](core.md)
Source: [`packages/interaction/permission/src/index.ts:159`](../../packages/interaction/permission/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/persistence.md
persistence.md: 177d596d3d01134266f139b47430e1e5064d04d6
persistence.zh.md: 6582c0d7d02d1976d5c3ca3322284098130ed4ad
persistence.md: 1edf3c88fbabe55058bbf94ed8f31decf47cb84a
persistence.zh.md: dbebd9ff819c8d50cfa9748678dac09c86adde6d

View File

@@ -204,3 +204,135 @@ Both implement the same abstract `SessionPersistence` (locate/create/append/prep
- **[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.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessionpersistence--sessionpersistence-abstract-seam"></a>
### `ctx.sessionPersistence` — `SessionPersistence` (abstract seam)
Durable append-only session storage. Implementations preserve contiguous, losslessly JSON-serializable events; append resolves only after durability, and load balances a complete interrupted tail without rewriting committed events.
```ts cordis-catalog
/**
* Resolve this backend's independent local artifact for a session without
* reading, creating, flushing, or otherwise materializing it. Backends such
* as SQLite that do not own one artifact per session return `undefined`.
* @param meta - the immutable session header whose artifact is requested.
* @returns the backend-specific absolute location, when one exists.
*/
abstract locate(meta: SessionHeader): SessionLocation | undefined
/**
* Register a new session's metadata. A backend MAY defer the physical write
* until the first {@link append} (lazy materialization), in which case a
* created-but-never-appended session is absent from {@link list}
* — abandoned sessions leave nothing behind.
* @param meta - the immutable header (id, version, cwd, lineage) to record.
*/
abstract create(meta: SessionHeader): Promise<void>
/**
* Durably persist a batch of events. Honors the append-only and contiguous-
* seq contracts: the first event's `seq` MUST equal the stored next-seq
* (after `load` has durably closed any interrupted turn). Rejects non-JSON-
* serializable `event.data` with an error naming the offending event type.
* @param id - the session the batch belongs to.
* @param events - the contiguous batch to persist, in seq order.
*/
abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void>
/**
* Prepare the exact unpublished Session used by resume. Implementations may
* reuse object graphs retained by an earlier {@link inspect} after confirming
* their durable revision is still current; disposal releases an unpublished
* reservation. Revision retries require the durable log to remain unchanged
* for one read/check round trip; continuous external writers may delay completion.
* @param id - persisted session to prepare.
* @param signal - optional cancellation for preparation work.
* @returns one owned unpublished Session preparation.
*/
async prepare(id: SessionId, signal?: AbortSignal): Promise<SessionPreparation>
/**
* Load an immutable balanced logical view and commit any required cold
* recovery. A complete interrupted final turn is preserved and durably
* closed with missing tool errors plus any open step and turn boundaries;
* only a torn final record is discarded. Unknown versions and corruption in
* the committed prefix reject. Implementations MUST NOT crash-repair an
* identity still bound to a live Session: a balanced live log may return as a
* durable snapshot, while an open live turn rejects. Returned values may be
* shared with immutable live or prepared state and must not be mutated.
* Revision-based implementations may wait for one stable read/check round trip.
* @param id - the persisted session to reload.
* @returns the header and a log ending on a balanced `turn/end`.
*/
abstract load(id: SessionId): Promise<SessionInspection>
/**
* Inspect an immutable logical session without committing recovery or
* publishing it. A cold complete interrupted turn receives synthetic closers
* in memory and a torn physical tail remains untouched. An already-live
* Session instead yields its current immutable snapshot, which may contain an
* open turn and its `session/end-seed` boundary. Coordinator-backed
* implementations retain the exact cold unpublished Session for bounded
* reuse by a later {@link prepare}. A stale ready source is reloaded; a source
* already committing or reserved for resume remains exclusive, and inspection
* may borrow its immutable view. Callers borrow only the immutable header and
* log. Continuous external writers may delay revision convergence.
* @param id - the persisted session to inspect.
* @param signal - optional cancellation for queued and backend read work.
* @returns the validated header and current logical event log.
*/
abstract inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection>
/**
* Read the stored events from `fromSeq` onward — the read-from-seq
* primitive for read models that resume from a watermark (e.g. a persisted
* projection cache folding only the tail past its checkpoint). Unlike
* {@link inspect}, it is a detached physical suffix read: no preparation
* cache, torn-tail truncation, synthetic closers, or coordinator-state
* publication. Only events from the valid contiguous stored prefix are
* returned, so a torn fragment never reaches the caller. `fromSeq` at or
* beyond the stored prefix returns an empty event list (never an error).
* Backends whose medium can seek by seq
* (SQLite) read only the suffix; sequential media (JSONL, both encodings)
* still parse the whole artifact and skip forward — the primitive bounds
* what is RETURNED and refolded, not every backend's physical read.
* @param id - the persisted session to read.
* @param fromSeq - first event seq to include; a non-negative safe integer.
* @param signal - optional cancellation for queued and backend read work.
* @returns the header and the stored events with `seq >= fromSeq`.
*/
abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
/**
* Lightweight listing from metadata, without a full-log parse.
* @param signal - optional cancellation for backend listing work.
* @returns one header per materialized session.
*/
abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
/**
* List materialized sessions with cheap per-log change tokens.
*
* Repeated observations of an unchanged log return the same revision. A
* successful mutating {@link load} repair changes the next listed revision.
* Revisions also distinguish independently backed stores so backend-local
* counters cannot compare equal across different persistence sources.
* @param signal - optional cancellation for backend snapshot-listing work.
* @returns one header and opaque revision per materialized session without loading full logs.
*/
abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
```
Types: [SessionEvent](core.md) · [SessionId](core.md)
Source: [`packages/session/session-persistence/src/index.ts:72`](../../packages/session/session-persistence/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -204,3 +204,135 @@ interface SessionPersistenceSnapshot {
- **[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。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessionpersistence--sessionpersistence-abstract-seam"></a>
### `ctx.sessionPersistence` — `SessionPersistence` (abstract seam)
Durable append-only session storage. Implementations preserve contiguous, losslessly JSON-serializable events; append resolves only after durability, and load balances a complete interrupted tail without rewriting committed events.
```ts cordis-catalog
/**
* Resolve this backend's independent local artifact for a session without
* reading, creating, flushing, or otherwise materializing it. Backends such
* as SQLite that do not own one artifact per session return `undefined`.
* @param meta - the immutable session header whose artifact is requested.
* @returns the backend-specific absolute location, when one exists.
*/
abstract locate(meta: SessionHeader): SessionLocation | undefined
/**
* Register a new session's metadata. A backend MAY defer the physical write
* until the first {@link append} (lazy materialization), in which case a
* created-but-never-appended session is absent from {@link list}
* — abandoned sessions leave nothing behind.
* @param meta - the immutable header (id, version, cwd, lineage) to record.
*/
abstract create(meta: SessionHeader): Promise<void>
/**
* Durably persist a batch of events. Honors the append-only and contiguous-
* seq contracts: the first event's `seq` MUST equal the stored next-seq
* (after `load` has durably closed any interrupted turn). Rejects non-JSON-
* serializable `event.data` with an error naming the offending event type.
* @param id - the session the batch belongs to.
* @param events - the contiguous batch to persist, in seq order.
*/
abstract append(id: SessionId, events: readonly SessionEvent[]): Promise<void>
/**
* Prepare the exact unpublished Session used by resume. Implementations may
* reuse object graphs retained by an earlier {@link inspect} after confirming
* their durable revision is still current; disposal releases an unpublished
* reservation. Revision retries require the durable log to remain unchanged
* for one read/check round trip; continuous external writers may delay completion.
* @param id - persisted session to prepare.
* @param signal - optional cancellation for preparation work.
* @returns one owned unpublished Session preparation.
*/
async prepare(id: SessionId, signal?: AbortSignal): Promise<SessionPreparation>
/**
* Load an immutable balanced logical view and commit any required cold
* recovery. A complete interrupted final turn is preserved and durably
* closed with missing tool errors plus any open step and turn boundaries;
* only a torn final record is discarded. Unknown versions and corruption in
* the committed prefix reject. Implementations MUST NOT crash-repair an
* identity still bound to a live Session: a balanced live log may return as a
* durable snapshot, while an open live turn rejects. Returned values may be
* shared with immutable live or prepared state and must not be mutated.
* Revision-based implementations may wait for one stable read/check round trip.
* @param id - the persisted session to reload.
* @returns the header and a log ending on a balanced `turn/end`.
*/
abstract load(id: SessionId): Promise<SessionInspection>
/**
* Inspect an immutable logical session without committing recovery or
* publishing it. A cold complete interrupted turn receives synthetic closers
* in memory and a torn physical tail remains untouched. An already-live
* Session instead yields its current immutable snapshot, which may contain an
* open turn and its `session/end-seed` boundary. Coordinator-backed
* implementations retain the exact cold unpublished Session for bounded
* reuse by a later {@link prepare}. A stale ready source is reloaded; a source
* already committing or reserved for resume remains exclusive, and inspection
* may borrow its immutable view. Callers borrow only the immutable header and
* log. Continuous external writers may delay revision convergence.
* @param id - the persisted session to inspect.
* @param signal - optional cancellation for queued and backend read work.
* @returns the validated header and current logical event log.
*/
abstract inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection>
/**
* Read the stored events from `fromSeq` onward — the read-from-seq
* primitive for read models that resume from a watermark (e.g. a persisted
* projection cache folding only the tail past its checkpoint). Unlike
* {@link inspect}, it is a detached physical suffix read: no preparation
* cache, torn-tail truncation, synthetic closers, or coordinator-state
* publication. Only events from the valid contiguous stored prefix are
* returned, so a torn fragment never reaches the caller. `fromSeq` at or
* beyond the stored prefix returns an empty event list (never an error).
* Backends whose medium can seek by seq
* (SQLite) read only the suffix; sequential media (JSONL, both encodings)
* still parse the whole artifact and skip forward — the primitive bounds
* what is RETURNED and refolded, not every backend's physical read.
* @param id - the persisted session to read.
* @param fromSeq - first event seq to include; a non-negative safe integer.
* @param signal - optional cancellation for queued and backend read work.
* @returns the header and the stored events with `seq >= fromSeq`.
*/
abstract readFrom(id: SessionId, fromSeq: number, signal?: AbortSignal): Promise<{ meta: SessionHeader; events: SessionEvent[] }>
/**
* Lightweight listing from metadata, without a full-log parse.
* @param signal - optional cancellation for backend listing work.
* @returns one header per materialized session.
*/
abstract list(signal?: AbortSignal): Promise<SessionHeader[]>
/**
* List materialized sessions with cheap per-log change tokens.
*
* Repeated observations of an unchanged log return the same revision. A
* successful mutating {@link load} repair changes the next listed revision.
* Revisions also distinguish independently backed stores so backend-local
* counters cannot compare equal across different persistence sources.
* @param signal - optional cancellation for backend snapshot-listing work.
* @returns one header and opaque revision per materialized session without loading full logs.
*/
abstract listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]>
```
Types: [SessionEvent](core.md) · [SessionId](core.md)
Source: [`packages/session/session-persistence/src/index.ts:72`](../../packages/session/session-persistence/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/plan.md
plan.md: 661a393b18f9154771c0b9358ad7f016a4fdec17
plan.zh.md: 068b63fd9f3bf46b7962031bba154cc7fc7daafd
plan.md: ee4b911050863765960ba46952ba923a7115e129
plan.zh.md: 0481bdf76adf2f00fe18073fe8daa5ba172bbfe1

View File

@@ -36,4 +36,51 @@ When [`ctx.commands`](commands.md) is composed, the plugin registers `/plan [off
## The service
`ctx.planMode` owns the logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool; `get`/`set` signatures are in the generated [service catalog](../cordis-catalog/services.md#ctxplanmode--planmodeservice).
`ctx.planMode` owns the logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool; `get`/`set` signatures are in the generated [service catalog](#ctxplanmode--planmodeservice).
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxplanmode--planmodeservice"></a>
### `ctx.planMode` — `PlanModeService`
`ctx.planMode`: owns logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool. UIs observe committed flips through `session/event`; there is no live mirror.
```ts cordis-catalog
/**
* Read the logged plan state and any selected state awaiting a boundary.
*
* @param agent The agent to read.
* @returns Current logged state plus a pending selection, when present.
*/
get(agent: Agent): { active: boolean; pending?: boolean }
/**
* Select whether plan mode should be active. Between turns the change
* commits immediately — no request boundary would arrive until the next
* prompt, so a queued intent would hang (the open-turn fold is the idle
* signal: agent status stays `running` through post-turn checkpointing,
* where a boundary equally never comes). During an open turn the
* selection is held as pending intent for the next in-turn request
* boundary. Repeated selection of the current or already-pending state is
* a no-op.
*
* @param agent The agent to switch.
* @param active Whether plan mode should be active.
* @returns what happened: `committed` (logged now), `queued` (awaiting the
* next boundary), `cancelled` (an opposite pending selection was cleared;
* the logged state already matches), or `noop` (already in that state).
*/
set(agent: Agent, active: boolean): 'committed' | 'queued' | 'cancelled' | 'noop'
```
Types: [Agent](core.md)
Source: [`packages/plan/plan-mode/src/index.ts:183`](../../packages/plan/plan-mode/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -36,4 +36,51 @@ interface PlanModeConfig {
## 服务
`ctx.planMode` 拥有已记录的计划状态、边界处的应用与叙述、`plan:policy` 段落、`/plan` 命令和稳定注册的退出工具;`get`/`set` 签名见生成的[服务目录](../cordis-catalog/services.md#ctxplanmode--planmodeservice)。
`ctx.planMode` 拥有已记录的计划状态、边界处的应用与叙述、`plan:policy` 段落、`/plan` 命令和稳定注册的退出工具;`get`/`set` 签名见生成的[服务目录](#ctxplanmode--planmodeservice)。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxplanmode--planmodeservice"></a>
### `ctx.planMode` — `PlanModeService`
`ctx.planMode`: owns logged plan state, boundary application and narration, the `plan:policy` section, the `/plan` command, and the stable exit tool. UIs observe committed flips through `session/event`; there is no live mirror.
```ts cordis-catalog
/**
* Read the logged plan state and any selected state awaiting a boundary.
*
* @param agent The agent to read.
* @returns Current logged state plus a pending selection, when present.
*/
get(agent: Agent): { active: boolean; pending?: boolean }
/**
* Select whether plan mode should be active. Between turns the change
* commits immediately — no request boundary would arrive until the next
* prompt, so a queued intent would hang (the open-turn fold is the idle
* signal: agent status stays `running` through post-turn checkpointing,
* where a boundary equally never comes). During an open turn the
* selection is held as pending intent for the next in-turn request
* boundary. Repeated selection of the current or already-pending state is
* a no-op.
*
* @param agent The agent to switch.
* @param active Whether plan mode should be active.
* @returns what happened: `committed` (logged now), `queued` (awaiting the
* next boundary), `cancelled` (an opposite pending selection was cleared;
* the logged state already matches), or `noop` (already in that state).
*/
set(agent: Agent, active: boolean): 'committed' | 'queued' | 'cancelled' | 'noop'
```
Types: [Agent](core.md)
Source: [`packages/plan/plan-mode/src/index.ts:183`](../../packages/plan/plan-mode/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/pty.md
pty.md: 97e1e662d1128ab0555e34f8284cf69d7d9d0d1a
pty.zh.md: a57f7448274de583dbb110ba9499e15dfb4de5f9
# pnpm run verify-translation-pairing --write docs/subsystems/pty.md
pty.md: 45b157c73926d85d5fc2bb037d3954da7bf75063
pty.zh.md: 4409ef0b0504780b0b5564a34b61d4f1b2c02775

View File

@@ -89,3 +89,96 @@ interface PtySendResult {
## Ownership and durability
`PtyService` attaches one awaited cleanup to the exact owner scope, rejects foreign operations, and keeps sessions alive across backend or tool-plugin reload. PTY state and raw bytes remain process-local. Model input and bounded returned output are durable through the existing `tool/call`, `tool/result`, and task-result paths rather than duplicate PTY session events.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxpty--ptyservice"></a>
### `ctx.pty` — `PtyService`
In-process registry for replaceable PTY backends and exact-Agent sessions.
```ts cordis-catalog
/**
* Register one backend type for this effect scope.
* @param backend - provider with a non-empty unique type.
* @returns disposer that removes exactly this contribution.
*/
registerBackend(backend: PtyBackend): () => void
/**
* List registered backend types in registration order.
* @returns fresh backend type names.
*/
listBackends(): string[]
/**
* Create and publish one owner-scoped session after backend setup succeeds.
* @param owner - exact registered Agent that owns access and cleanup.
* @param request - backend type plus optional owner-local name and cwd.
* @param signal - cancellation of unpublished setup.
* @returns published identity, metadata, status, and MOTD.
*/
async spawn(owner: Agent, request: PtySpawnRequest, signal?: AbortSignal): Promise<PtySpawnResult>
/**
* Test whether an exact owner has a published session or unpublished spawn.
* @param owner - exact live owner to inspect.
* @returns true across the entire spawn-to-close interval, with no publication gap.
*/
hasOwnerActivity(owner: Agent): boolean
/**
* Start one exclusive interactive send.
* @param owner - exact session owner.
* @param id - target PTY identity.
* @param request - explicit text, submit behavior, and cancellation.
* @returns live operation handle for foreground await or task registration.
*/
startSend(owner: Agent, id: PtySessionId, request: PtySendRequest): PtySendOperation
/**
* Read one bounded scrollback page from an owned session.
* @param owner - exact session owner.
* @param id - target PTY identity.
* @param request - optional newest-relative offset and line count.
* @returns bounded retained text and pagination metadata.
*/
read(owner: Agent, id: PtySessionId, request: PtyReadRequest = {}): PtyReadResult
/**
* Deliver an allowed signal through an owned backend session.
* @param owner - exact session owner.
* @param id - target PTY identity.
* @param signal - allowed POSIX signal name.
* @returns delivered foreground process-group identity.
*/
signal(owner: Agent, id: PtySessionId, signal: PtySignal): Promise<PtySignalResult>
/**
* Close one owned session and remove it only after quiescent backend cleanup.
* @param owner - exact session owner.
* @param id - target PTY identity.
* @param reason - diagnostic cleanup reason.
* @returns true for a newly closed session, false when the same close is already in flight.
*/
async kill(owner: Agent, id: PtySessionId, reason: string = 'model request'): Promise<boolean>
/**
* List fresh snapshots for exactly one owner.
* @param owner - exact owner whose sessions are visible.
* @returns owner-visible snapshots in publication order.
*/
list(owner: Agent): PtySessionSnapshot[]
```
Types: [Agent](core.md)
Source: [`packages/pty/pty/src/index.ts:105`](../../packages/pty/pty/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -89,3 +89,96 @@ interface PtySendResult {
## 归属与持久性
`PtyService` 会将一项等待完成的清理附加到确切的拥有者作用域拒绝其他拥有者的操作并让会话在后端或工具插件重载期间保持存活。PTY 状态与原始字节仍局限在进程内。模型输入与有界返回输出通过现有 `tool/call`、`tool/result` 和任务结果路径持久保存,而不是重复记录 PTY 会话事件。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxpty--ptyservice"></a>
### `ctx.pty` — `PtyService`
In-process registry for replaceable PTY backends and exact-Agent sessions.
```ts cordis-catalog
/**
* Register one backend type for this effect scope.
* @param backend - provider with a non-empty unique type.
* @returns disposer that removes exactly this contribution.
*/
registerBackend(backend: PtyBackend): () => void
/**
* List registered backend types in registration order.
* @returns fresh backend type names.
*/
listBackends(): string[]
/**
* Create and publish one owner-scoped session after backend setup succeeds.
* @param owner - exact registered Agent that owns access and cleanup.
* @param request - backend type plus optional owner-local name and cwd.
* @param signal - cancellation of unpublished setup.
* @returns published identity, metadata, status, and MOTD.
*/
async spawn(owner: Agent, request: PtySpawnRequest, signal?: AbortSignal): Promise<PtySpawnResult>
/**
* Test whether an exact owner has a published session or unpublished spawn.
* @param owner - exact live owner to inspect.
* @returns true across the entire spawn-to-close interval, with no publication gap.
*/
hasOwnerActivity(owner: Agent): boolean
/**
* Start one exclusive interactive send.
* @param owner - exact session owner.
* @param id - target PTY identity.
* @param request - explicit text, submit behavior, and cancellation.
* @returns live operation handle for foreground await or task registration.
*/
startSend(owner: Agent, id: PtySessionId, request: PtySendRequest): PtySendOperation
/**
* Read one bounded scrollback page from an owned session.
* @param owner - exact session owner.
* @param id - target PTY identity.
* @param request - optional newest-relative offset and line count.
* @returns bounded retained text and pagination metadata.
*/
read(owner: Agent, id: PtySessionId, request: PtyReadRequest = {}): PtyReadResult
/**
* Deliver an allowed signal through an owned backend session.
* @param owner - exact session owner.
* @param id - target PTY identity.
* @param signal - allowed POSIX signal name.
* @returns delivered foreground process-group identity.
*/
signal(owner: Agent, id: PtySessionId, signal: PtySignal): Promise<PtySignalResult>
/**
* Close one owned session and remove it only after quiescent backend cleanup.
* @param owner - exact session owner.
* @param id - target PTY identity.
* @param reason - diagnostic cleanup reason.
* @returns true for a newly closed session, false when the same close is already in flight.
*/
async kill(owner: Agent, id: PtySessionId, reason: string = 'model request'): Promise<boolean>
/**
* List fresh snapshots for exactly one owner.
* @param owner - exact owner whose sessions are visible.
* @returns owner-visible snapshots in publication order.
*/
list(owner: Agent): PtySessionSnapshot[]
```
Types: [Agent](core.md)
Source: [`packages/pty/pty/src/index.ts:105`](../../packages/pty/pty/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/sandbox.md
sandbox.md: 9e5feafe046f18dad49aeaf281793f0b8e03c240
sandbox.zh.md: a1314d1b78eb0d46ea4c8ca5aa330ee83bf89132
# pnpm run verify-translation-pairing --write docs/subsystems/sandbox.md
sandbox.md: dd960b3021dcdc87cfd36fd439cbec0a810dd736
sandbox.zh.md: f526982a782144a0bf58abcde5d883e9dc813369

View File

@@ -146,3 +146,65 @@ The [local provider](../../packages/sandbox/sandbox-local/README.md) owns operat
`ctx.sandbox.confine(argv, policy)` returns a `ConfinedArgv` or throws `SandboxUnavailableError` with code `SANDBOX_UNAVAILABLE` when no usable backend exists. Consumers may also classify a failure while spawning or observing the returned argv; that attribution belongs to the consumer contract. Silent unconfined passthrough is never legal for a confined policy.
Provider selection, probing, caching, and backend-specific enforcement reports belong to the [local provider](../../packages/sandbox/sandbox-local/README.md).
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsandbox--sandboxprovider-abstract-seam"></a>
### `ctx.sandbox` — `SandboxProvider` (abstract seam)
Abstract process-sandbox service. confine must return enforcing argv or fail closed at wrap or runner-execution time; silent unconfined passthrough is forbidden. Functional probes arbitrate multi-runner chains and may be skipped for a sole candidate, whose own refusal remains the fail-closed end.
```ts cordis-catalog
/**
* Wrap `argv` so it executes confined under `policy` on this host; the
* caller spawns the returned argv in place of its own.
* @param argv - the exact argv the caller is about to spawn (program plus
* arguments), NOT a shell string — a shell-shaped consumer passes
* `['bash', '-c', command]`.
* @param policy - the file-effect policy this execution runs under,
* carried per call (see {@link SandboxPolicy}).
* @returns the argv to spawn instead, plus the enforcement completeness
* the selected backend achieves for it.
*/
abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv
```
Source: [`packages/sandbox/sandbox/src/index.ts:148`](../../packages/sandbox/sandbox/src/index.ts)
<a id="ctxsandboxpolicy--sandboxpolicyservice"></a>
### `ctx.sandboxPolicy` — `SandboxPolicyService`
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
```ts cordis-catalog
/**
* Resolve the complete policy for one capability call. An approved explicit
* mode outranks the session's last `sandbox/mode` event, which outranks the
* deployment default. A session cwd is its workspace-write boundary; the
* configured root is the fallback for agentless calls and sessions without a
* cwd.
* @param request - optional session and approved mode override.
* @returns the fully resolved per-call mode and absolute workspace root.
*/
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy
/**
* Read the session override without applying the deployment default.
* @param session - session whose log supplies the override.
* @returns the last logged mode, or `undefined` without one.
*/
overrideOf(session: Session): SandboxMode | undefined
```
Types: [Session](session.md)
Source: [`packages/sandbox/sandbox-policy/src/index.ts:91`](../../packages/sandbox/sandbox-policy/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -146,3 +146,65 @@ interface ConfinedArgv {
`ctx.sandbox.confine(argv, policy)` 返回一个 `ConfinedArgv`,或在没有可用后端时抛出 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)。消费方也可以在 spawn 或观察所返回的 argv 时对失败进行分类;该归因属于消费方契约。对于受限策略,静默的无隔离透传永远不合法。
提供方选择、探测、缓存和后端专有的强制执行报告归[本地提供方](../../packages/sandbox/sandbox-local/README.md)所有。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsandbox--sandboxprovider-abstract-seam"></a>
### `ctx.sandbox` — `SandboxProvider` (abstract seam)
Abstract process-sandbox service. confine must return enforcing argv or fail closed at wrap or runner-execution time; silent unconfined passthrough is forbidden. Functional probes arbitrate multi-runner chains and may be skipped for a sole candidate, whose own refusal remains the fail-closed end.
```ts cordis-catalog
/**
* Wrap `argv` so it executes confined under `policy` on this host; the
* caller spawns the returned argv in place of its own.
* @param argv - the exact argv the caller is about to spawn (program plus
* arguments), NOT a shell string — a shell-shaped consumer passes
* `['bash', '-c', command]`.
* @param policy - the file-effect policy this execution runs under,
* carried per call (see {@link SandboxPolicy}).
* @returns the argv to spawn instead, plus the enforcement completeness
* the selected backend achieves for it.
*/
abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv
```
Source: [`packages/sandbox/sandbox/src/index.ts:148`](../../packages/sandbox/sandbox/src/index.ts)
<a id="ctxsandboxpolicy--sandboxpolicyservice"></a>
### `ctx.sandboxPolicy` — `SandboxPolicyService`
The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mode, fallback workspace root, and current request-time policy section. Tool layers call resolve for each execution so a session's mode log and immutable cwd travel together to every enforcing capability.
```ts cordis-catalog
/**
* Resolve the complete policy for one capability call. An approved explicit
* mode outranks the session's last `sandbox/mode` event, which outranks the
* deployment default. A session cwd is its workspace-write boundary; the
* configured root is the fallback for agentless calls and sessions without a
* cwd.
* @param request - optional session and approved mode override.
* @returns the fully resolved per-call mode and absolute workspace root.
*/
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy
/**
* Read the session override without applying the deployment default.
* @param session - session whose log supplies the override.
* @returns the last logged mode, or `undefined` without one.
*/
overrideOf(session: Session): SandboxMode | undefined
```
Types: [Session](session.md)
Source: [`packages/sandbox/sandbox-policy/src/index.ts:91`](../../packages/sandbox/sandbox-policy/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md
session-projection.md: 56f9fb43dddc482264aab52ceeb9b9920df148cd
session-projection.zh.md: 4a2310fed4af6f018af07b5f2b46d3c28aa361a4

View File

@@ -0,0 +1,262 @@
# Session Projections
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 interface 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).
Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts)
## The unit
`SessionProjectionMap` is the merge-extensible type table for the whole chain (host unit, wire block, client hook); values are wire-JSON whole values, and rendering belongs to the slot system, never this layer. A domain contributes one `ProjectionDefinition` per key:
```ts type-equiv
/**
* One domain's state-driven computation unit: three pure synchronous
* functions plus declarations — never an opaque getter. The framework drives
* `apply` on every committed session event; the domain holds no
* subscriptions and owns only the mathematics. All three functions MUST be
* synchronous (an async unit would tear the carriers' consistency cut) and
* `state` MUST be plain JSON (the persisted-cache precondition).
*/
interface ProjectionDefinition<K extends keyof SessionProjectionMap, S> {
/** The projection key this unit owns (its `SessionProjectionMap` entry). */
key: K
/** Validates the wire payload (`view` output) before it leaves the host. */
schema: ZodType<SessionProjectionMap[K]>
/**
* State for the empty log.
* @returns the initial state.
*/
init(): S
/**
* Pure transition: previous state + one committed event → next state. A
* unit uninterested in an event MUST return the same state reference — an
* unchanged reference (`Object.is`) produces zero downstream work.
* @param state - the state covering all prior events.
* @param event - the next committed session event.
* @returns the next state (same reference when the event is not the unit's).
*/
apply(state: S, event: SessionEvent): S
/**
* State → wire payload (the read-side projection).
* @param state - the current state.
* @returns the whole current value for this unit's key.
*/
view(state: S): SessionProjectionMap[K]
/**
* Persisted-cache invalidation anchor: bump whenever the state shape or the
* fold semantics change, so persisted `(sessionId, key, ver, seq, val)`
* rows from an older unit are discarded instead of being forward-applied
* into garbage. Non-negative integer.
*/
stateVersion: number
}
```
The whole-value event rule is load-bearing: a state-carrying log event carries the complete post-change state, never a bare delta — it keeps every transition trivially cheap and every served value self-describing (last-wins for consumers).
## The snapshot and the change feed
```ts type-equiv
/**
* One consistent read cut over every registered unit for one session.
* `asOfSeq` is the shared watermark — the seq of the last event every value
* reflects (`-1` for an empty log, mirroring `session/subscribed.lastSeq`).
*/
interface ProjectionSnapshot {
/** Seq of the last event the values reflect; -1 for an empty log. */
asOfSeq: number
/** Whole current value per registered key. */
values: Partial<SessionProjectionMap>
}
```
```ts type-equiv
/**
* Change-feed listener: one unit's value changed for one session. `value` is
* the schema-validated `view` output; `seq` is the unit's watermark at
* emission (the seq of the event that caused the change).
*/
type ProjectionChangeListener = (
session: Session,
key: Extract<keyof SessionProjectionMap, string>,
value: unknown,
seq: number,
) => void
```
`snapshot(session)` is fully synchronous — a carrier reads it in the same tick as its page slice, which is what makes `asOfSeq` one consistent cut — and every value passes its unit's schema before leaving (an accidentally-async `view` returns a Promise, which fails that boundary parse loudly). The change feed fires once per unit whose state *reference* changed, per committed event: the same-reference discipline in `apply` is the gate.
## The registry: `ctx.sessionProjections`
`SessionProjectionRegistry` ([signatures](#ctxsessionprojections--sessionprojectionregistry)) owns the drive: one `session/event` subscription, eager `apply` over every registered unit, and per-session per-unit watermark cells. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect whose disposer rides the calling fiber: an unloaded domain plugin's key (with its cached cells) disappears from subsequent drives and snapshots, and clients read that as capability absence; duplicate keys throw. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessionprojectioncache--sessionprojectioncache"></a>
### `ctx.sessionProjectionCache` — `SessionProjectionCache`
The persisted projection cache service. Opens the `session_projcache` domain at init, checkpoints live sessions on a throttled write-behind (count/interval triggers from Config) plus two mandatory points — `turn/end` and session disposal (the live-to-cold moment) — and serves the cold-read ladder: cached row, persistence `readFrom` tail, registry `restore`, durable write-back. Every durable write is fail-soft: failures log a warning and the cache self-heals on the next write or cold read.
```ts cordis-catalog
/**
* The zero-I/O listing read: whole values viewed straight from the stored
* rows (version-matching keys only), each cut carried with its watermark
* so a client value store can seed under its higher-seq-wins rule — as
* stale as the last durable checkpoint but never wrong, and never from an
* unrelated log (the caller's header is the identity witness). Fresher
* paths (the history tail baseline, {@link coldSnapshot}) supersede these
* values whenever a session is actually opened.
* @param meta - the listed session's header (identity witness; no log read).
* @returns the cut (`asOfSeq` = lowest served-row watermark), or
* `undefined` when no usable row exists for this lifecycle.
*/
cachedSnapshot(meta: SessionHeader): ProjectionSnapshot | undefined
/**
* Durably checkpoint one live session NOW (both mandatory points call
* this; tests and carriers may too). The registry cut is snapshotted at
* this boundary (states are live references), then the whole record is
* replaced. NOT fail-soft — callers on the fail-soft paths contain it.
* @param session - the live session to checkpoint.
* @returns resolution after durability and event emission.
*/
async write(session: Session): Promise<void>
/**
* Cold-read one persisted session's projections with zero full-log load:
* cached rows + a persistence `readFrom` tail from the registry's restore
* floor, refolded by the registry and written back (fail-soft) so the next
* cold read starts closer. A cache row invalidated by a shrunk log
* (crash-repair truncation) triggers one full re-read from seq 0 — the
* ladder's slow rung, still no crash. Rejects when the session has no
* persisted log (`not found` from the persistence seam).
* @param id - the persisted session to read.
* @param signal - optional cancellation for the persistence reads.
* @returns the snapshot cut at the stored log end.
*/
async coldSnapshot(id: SessionId, signal?: AbortSignal): Promise<ProjectionSnapshot>
```
Types: [Session](session.md) · [SessionHeader](persistence.md) · [SessionId](core.md)
Source: [`packages/session/session-projection-cache/src/index.ts:71`](../../packages/session/session-projection-cache/src/index.ts)
<a id="ctxsessionprojections--sessionprojectionregistry"></a>
### `ctx.sessionProjections` — `SessionProjectionRegistry`
`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. Duplicate keys throw. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected.
```ts cordis-catalog
/**
* Register one domain's unit. The registration is an effect on the calling
* context's fiber: disposing the fiber (or calling the returned disposer)
* removes the key — and the unit's cached cells — from subsequent drives
* and snapshots.
* @param definition - key, boundary schema, pure unit functions, and stateVersion.
* @returns the exact disposer that unregisters this unit.
*/
register<K extends keyof SessionProjectionMap, S>(definition: ProjectionDefinition<K, S>): () => void
/**
* Subscribe to the change feed. The registration is an effect on the
* calling context's fiber.
* @param listener - called once per unit whose state reference changed, per committed event.
* @returns the exact disposer that unsubscribes.
*/
onChanged(listener: ProjectionChangeListener): () => void
/**
* One consistent cut over every registered unit for one session, read from
* the watermark cache (missing cells fold lazily over the in-memory log).
* Fully synchronous — every value and `asOfSeq` reflect the same log
* position. Each value passes its unit's schema before leaving.
* @param session - the session whose projection values are read.
* @returns the snapshot; `values` is empty when no unit is registered.
*/
snapshot(session: Session): ProjectionSnapshot
/**
* State-level checkpoint of every registered unit for one session, read
* from the watermark cache (missing cells fold lazily over the in-memory
* log). This is the write side of the persisted projection cache: the
* returned rows are the `(key → {ver, seq, val})` part of the durable
* `(sessionId, key, ver, seq, val)`
* rows. Every `val` is a DETACHED structured clone — never the live
* cell reference: the watermark cache is this registry's authoritative
* mutable state, and a caller reaching the live reference could corrupt
* every subsequent snapshot and frame through it (plain JSON by the unit
* contract, so the clone is total).
* @param session - the session whose unit states are checkpointed.
* @returns one row per registered key; empty when no unit is registered.
*/
checkpoint(session: Session): ProjectionCheckpoint
/**
* The stored seq a {@link restore} tail read over `checkpoint` must start
* at: one event BELOW the lowest usable watermark (a row is usable when
* its `ver` matches the live unit's `stateVersion`; an absent or mismatched row
* pulls the floor to `0` — that key must refold the full log). The
* one-below anchor is load-bearing: the tail then proves how far the
* stored log still extends, so {@link restore} can detect a log that
* shrank below a row's watermark (crash-repair truncation) instead of
* serving the stale row as current — an empty tail read from the anchor
* yields an end below every watermark and the restore rejects for a full
* re-read.
* @param checkpoint - persisted rows for one session (possibly stale or empty).
* @returns the seq to hand the persistence `readFrom`, or `undefined`
* when no unit is registered (no read needed — {@link restore} would
* serve empty values regardless).
*/
restoreFloor(checkpoint: ProjectionCheckpoint): number | undefined
/**
* View a checkpoint's rows without any log read: for every registered
* unit whose row's `ver` matches, serve the schema-validated
* `view` of the stored state; mismatched or absent rows leave their key
* absent (a cold or listing consumer treats it as not-yet-available and a
* fuller read path refolds it). The zero-I/O rung of the read ladder —
* values are as stale as their rows, never wrong.
* @param checkpoint - persisted rows for one session (possibly stale or empty).
* @returns whole values per key with a usable row; empty when none.
*/
viewCheckpoint(checkpoint: ProjectionCheckpoint): Partial<SessionProjectionMap>
/**
* Cold read: fold every registered unit over a stored log suffix, seeding
* each from its checkpoint row when usable — the one read recipe (cached
* state + forward tail replay + `view`) applied without a live `Session`.
* Call with the events returned by a persistence
* `readFrom(id, restoreFloor(checkpoint))` and that same floor as
* `baseSeq`; the floor's one-below anchor makes the supplied end honest,
* so a shrunk log is detected here. A row is usable iff its
* `ver` matches the live unit's `stateVersion`, it does not predate `baseSeq`
* (`seq >= baseSeq - 1`), and it does not claim events past the
* supplied end (`seq <= endSeq`); an unusable row is discarded
* and its key refolds from `init` — which is only sound over the full
* log, so a discarded row with `baseSeq > 0` throws (the caller re-reads
* from seq 0, e.g. after a crash-repair truncation shrank the log below
* a row's watermark).
* @param checkpoint - persisted rows for one session (possibly stale or empty).
* @param events - the stored events with `seq >= baseSeq`, in seq order.
* @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
* @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
* supplied event's seq, `baseSeq - 1` for an empty tail) plus the
* refreshed checkpoint rows at that cut, ready for a durable write-back.
*/
restore(checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
```
Types: [Session](session.md) · [SessionEvent](core.md)
Source: [`packages/session/session-projection/src/index.ts:156`](../../packages/session/session-projection/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -0,0 +1,262 @@
# 会话投影
[English](session-projection.md) | 中文
会话投影 seam 是一项[能力 seam](../capability-seams.md):领域 host 插件经由它向客户端载体供给按会话的日志派生状态的当前全量值;三方分别是接口与注册表([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)
## 投影单元
`SessionProjectionMap` 是整条链路host 侧单元、协议块、客户端钩子)的 merge-extensible 类型表;值是协议层 JSON 全量值,渲染归 slot 体系管,永远不归本层。领域为每个 key 贡献一个 `ProjectionDefinition`
```ts type-equiv
/**
* One domain's state-driven computation unit: three pure synchronous
* functions plus declarations — never an opaque getter. The framework drives
* `apply` on every committed session event; the domain holds no
* subscriptions and owns only the mathematics. All three functions MUST be
* synchronous (an async unit would tear the carriers' consistency cut) and
* `state` MUST be plain JSON (the persisted-cache precondition).
*/
interface ProjectionDefinition<K extends keyof SessionProjectionMap, S> {
/** The projection key this unit owns (its `SessionProjectionMap` entry). */
key: K
/** Validates the wire payload (`view` output) before it leaves the host. */
schema: ZodType<SessionProjectionMap[K]>
/**
* State for the empty log.
* @returns the initial state.
*/
init(): S
/**
* Pure transition: previous state + one committed event → next state. A
* unit uninterested in an event MUST return the same state reference — an
* unchanged reference (`Object.is`) produces zero downstream work.
* @param state - the state covering all prior events.
* @param event - the next committed session event.
* @returns the next state (same reference when the event is not the unit's).
*/
apply(state: S, event: SessionEvent): S
/**
* State → wire payload (the read-side projection).
* @param state - the current state.
* @returns the whole current value for this unit's key.
*/
view(state: S): SessionProjectionMap[K]
/**
* Persisted-cache invalidation anchor: bump whenever the state shape or the
* fold semantics change, so persisted `(sessionId, key, ver, seq, val)`
* rows from an older unit are discarded instead of being forward-applied
* into garbage. Non-negative integer.
*/
stateVersion: number
}
```
全量值事件规则是承重结构:携带状态的日志事件携带的是变更后的完整状态,绝不是裸增量——这让每次状态转移始终足够廉价,也让每个被供给的值自描述(对消费方即 last-wins
## 快照与变更流
```ts type-equiv
/**
* One consistent read cut over every registered unit for one session.
* `asOfSeq` is the shared watermark — the seq of the last event every value
* reflects (`-1` for an empty log, mirroring `session/subscribed.lastSeq`).
*/
interface ProjectionSnapshot {
/** Seq of the last event the values reflect; -1 for an empty log. */
asOfSeq: number
/** Whole current value per registered key. */
values: Partial<SessionProjectionMap>
}
```
```ts type-equiv
/**
* Change-feed listener: one unit's value changed for one session. `value` is
* the schema-validated `view` output; `seq` is the unit's watermark at
* emission (the seq of the event that caused the change).
*/
type ProjectionChangeListener = (
session: Session,
key: Extract<keyof SessionProjectionMap, string>,
value: unknown,
seq: number,
) => void
```
`snapshot(session)` 是完全同步的:载体在切出页面切片的同一 tick 内读取它,`asOfSeq` 之所以是一个一致切面正系于此;且每个值在离开前都要经过其单元的 schema 校验(误写成异步的 `view` 会返回 Promise让这道边界解析当场大声失败。变更流对每个已提交事件、每个状态*引用*发生变化的单元各触发一次:`apply` 的同引用纪律就是那道闸门。
## 注册表:`ctx.sessionProjections`
`SessionProjectionRegistry`[签名](#ctxsessionprojections--sessionprojectionregistry))拥有驱动权:一份 `session/event` 订阅、对每个已注册单元的正向 `apply`以及每会话每单元的水位线watermarkcell。cell 惰性构建:在事件流过之后才注册的单元,或比注册表更早的会话,都在首次触达(事件或读取)时从 `init` 出发在内存日志上折叠。注册是一个 effect其 disposer 随调用方 fiber 走:领域插件卸载后,其 key连同缓存的 cell从后续驱动与快照中消失客户端将其读作能力缺失key 重复直接 throw。领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessionprojectioncache--sessionprojectioncache"></a>
### `ctx.sessionProjectionCache` — `SessionProjectionCache`
The persisted projection cache service. Opens the `session_projcache` domain at init, checkpoints live sessions on a throttled write-behind (count/interval triggers from Config) plus two mandatory points — `turn/end` and session disposal (the live-to-cold moment) — and serves the cold-read ladder: cached row, persistence `readFrom` tail, registry `restore`, durable write-back. Every durable write is fail-soft: failures log a warning and the cache self-heals on the next write or cold read.
```ts cordis-catalog
/**
* The zero-I/O listing read: whole values viewed straight from the stored
* rows (version-matching keys only), each cut carried with its watermark
* so a client value store can seed under its higher-seq-wins rule — as
* stale as the last durable checkpoint but never wrong, and never from an
* unrelated log (the caller's header is the identity witness). Fresher
* paths (the history tail baseline, {@link coldSnapshot}) supersede these
* values whenever a session is actually opened.
* @param meta - the listed session's header (identity witness; no log read).
* @returns the cut (`asOfSeq` = lowest served-row watermark), or
* `undefined` when no usable row exists for this lifecycle.
*/
cachedSnapshot(meta: SessionHeader): ProjectionSnapshot | undefined
/**
* Durably checkpoint one live session NOW (both mandatory points call
* this; tests and carriers may too). The registry cut is snapshotted at
* this boundary (states are live references), then the whole record is
* replaced. NOT fail-soft — callers on the fail-soft paths contain it.
* @param session - the live session to checkpoint.
* @returns resolution after durability and event emission.
*/
async write(session: Session): Promise<void>
/**
* Cold-read one persisted session's projections with zero full-log load:
* cached rows + a persistence `readFrom` tail from the registry's restore
* floor, refolded by the registry and written back (fail-soft) so the next
* cold read starts closer. A cache row invalidated by a shrunk log
* (crash-repair truncation) triggers one full re-read from seq 0 — the
* ladder's slow rung, still no crash. Rejects when the session has no
* persisted log (`not found` from the persistence seam).
* @param id - the persisted session to read.
* @param signal - optional cancellation for the persistence reads.
* @returns the snapshot cut at the stored log end.
*/
async coldSnapshot(id: SessionId, signal?: AbortSignal): Promise<ProjectionSnapshot>
```
Types: [Session](session.md) · [SessionHeader](persistence.md) · [SessionId](core.md)
Source: [`packages/session/session-projection-cache/src/index.ts:71`](../../packages/session/session-projection-cache/src/index.ts)
<a id="ctxsessionprojections--sessionprojectionregistry"></a>
### `ctx.sessionProjections` — `SessionProjectionRegistry`
`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive), and a changed state reference notifies the change feed with the schema-validated view. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. Duplicate keys throw. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected.
```ts cordis-catalog
/**
* Register one domain's unit. The registration is an effect on the calling
* context's fiber: disposing the fiber (or calling the returned disposer)
* removes the key — and the unit's cached cells — from subsequent drives
* and snapshots.
* @param definition - key, boundary schema, pure unit functions, and stateVersion.
* @returns the exact disposer that unregisters this unit.
*/
register<K extends keyof SessionProjectionMap, S>(definition: ProjectionDefinition<K, S>): () => void
/**
* Subscribe to the change feed. The registration is an effect on the
* calling context's fiber.
* @param listener - called once per unit whose state reference changed, per committed event.
* @returns the exact disposer that unsubscribes.
*/
onChanged(listener: ProjectionChangeListener): () => void
/**
* One consistent cut over every registered unit for one session, read from
* the watermark cache (missing cells fold lazily over the in-memory log).
* Fully synchronous — every value and `asOfSeq` reflect the same log
* position. Each value passes its unit's schema before leaving.
* @param session - the session whose projection values are read.
* @returns the snapshot; `values` is empty when no unit is registered.
*/
snapshot(session: Session): ProjectionSnapshot
/**
* State-level checkpoint of every registered unit for one session, read
* from the watermark cache (missing cells fold lazily over the in-memory
* log). This is the write side of the persisted projection cache: the
* returned rows are the `(key → {ver, seq, val})` part of the durable
* `(sessionId, key, ver, seq, val)`
* rows. Every `val` is a DETACHED structured clone — never the live
* cell reference: the watermark cache is this registry's authoritative
* mutable state, and a caller reaching the live reference could corrupt
* every subsequent snapshot and frame through it (plain JSON by the unit
* contract, so the clone is total).
* @param session - the session whose unit states are checkpointed.
* @returns one row per registered key; empty when no unit is registered.
*/
checkpoint(session: Session): ProjectionCheckpoint
/**
* The stored seq a {@link restore} tail read over `checkpoint` must start
* at: one event BELOW the lowest usable watermark (a row is usable when
* its `ver` matches the live unit's `stateVersion`; an absent or mismatched row
* pulls the floor to `0` — that key must refold the full log). The
* one-below anchor is load-bearing: the tail then proves how far the
* stored log still extends, so {@link restore} can detect a log that
* shrank below a row's watermark (crash-repair truncation) instead of
* serving the stale row as current — an empty tail read from the anchor
* yields an end below every watermark and the restore rejects for a full
* re-read.
* @param checkpoint - persisted rows for one session (possibly stale or empty).
* @returns the seq to hand the persistence `readFrom`, or `undefined`
* when no unit is registered (no read needed — {@link restore} would
* serve empty values regardless).
*/
restoreFloor(checkpoint: ProjectionCheckpoint): number | undefined
/**
* View a checkpoint's rows without any log read: for every registered
* unit whose row's `ver` matches, serve the schema-validated
* `view` of the stored state; mismatched or absent rows leave their key
* absent (a cold or listing consumer treats it as not-yet-available and a
* fuller read path refolds it). The zero-I/O rung of the read ladder —
* values are as stale as their rows, never wrong.
* @param checkpoint - persisted rows for one session (possibly stale or empty).
* @returns whole values per key with a usable row; empty when none.
*/
viewCheckpoint(checkpoint: ProjectionCheckpoint): Partial<SessionProjectionMap>
/**
* Cold read: fold every registered unit over a stored log suffix, seeding
* each from its checkpoint row when usable — the one read recipe (cached
* state + forward tail replay + `view`) applied without a live `Session`.
* Call with the events returned by a persistence
* `readFrom(id, restoreFloor(checkpoint))` and that same floor as
* `baseSeq`; the floor's one-below anchor makes the supplied end honest,
* so a shrunk log is detected here. A row is usable iff its
* `ver` matches the live unit's `stateVersion`, it does not predate `baseSeq`
* (`seq >= baseSeq - 1`), and it does not claim events past the
* supplied end (`seq <= endSeq`); an unusable row is discarded
* and its key refolds from `init` — which is only sound over the full
* log, so a discarded row with `baseSeq > 0` throws (the caller re-reads
* from seq 0, e.g. after a crash-repair truncation shrank the log below
* a row's watermark).
* @param checkpoint - persisted rows for one session (possibly stale or empty).
* @param events - the stored events with `seq >= baseSeq`, in seq order.
* @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
* @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
* supplied event's seq, `baseSeq - 1` for an empty tail) plus the
* refreshed checkpoint rows at that cut, ready for a durable write-back.
*/
restore(checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint }
```
Types: [Session](session.md) · [SessionEvent](core.md)
Source: [`packages/session/session-projection/src/index.ts:156`](../../packages/session/session-projection/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/session-query.md
session-query.md: e7514dd6c3bc20a07395663bff40ce65e1363b78
session-query.zh.md: 4c3dd4d435dbd8a20fbd4db5da1a7d649c2e6d0b
# pnpm run verify-translation-pairing --write docs/subsystems/session-query.md
session-query.md: 54ae1eaf34a17d4979a621e4fc310780b425364e
session-query.zh.md: b770514757c30aa5d87b25e5d280b1ce6710cd9c

View File

@@ -354,3 +354,141 @@ type SessionQueryErrorCode =
| 'SESSION_QUERY_STALE_CURSOR'
| 'SESSION_QUERY_SOURCE_CONFLICT'
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessionquery--sessionqueryservice-abstract-seam"></a>
### `ctx.sessionQuery` — `SessionQueryService` (abstract seam)
Unified live-preferred session query service.
Exact reads, filters, and traces are backend-independent concrete behavior. A backend implements full-text observation, reconciliation, ranking, cursor generations, and query execution on the same `ctx.sessionQuery` service.
```ts cordis-catalog
/**
* Search the live-preferred logical corpus and group by session.
* @param request - query text, metadata filters, page size, and cursor.
* @param exec - optional cancellation control.
* @returns session hits ranked by their strongest matching event.
*/
abstract searchSessions( request: SessionSearchRequest, exec?: SessionSearchExecContext, ): Promise<SessionSearchPage<SessionSearchHit>>
/**
* Search events within one live-preferred logical session.
* @param request - target session, query text, filters, page size, and cursor.
* @param exec - optional cancellation control.
* @returns matching event hits and their target header from one indexed generation.
*/
abstract searchEvents( request: SessionEventSearchRequest, exec?: SessionSearchExecContext, ): Promise<SessionEventSearchPage>
/**
* List the complete logical corpus using live-preferred records.
* @param signal - optional cancellation for persistence listing.
* @returns deterministic newest-first cloned session records.
*/
listSessions(signal?: AbortSignal): Promise<SessionRecord[]>
/**
* Read and replay-validate one complete logical session log without making it live.
* @param sessionId - live or persisted session id to read.
* @returns cloned header and complete raw event log from one observation.
* @throws when persistence, header compatibility, or replay validation fails.
*/
async readSession(sessionId: SessionId): Promise<SessionLogSnapshot>
/**
* Filter the complete logical corpus with provider-independent predicates.
* @param filters - ANDed session metadata and availability clauses.
* @param signal - optional cancellation for persistence listing.
* @returns matching cloned records in deterministic newest-first order.
*/
async filterSessions( filters: readonly SessionResultFilter[], signal?: AbortSignal, ): Promise<SessionRecord[]>
/**
* Fold the latest log-backed title from one live-preferred logical session.
* @param sessionId - live or persisted session id to read.
* @param signal - optional cancellation for source resolution and title folding.
* @returns latest title snapshot, or `undefined` when the log has no title event.
*/
async readTitle( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionTitleSnapshot | undefined>
/**
* Fold the latest title and return its source header from one corpus observation.
* @param sessionId - live or persisted session id to read.
* @param signal - optional cancellation for source resolution and title folding.
* @returns cloned source header and optional latest title snapshot.
*/
async readTitleSnapshot( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionTitleObservation>
/**
* Fold titles for unique sessions from one cancellable corpus observation.
*
* Results preserve first-occurrence input order. Operational failures stay
* isolated per session, while cancellation rejects the complete operation.
* @param sessionIds - live or persisted session ids to observe.
* @param signal - optional cancellation shared by all source reads.
* @returns one fulfilled or rejected result per unique requested id.
*/
async readTitleSnapshots( sessionIds: readonly SessionId[], signal?: AbortSignal, ): Promise<SessionTitleObservationResult[]>
/**
* List lightweight raw-log event records for one logical session.
* @param sessionId - live-preferred session id to read.
* @returns event records in ascending seq order.
*/
async listEvents(sessionId: SessionId): Promise<SessionEventRecord[]>
/**
* Scan first-party semantic event documents with provider-independent filters.
* @param sessionId - live-preferred session id to scan.
* @param filters - ANDed metadata and literal-text predicates.
* @returns matching semantic documents in ascending seq order.
*/
async filterEvents( sessionId: SessionId, filters: readonly SessionEventResultFilter[], ): Promise<SessionEventSearchDocument[]>
/**
* Read one session's complete current model surface from one corpus observation.
* @param sessionId - live-preferred session id to read.
* @returns cloned header, current surface, and raw-log capture boundary.
* @throws when source resolution fails or the session surface is invalid.
*/
async readSurface(sessionId: SessionId): Promise<SessionSurfaceSnapshot>
/**
* Trace known ancestry and descendants from one corpus observation.
* @param sessionId - logical session id to trace.
* @param signal - optional cancellation for persistence listing.
* @returns a complete lineage or an explicit unresolved parent boundary.
* @throws when corpus resolution fails, the target is absent, or its known ancestry cycles.
*/
async traceSession(sessionId: SessionId, signal?: AbortSignal): Promise<SessionLineageTrace>
/**
* Trace one event's direct positional and provenance relationships.
* @param request - target session id and event seq.
* @param signal - optional cancellation for persisted source resolution.
* @returns source header, direct links, and the target's positional replacement chain.
* @throws when source resolution fails, the target is absent, or surface/provenance validation fails.
*/
async traceEvent(request: SessionEventTraceRequest, signal?: AbortSignal): Promise<SessionEventTraceObservation>
/**
* Read one full event plus a bounded raw-log context window.
* @param request - target session/seq and context sizes.
* @param signal - optional cancellation for persisted source resolution.
* @returns cloned target and neighboring events.
*/
async readEvent(request: SessionEventReadRequest, signal?: AbortSignal): Promise<SessionEventWindow>
```
Types: [SessionId](core.md) · [SessionTitleSnapshot](session-title.md)
Source: [`packages/session-query/session-query/src/index.ts:81`](../../packages/session-query/session-query/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -354,3 +354,141 @@ type SessionQueryErrorCode =
| 'SESSION_QUERY_STALE_CURSOR'
| 'SESSION_QUERY_SOURCE_CONFLICT'
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessionquery--sessionqueryservice-abstract-seam"></a>
### `ctx.sessionQuery` — `SessionQueryService` (abstract seam)
Unified live-preferred session query service.
Exact reads, filters, and traces are backend-independent concrete behavior. A backend implements full-text observation, reconciliation, ranking, cursor generations, and query execution on the same `ctx.sessionQuery` service.
```ts cordis-catalog
/**
* Search the live-preferred logical corpus and group by session.
* @param request - query text, metadata filters, page size, and cursor.
* @param exec - optional cancellation control.
* @returns session hits ranked by their strongest matching event.
*/
abstract searchSessions( request: SessionSearchRequest, exec?: SessionSearchExecContext, ): Promise<SessionSearchPage<SessionSearchHit>>
/**
* Search events within one live-preferred logical session.
* @param request - target session, query text, filters, page size, and cursor.
* @param exec - optional cancellation control.
* @returns matching event hits and their target header from one indexed generation.
*/
abstract searchEvents( request: SessionEventSearchRequest, exec?: SessionSearchExecContext, ): Promise<SessionEventSearchPage>
/**
* List the complete logical corpus using live-preferred records.
* @param signal - optional cancellation for persistence listing.
* @returns deterministic newest-first cloned session records.
*/
listSessions(signal?: AbortSignal): Promise<SessionRecord[]>
/**
* Read and replay-validate one complete logical session log without making it live.
* @param sessionId - live or persisted session id to read.
* @returns cloned header and complete raw event log from one observation.
* @throws when persistence, header compatibility, or replay validation fails.
*/
async readSession(sessionId: SessionId): Promise<SessionLogSnapshot>
/**
* Filter the complete logical corpus with provider-independent predicates.
* @param filters - ANDed session metadata and availability clauses.
* @param signal - optional cancellation for persistence listing.
* @returns matching cloned records in deterministic newest-first order.
*/
async filterSessions( filters: readonly SessionResultFilter[], signal?: AbortSignal, ): Promise<SessionRecord[]>
/**
* Fold the latest log-backed title from one live-preferred logical session.
* @param sessionId - live or persisted session id to read.
* @param signal - optional cancellation for source resolution and title folding.
* @returns latest title snapshot, or `undefined` when the log has no title event.
*/
async readTitle( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionTitleSnapshot | undefined>
/**
* Fold the latest title and return its source header from one corpus observation.
* @param sessionId - live or persisted session id to read.
* @param signal - optional cancellation for source resolution and title folding.
* @returns cloned source header and optional latest title snapshot.
*/
async readTitleSnapshot( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionTitleObservation>
/**
* Fold titles for unique sessions from one cancellable corpus observation.
*
* Results preserve first-occurrence input order. Operational failures stay
* isolated per session, while cancellation rejects the complete operation.
* @param sessionIds - live or persisted session ids to observe.
* @param signal - optional cancellation shared by all source reads.
* @returns one fulfilled or rejected result per unique requested id.
*/
async readTitleSnapshots( sessionIds: readonly SessionId[], signal?: AbortSignal, ): Promise<SessionTitleObservationResult[]>
/**
* List lightweight raw-log event records for one logical session.
* @param sessionId - live-preferred session id to read.
* @returns event records in ascending seq order.
*/
async listEvents(sessionId: SessionId): Promise<SessionEventRecord[]>
/**
* Scan first-party semantic event documents with provider-independent filters.
* @param sessionId - live-preferred session id to scan.
* @param filters - ANDed metadata and literal-text predicates.
* @returns matching semantic documents in ascending seq order.
*/
async filterEvents( sessionId: SessionId, filters: readonly SessionEventResultFilter[], ): Promise<SessionEventSearchDocument[]>
/**
* Read one session's complete current model surface from one corpus observation.
* @param sessionId - live-preferred session id to read.
* @returns cloned header, current surface, and raw-log capture boundary.
* @throws when source resolution fails or the session surface is invalid.
*/
async readSurface(sessionId: SessionId): Promise<SessionSurfaceSnapshot>
/**
* Trace known ancestry and descendants from one corpus observation.
* @param sessionId - logical session id to trace.
* @param signal - optional cancellation for persistence listing.
* @returns a complete lineage or an explicit unresolved parent boundary.
* @throws when corpus resolution fails, the target is absent, or its known ancestry cycles.
*/
async traceSession(sessionId: SessionId, signal?: AbortSignal): Promise<SessionLineageTrace>
/**
* Trace one event's direct positional and provenance relationships.
* @param request - target session id and event seq.
* @param signal - optional cancellation for persisted source resolution.
* @returns source header, direct links, and the target's positional replacement chain.
* @throws when source resolution fails, the target is absent, or surface/provenance validation fails.
*/
async traceEvent(request: SessionEventTraceRequest, signal?: AbortSignal): Promise<SessionEventTraceObservation>
/**
* Read one full event plus a bounded raw-log context window.
* @param request - target session/seq and context sizes.
* @param signal - optional cancellation for persisted source resolution.
* @returns cloned target and neighboring events.
*/
async readEvent(request: SessionEventReadRequest, signal?: AbortSignal): Promise<SessionEventWindow>
```
Types: [SessionId](core.md) · [SessionTitleSnapshot](session-title.md)
Source: [`packages/session-query/session-query/src/index.ts:81`](../../packages/session-query/session-query/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/session-reference.md
session-reference.md: 5375677f6a1748909743ca76d5191cb9e736a40a
session-reference.zh.md: 3ff4a1719926bda0a9111482a7778a8c94553370
session-reference.md: 60e29c56ea372e100009b94caedea5327308432a
session-reference.zh.md: 3ae9264abc15848646e5397dfcbeff764c038918

View File

@@ -65,3 +65,44 @@ type SessionReferenceErrorCode =
| 'SESSION_REFERENCE_BUDGET_EXCEEDED'
| 'SESSION_REFERENCE_CANCELLED'
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessionreferences--sessionreferenceservice"></a>
### `ctx.sessionReferences` — `SessionReferenceService`
Exact-read consumer that prepares immutable cross-session message context.
```ts cordis-catalog
/**
* List reference candidates, ranked by working-directory affinity.
* @param agent - target agent; self is excluded and its cwd drives ranking.
* @param query - optional case-insensitive session-id/cwd/title substring.
* @param limit - optional positive result cap.
* @param signal - optional cancellation boundary for host autocomplete teardown.
* @returns candidates labeled by latest title or, when absent, session id.
*/
async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>
/**
* Snapshot all references before enqueue and return one aggregated durable context.
* @param agent - target agent; references to it are rejected.
* @param content - already host-normalized readable message content.
* @param references - structured source sessions in mention order.
* @param signal - optional cancellation boundary for host request teardown.
* @returns detached content and optional referenced-session context.
*/
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
```
Types: [Agent](core.md) · [ContentBlock](core.md)
Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -65,3 +65,44 @@ type SessionReferenceErrorCode =
| 'SESSION_REFERENCE_BUDGET_EXCEEDED'
| 'SESSION_REFERENCE_CANCELLED'
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessionreferences--sessionreferenceservice"></a>
### `ctx.sessionReferences` — `SessionReferenceService`
Exact-read consumer that prepares immutable cross-session message context.
```ts cordis-catalog
/**
* List reference candidates, ranked by working-directory affinity.
* @param agent - target agent; self is excluded and its cwd drives ranking.
* @param query - optional case-insensitive session-id/cwd/title substring.
* @param limit - optional positive result cap.
* @param signal - optional cancellation boundary for host autocomplete teardown.
* @returns candidates labeled by latest title or, when absent, session id.
*/
async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>
/**
* Snapshot all references before enqueue and return one aggregated durable context.
* @param agent - target agent; references to it are rejected.
* @param content - already host-normalized readable message content.
* @param references - structured source sessions in mention order.
* @param signal - optional cancellation boundary for host request teardown.
* @returns detached content and optional referenced-session context.
*/
async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>
```
Types: [Agent](core.md) · [ContentBlock](core.md)
Source: [`packages/context/session-reference/src/index.ts:70`](../../packages/context/session-reference/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/session-title.md
session-title.md: 7bced67f07df3f00471f02b766922cdc3e4fd29e
session-title.zh.md: e8e501d3aafee49395da3d9a8efd4e91a724f09e
session-title.md: 3a2df284f2af14def284e502f4e37daaf4d7a262
session-title.zh.md: 3a74baa9005459b35b658df95bdd7b57256b3e2e

View File

@@ -144,3 +144,61 @@ interface SessionTitleProvider {
generate(request: SessionTitleProviderRequest): Promise<SessionTitleProviderResult>
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessiontitle--sessiontitleservice"></a>
### `ctx.sessionTitle` — `SessionTitleService`
Log-backed title fold plus asynchronous fallback generation.
```ts cordis-catalog
/**
* Read the latest folded title from one live or replayed session.
* @param session - session whose log is the title source of truth.
* @returns latest title snapshot, or `undefined` before eligible input.
*/
get(session: Session): SessionTitleSnapshot | undefined
/**
* Accept an explicit user title. Appends a `session/title` event with the
* `user` source, which pins the title: in-flight automatic generation is
* superseded and later user messages schedule none (an explicit
* {@link SessionTitleService.refresh} remains the deliberate unpin).
* @param session - exact live session to rename.
* @param title - raw user input; normalized before acceptance.
* @returns the accepted title snapshot.
* @throws {SessionTitleInvalidError} when the title normalizes to empty.
* @throws {Error} when the session is not live or the service is disposed.
*/
rename(session: Session, title: string): SessionTitleSnapshot
/**
* Explicitly retry the registered provider, or materialize the built-in
* fallback when no provider is registered.
* @param session - exact live session to refresh.
* @param signal - optional caller cancellation.
* @returns latest accepted title, or `undefined` when no eligible text exists.
*/
async refresh(session: Session, signal?: AbortSignal): Promise<SessionTitleSnapshot | undefined>
/**
* Register the sole optional title provider. Disposal aborts its pending and
* active work before another provider may register.
* @param provider - provider identity, cadence, and generation function.
* @returns exact Cordis effect disposer, which settles after active calls quiesce.
*/
register(provider: SessionTitleProvider): () => Promise<void>
```
Types: [Session](session.md)
Source: [`packages/session/session-title/src/index.ts:261`](../../packages/session/session-title/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -144,3 +144,61 @@ interface SessionTitleProvider {
generate(request: SessionTitleProviderRequest): Promise<SessionTitleProviderResult>
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessiontitle--sessiontitleservice"></a>
### `ctx.sessionTitle` — `SessionTitleService`
Log-backed title fold plus asynchronous fallback generation.
```ts cordis-catalog
/**
* Read the latest folded title from one live or replayed session.
* @param session - session whose log is the title source of truth.
* @returns latest title snapshot, or `undefined` before eligible input.
*/
get(session: Session): SessionTitleSnapshot | undefined
/**
* Accept an explicit user title. Appends a `session/title` event with the
* `user` source, which pins the title: in-flight automatic generation is
* superseded and later user messages schedule none (an explicit
* {@link SessionTitleService.refresh} remains the deliberate unpin).
* @param session - exact live session to rename.
* @param title - raw user input; normalized before acceptance.
* @returns the accepted title snapshot.
* @throws {SessionTitleInvalidError} when the title normalizes to empty.
* @throws {Error} when the session is not live or the service is disposed.
*/
rename(session: Session, title: string): SessionTitleSnapshot
/**
* Explicitly retry the registered provider, or materialize the built-in
* fallback when no provider is registered.
* @param session - exact live session to refresh.
* @param signal - optional caller cancellation.
* @returns latest accepted title, or `undefined` when no eligible text exists.
*/
async refresh(session: Session, signal?: AbortSignal): Promise<SessionTitleSnapshot | undefined>
/**
* Register the sole optional title provider. Disposal aborts its pending and
* active work before another provider may register.
* @param provider - provider identity, cadence, and generation function.
* @returns exact Cordis effect disposer, which settles after active calls quiesce.
*/
register(provider: SessionTitleProvider): () => Promise<void>
```
Types: [Session](session.md)
Source: [`packages/session/session-title/src/index.ts:261`](../../packages/session/session-title/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/session.md
session.md: 5d54b03df8ec3345e8bf04702f242e3aacf8ec39
session.zh.md: 5a867cfe302bf02994bfe5a5a704bed55222e8ea
session.md: 28b3f959b47a24373860c2334f9273286d072c95
session.zh.md: 5870265f55892231700b65ac6ed69d827a0ab1ec

View File

@@ -343,7 +343,7 @@ interface SurfaceFoldResult {
## `Session` public API
The body-stripped declaration keeps the plain class's detached factory, state accessors, append boundary, and history projections synchronized with source. Store operations remain in the generated [`ctx.sessions` service catalog](../cordis-catalog/services.md#ctxsessions--sessionstore).
The body-stripped declaration keeps the plain class's detached factory, state accessors, append boundary, and history projections synchronized with source. Store operations remain in the generated [`ctx.sessions` section](#ctxsessions--sessionstore).
```ts public-api
/**
@@ -586,3 +586,247 @@ The hook bridges' `hook/invoked` / `hook/result` provenance pairs (from `@deepse
What a persistence backend relies on: the durable log persists every event losslessly, **including** `assistant/chunk` — `seq` must stay contiguous, so chunks cannot be filtered out of the canonical log. A backend may choose its own storage encoding for an event batch as long as `load` returns the exact appended events (the JSONL backend's default packed chunk rows are such an encoding — see [persistence.md](persistence.md)). All `event.data` must be JSON-serializable; `Session.append` enforces this at the source (throwing on non-serializable data), so a bad event never enters the log and `session.events` always equals what a backend can persist. Adding an event type that carries non-serializable data, corrupts core execution nesting, or violates its owner's declared relation is a breaking change to the on-disk format.
The backends that consume this contract are on [persistence.md](persistence.md).
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessions--sessionstore"></a>
### `ctx.sessions` — `SessionStore`
In-memory session store (`ctx.sessions`).
Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose.
```ts cordis-catalog
/**
* Create a session owned by the calling fiber: disposing that fiber stops
* event notification and removes the session from the store. `options.seed`
* populates the session with a copy of those events (replay/fork);
* `options.meta` attaches creation metadata (validated absolute `cwd`, seed
* and parent lineage, and delegation depth) as the immutable
* {@link SessionHeader} (the store fills `version`/`id`/`createdAt`).
*
* For an agent whose session must be torn down IN ORDER with its loop (so the
* loop's final events are published before the store attachment ends), do NOT use this
* — fold the session lifecycle into the agent's own effect via
* {@link prepare} + {@link enter} + {@link announce} (see
* `dsh-agent-loop`'s creation transaction).
*
* @param id - the session id; omitted, the store mints `session-<n>`.
* @param options - seed events and/or creation metadata for the header.
* @returns the live session, already entered and announced.
* @throws if a session with `id` already exists, metadata is not a plain
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
* non-absolute path (storage backends key directories off it).
*/
create(id?: SessionId, options?: CreateSessionOptions): Session
/**
* Build a session WITHOUT entering it into the store — validate the id/cwd and
* construct the {@link Session} (with its immutable {@link SessionHeader}).
* Pairs with {@link enter} + {@link announce}: a caller that owns a composite
* `ctx.effect` (the agent factory) folds the session lifecycle into that ONE
* effect so a fiber unload tears the session + agent down as a single ORDERED
* chain rather than as racing sibling effects — which would remove the publication hooks
* before the driver's closing events commit, dropping them.
*
* @param id - the session id; omitted, the store mints `session-<n>`.
* @param options - seed events and/or creation metadata for the header. With
* `seedSource: 'persistence'`, metadata and events must be fresh detached
* graphs whose ownership transfers to this call: they are validated and
* frozen in place through {@link Session.fromRestore}, so the caller must
* retain no mutable aliases.
* @returns the constructed session, NOT yet in the store.
* @throws if a session with `id` already exists, metadata is not a plain
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
* non-absolute path.
*/
prepare(id?: SessionId, options?: PrepareSessionOptions): Session
/**
* Enter a {@link prepare}d session into the store: install the module-private
* append publication hooks and add it to the store. Returns the DETACH
* disposer (hooks + store removal). Does NOT emit `session/created` —
* the caller yields this disposer inside its effect and THEN calls
* {@link announce}, so a throwing `session/created` listener rolls the attach
* back instead of leaking it.
*
* Re-checks the id for a duplicate: `prepare` and `enter` are public
* cross-package primitives and a caller may interleave arbitrary work (or
* another create) between them, so a stale prepared session must NOT overwrite
* a live store entry of the same id — its detach disposer would later delete
* the REAL session. The {@link create} convenience and the agent factory call
* the two back-to-back so they never trip this, but the public seam cannot
* assume that.
*
* @param session - a {@link prepare}d session not yet in the store.
* @returns the detach disposer (publication hooks + store removal). When called from
* a synchronous `session/created` listener, removal and disposal wait until
* that creation dispatch unwinds.
* @throws if a session with this id is already in the store.
*/
enter(session: Session): () => void
/** Emit `session/created` exactly once for an {@link enter}ed session (with
* the carrier {@link enter} captured). Separate from {@link enter} so the
* caller can yield the detach disposer first (rollback safety — see
* {@link enter}).
* @param session - the entered session to announce to listeners.
* @throws if the session is not live or its announcement already began,
* including a reentrant call from a creation listener. */
announce(session: Session): void
/**
* Dispatch the awaited `session/flush` durability checkpoint for `session`,
* with the carrier captured at {@link enter}. THE flush entry point: the
* store owns the carrier, so callers (the checkpoint policy's per-request
* barrier, goal-session's idle checkpoint, teardown drains, and consumers
* that flush themselves before reading storage) must come through here
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner,
* one spelling, and the scoped-dispatch invariant can pin it.
* @param session - the session whose buffered events must reach durable storage.
* @returns whether at least one durability listener participated, after every
* listener has settled successfully.
* @throws the first registered listener failure after every listener settles.
*/
async flush(session: Session): Promise<boolean>
/**
* Look up a live session.
* @param id - the session id to look up.
* @returns the session, or undefined when no live session has that id.
*/
get(id: SessionId): Session | undefined
/**
* All live sessions, in creation order.
* @returns a fresh array; mutating it does not affect the store.
*/
list(): Session[]
/**
* Create a live child session from a stable prefix of a live source.
* `boundary` is an inclusive source event seq; omitted means the source's
* current last event. The selected slice may end with a between-turn event
* but must not end inside an open turn.
*
* @param source - Live source session object or id.
* @param boundary - Inclusive source event seq to fork through; omitted means
* the source's current last event, and omitted on an empty source forks an
* empty child.
* @param childSessionId - Optional child session id; omitted delegates to
* `SessionStore`'s id policy.
* @returns The created live child session.
*/
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
```
Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md)
Source: [`packages/core/session/src/index.ts:807`](../../packages/core/session/src/index.ts)
<a id="session-events"></a>
### `session/*` events
<a id="sessioncreated--emit"></a>
#### `session/created` — emit
Creation announcement during session publication. A synchronous throw vetoes and rolls back with a paired disposal; detach requested during dispatch is deferred. A returned-promise rejection is logged but cannot retroactively veto this synchronous boundary. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only sessions entered through that agent's context.
```ts cordis-catalog
/**
* Creation announcement during session publication. A synchronous throw vetoes and rolls
* back with a paired disposal; detach requested during dispatch is deferred.
* A returned-promise rejection is logged but cannot retroactively veto this
* synchronous boundary.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
* receive only sessions entered through that agent's context.
* @param session - the session just entered and announced.
* @dshScopeScan unsupported
* @mode emit
*/
'session/created'(this: Scoped<Session>, session: Session): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:74`](../../packages/core/session/src/index.ts)
<a id="sessiondisposed--emit"></a>
#### `session/disposed` — emit
Emitted once when an announced session leaves the store, including publication rollback, but never for an entry whose creation announcement did not begin. Listener failures are logged and contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope.
```ts cordis-catalog
/**
* Emitted once when an announced session leaves the store, including
* publication rollback, but never for an entry whose creation announcement
* did not begin. Listener failures are logged and contained.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope.
* @param session - the session that is no longer live in the store.
* @dshScopeScan unsupported
* @mode emit
*/
'session/disposed'(this: Scoped<Session>, session: Session): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:84`](../../packages/core/session/src/index.ts)
<a id="sessionevent--emit"></a>
#### `session/event` — emit
Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context.
```ts cordis-catalog
/**
* Post-commit, fire-and-forget append feed. The listener snapshot resolves
* before the log push, but callbacks run after it; observer failures are
* logged and contained without making the committed append fail.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
* receive only events from sessions entered through that agent's context.
* @param session - the session whose log grew.
* @param event - the appended event, exactly as recorded.
* @dshScopeScan unsupported
* @mode emit
*/
'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void
```
Types: [Scoped](scope.md) · [SessionEvent](core.md)
Source: [`packages/core/session/src/index.ts:96`](../../packages/core/session/src/index.ts)
<a id="sessionflush--parallel"></a>
#### `session/flush` — parallel
Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the session's owner scope.
```ts cordis-catalog
/**
* Awaited parallel durability checkpoint: every listener runs and the
* caller awaits all of them, with no waterfall veto. Scope-filtered dispatch
* (`@deepseek-ai/dsh-scope`) reuses the session's owner scope.
* @param session - the session whose buffered events must reach durable storage.
* @dshScopeScan unsupported
* @mode parallel
*/
'session/flush'(this: Scoped<Session>, session: Session): Promise<void> | void
```
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:105`](../../packages/core/session/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -345,7 +345,7 @@ interface SurfaceFoldResult {
## `Session` 公共 API
去除方法体的声明与源码中的普通类保持同步,覆盖其脱离态工厂、状态访问器、追加边界和历史投影。存储操作仍由生成的 [`ctx.sessions` 服务目录](../cordis-catalog/services.md#ctxsessions--sessionstore)记录。
去除方法体的声明与源码中的普通类保持同步,覆盖其脱离态工厂、状态访问器、追加边界和历史投影。存储操作仍由生成的 [`ctx.sessions` 小节](#ctxsessions--sessionstore)记录。
```ts public-api
/**
@@ -590,3 +590,247 @@ interface TurnEndReasonMap {
持久化后端依赖的契约如下:持久日志无损保存每个事件,**包括** `assistant/chunk``seq` 必须连续,因此不能从规范日志中过滤分片。后端可以为事件批次选择自己的存储编码,只要 `load` 返回与追加时完全一致的事件即可JSONL 后端默认启用的打包分片行就是此类编码;见 [persistence.md](persistence.md))。所有 `event.data` 都必须可序列化为 JSON`Session.append` 会从源头强制这一要求(遇到不可序列化数据时抛出),因此错误事件绝不会进入日志,`session.events` 始终与后端可持久化的内容一致。新增会携带不可序列化数据、破坏核心执行嵌套或违反事件所有方声明关系的事件类型,都会构成磁盘格式的破坏性变更。
消费此契约的后端见 [persistence.md](persistence.md)。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsessions--sessionstore"></a>
### `ctx.sessions` — `SessionStore`
In-memory session store (`ctx.sessions`).
Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose.
```ts cordis-catalog
/**
* Create a session owned by the calling fiber: disposing that fiber stops
* event notification and removes the session from the store. `options.seed`
* populates the session with a copy of those events (replay/fork);
* `options.meta` attaches creation metadata (validated absolute `cwd`, seed
* and parent lineage, and delegation depth) as the immutable
* {@link SessionHeader} (the store fills `version`/`id`/`createdAt`).
*
* For an agent whose session must be torn down IN ORDER with its loop (so the
* loop's final events are published before the store attachment ends), do NOT use this
* — fold the session lifecycle into the agent's own effect via
* {@link prepare} + {@link enter} + {@link announce} (see
* `dsh-agent-loop`'s creation transaction).
*
* @param id - the session id; omitted, the store mints `session-<n>`.
* @param options - seed events and/or creation metadata for the header.
* @returns the live session, already entered and announced.
* @throws if a session with `id` already exists, metadata is not a plain
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
* non-absolute path (storage backends key directories off it).
*/
create(id?: SessionId, options?: CreateSessionOptions): Session
/**
* Build a session WITHOUT entering it into the store — validate the id/cwd and
* construct the {@link Session} (with its immutable {@link SessionHeader}).
* Pairs with {@link enter} + {@link announce}: a caller that owns a composite
* `ctx.effect` (the agent factory) folds the session lifecycle into that ONE
* effect so a fiber unload tears the session + agent down as a single ORDERED
* chain rather than as racing sibling effects — which would remove the publication hooks
* before the driver's closing events commit, dropping them.
*
* @param id - the session id; omitted, the store mints `session-<n>`.
* @param options - seed events and/or creation metadata for the header. With
* `seedSource: 'persistence'`, metadata and events must be fresh detached
* graphs whose ownership transfers to this call: they are validated and
* frozen in place through {@link Session.fromRestore}, so the caller must
* retain no mutable aliases.
* @returns the constructed session, NOT yet in the store.
* @throws if a session with `id` already exists, metadata is not a plain
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
* non-absolute path.
*/
prepare(id?: SessionId, options?: PrepareSessionOptions): Session
/**
* Enter a {@link prepare}d session into the store: install the module-private
* append publication hooks and add it to the store. Returns the DETACH
* disposer (hooks + store removal). Does NOT emit `session/created` —
* the caller yields this disposer inside its effect and THEN calls
* {@link announce}, so a throwing `session/created` listener rolls the attach
* back instead of leaking it.
*
* Re-checks the id for a duplicate: `prepare` and `enter` are public
* cross-package primitives and a caller may interleave arbitrary work (or
* another create) between them, so a stale prepared session must NOT overwrite
* a live store entry of the same id — its detach disposer would later delete
* the REAL session. The {@link create} convenience and the agent factory call
* the two back-to-back so they never trip this, but the public seam cannot
* assume that.
*
* @param session - a {@link prepare}d session not yet in the store.
* @returns the detach disposer (publication hooks + store removal). When called from
* a synchronous `session/created` listener, removal and disposal wait until
* that creation dispatch unwinds.
* @throws if a session with this id is already in the store.
*/
enter(session: Session): () => void
/** Emit `session/created` exactly once for an {@link enter}ed session (with
* the carrier {@link enter} captured). Separate from {@link enter} so the
* caller can yield the detach disposer first (rollback safety — see
* {@link enter}).
* @param session - the entered session to announce to listeners.
* @throws if the session is not live or its announcement already began,
* including a reentrant call from a creation listener. */
announce(session: Session): void
/**
* Dispatch the awaited `session/flush` durability checkpoint for `session`,
* with the carrier captured at {@link enter}. THE flush entry point: the
* store owns the carrier, so callers (the checkpoint policy's per-request
* barrier, goal-session's idle checkpoint, teardown drains, and consumers
* that flush themselves before reading storage) must come through here
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner,
* one spelling, and the scoped-dispatch invariant can pin it.
* @param session - the session whose buffered events must reach durable storage.
* @returns whether at least one durability listener participated, after every
* listener has settled successfully.
* @throws the first registered listener failure after every listener settles.
*/
async flush(session: Session): Promise<boolean>
/**
* Look up a live session.
* @param id - the session id to look up.
* @returns the session, or undefined when no live session has that id.
*/
get(id: SessionId): Session | undefined
/**
* All live sessions, in creation order.
* @returns a fresh array; mutating it does not affect the store.
*/
list(): Session[]
/**
* Create a live child session from a stable prefix of a live source.
* `boundary` is an inclusive source event seq; omitted means the source's
* current last event. The selected slice may end with a between-turn event
* but must not end inside an open turn.
*
* @param source - Live source session object or id.
* @param boundary - Inclusive source event seq to fork through; omitted means
* the source's current last event, and omitted on an empty source forks an
* empty child.
* @param childSessionId - Optional child session id; omitted delegates to
* `SessionStore`'s id policy.
* @returns The created live child session.
*/
fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session
```
Types: [CreateSessionOptions](persistence.md) · [PrepareSessionOptions](persistence.md) · [SessionId](core.md)
Source: [`packages/core/session/src/index.ts:807`](../../packages/core/session/src/index.ts)
<a id="session-events"></a>
### `session/*` events
<a id="sessioncreated--emit"></a>
#### `session/created` — emit
Creation announcement during session publication. A synchronous throw vetoes and rolls back with a paired disposal; detach requested during dispatch is deferred. A returned-promise rejection is logged but cannot retroactively veto this synchronous boundary. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only sessions entered through that agent's context.
```ts cordis-catalog
/**
* Creation announcement during session publication. A synchronous throw vetoes and rolls
* back with a paired disposal; detach requested during dispatch is deferred.
* A returned-promise rejection is logged but cannot retroactively veto this
* synchronous boundary.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
* receive only sessions entered through that agent's context.
* @param session - the session just entered and announced.
* @dshScopeScan unsupported
* @mode emit
*/
'session/created'(this: Scoped<Session>, session: Session): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:74`](../../packages/core/session/src/index.ts)
<a id="sessiondisposed--emit"></a>
#### `session/disposed` — emit
Emitted once when an announced session leaves the store, including publication rollback, but never for an entry whose creation announcement did not begin. Listener failures are logged and contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope.
```ts cordis-catalog
/**
* Emitted once when an announced session leaves the store, including
* publication rollback, but never for an entry whose creation announcement
* did not begin. Listener failures are logged and contained.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope.
* @param session - the session that is no longer live in the store.
* @dshScopeScan unsupported
* @mode emit
*/
'session/disposed'(this: Scoped<Session>, session: Session): void
```
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:84`](../../packages/core/session/src/index.ts)
<a id="sessionevent--emit"></a>
#### `session/event` — emit
Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context.
```ts cordis-catalog
/**
* Post-commit, fire-and-forget append feed. The listener snapshot resolves
* before the log push, but callbacks run after it; observer failures are
* logged and contained without making the committed append fail.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
* receive only events from sessions entered through that agent's context.
* @param session - the session whose log grew.
* @param event - the appended event, exactly as recorded.
* @dshScopeScan unsupported
* @mode emit
*/
'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void
```
Types: [Scoped](scope.md) · [SessionEvent](core.md)
Source: [`packages/core/session/src/index.ts:96`](../../packages/core/session/src/index.ts)
<a id="sessionflush--parallel"></a>
#### `session/flush` — parallel
Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the session's owner scope.
```ts cordis-catalog
/**
* Awaited parallel durability checkpoint: every listener runs and the
* caller awaits all of them, with no waterfall veto. Scope-filtered dispatch
* (`@deepseek-ai/dsh-scope`) reuses the session's owner scope.
* @param session - the session whose buffered events must reach durable storage.
* @dshScopeScan unsupported
* @mode parallel
*/
'session/flush'(this: Scoped<Session>, session: Session): Promise<void> | void
```
Types: [Scoped](scope.md)
Source: [`packages/core/session/src/index.ts:105`](../../packages/core/session/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/settings.md
settings.md: bd01c1d28407af9cab26f624a054a010e25a3ddd
settings.zh.md: 1cb7f8b507f29f2b6876fd48b4c37284df235e53
settings.md: 9256bf9436d2e77093fc8c6a3728b62fc4e8d67f
settings.zh.md: f6d662ebaf35eeb9d046353ea1087b0eb86e7068

View File

@@ -160,3 +160,151 @@ Every committed change — an in-process write or an externally observed provide
/** Origin of one committed settings change. */
type SettingsUpdateSource = 'update' | 'provider'
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsettings--settings-abstract-seam"></a>
### `ctx.settings` — `Settings` (abstract seam)
Abstract settings service. Providers implement raw-document storage (`load`/`persist`) and push external changes through Settings.publish; the base class owns namespace registration, resolution, validation, change detection, and the `settings/updated` commit event.
```ts cordis-catalog
/**
* Prepare the provider's user-editable document for a native editor. File
* providers may materialize an absent document before returning its path;
* non-file providers return undefined.
* @returns the absolute local document path, or undefined for non-file storage.
*/
prepareDocument(): Promise<string | undefined>
/**
* Register a namespace schema and receive its owner scope. The registration
* is an effect on the calling plugin's fiber: disposing that fiber removes
* the namespace and its observers. An invalid stored section fails the
* registration itself — the earliest point where the schema can judge it.
* @param ns - unique namespace; duplicate registration fails loud.
* @param schema - schemastery schema resolving this namespace's value.
* @param options - composition `base` layer and effect timing.
* @returns the owner scope for reads, observation, and updates.
*/
register<T>(ns: SettingsNamespace, schema: z<T>, options?: SettingsRegisterOptions<T>): SettingsScope<T>
/**
* Describe every registered namespace for configuration surfaces, including
* the composition `base` and raw user layers so a form can mark which fields
* the user overrode (presence in `user`) and what a reset returns to.
* @param options - redaction switch; wire surfaces must redact.
* @returns one descriptor per registered namespace, in registration order.
*/
describe(options?: SettingsDescribeOptions): SettingsDescriptor[]
/**
* Read one registered namespace's resolved value.
* @param ns - the namespace to read.
* @returns the resolved value, or `undefined` while unregistered.
*/
get(ns: SettingsNamespace): unknown
/**
* Merge a patch into one registered namespace's user layer, validate the
* resolved candidate, persist through the provider, then commit and emit.
* A validation failure rejects before anything is persisted. Writes to one
* namespace are serialized: concurrent updates apply in call order, each
* merging over the previous write's committed section.
* @param ns - the registered namespace to update.
* @param patch - plain-object patch over the user section.
* @param expectedRevision - the descriptor `revision` the caller read; a
* namespace that moved past it rejects with {@link SettingsConflictError}.
*/
async update(ns: SettingsNamespace, patch: object, expectedRevision?: number): Promise<void>
/**
* Replace one registered namespace's user section wholesale, validate,
* persist, then commit and emit. Keys absent from `section` fall back to the
* composition `base` and schema defaults — this is the removal/reset path a
* merge-only patch cannot express (`replace({})` re-inherits everything).
* @param ns - the registered namespace to replace.
* @param section - the complete next user section.
* @param expectedRevision - the descriptor `revision` the caller read; a
* namespace that moved past it rejects with {@link SettingsConflictError}.
*/
async replace(ns: SettingsNamespace, section: object, expectedRevision?: number): Promise<void>
/**
* Apply path-addressed edits to one registered namespace's user section,
* validate, persist, then commit and emit. The ops are applied to the
* section as it stands when the write reaches the front of the queue, so a
* caller never has to restate fields it did not touch — and, crucially,
* cannot delete fields it never saw. This is the write path for any caller
* holding a redacted view; `replace` remains the wholesale reset.
* @param ns - the registered namespace to edit.
* @param ops - ordered path edits; later ops observe earlier ones.
* @param expectedRevision - the descriptor `revision` the caller read; a
* namespace that moved past it rejects with {@link SettingsConflictError}.
*/
async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise<void>
```
Source: [`packages/settings/settings/src/index.ts:387`](../../packages/settings/settings/src/index.ts)
<a id="settings-events"></a>
### `settings/*` events
<a id="settingsdocument-updated--emit"></a>
#### `settings/document-updated` — emit
One registered namespace's RAW user section changed, whether or not the resolved value did. `settings/updated` is the consumer-facing event and stays deep-equal-gated; this one exists for configuration surfaces, which must learn that a field went from inherited to overridden (same resolved value, different meaning) and that their held revision is stale. Listener containment matches `settings/updated`.
```ts cordis-catalog
/**
* One registered namespace's RAW user section changed, whether or not the
* resolved value did. `settings/updated` is the consumer-facing event and
* stays deep-equal-gated; this one exists for configuration surfaces,
* which must learn that a field went from inherited to overridden (same
* resolved value, different meaning) and that their held revision is
* stale. Listener containment matches `settings/updated`.
* @param ns - the namespace whose stored section changed.
* @param revision - the namespace's new revision.
* @mode emit
*/
'settings/document-updated'(ns: SettingsNamespace, revision: number): void
```
Source: [`packages/settings/settings/src/index.ts:170`](../../packages/settings/settings/src/index.ts)
<a id="settingsupdated--emit"></a>
#### `settings/updated` — emit
Committed change to one registered namespace's resolved value. Emitted after the provider persisted (for `update`) or published (`provider`) the change; never emitted when the resolved value is deep-equal. Listener failures are contained and logged — a sync throw and an async rejection alike — except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
```ts cordis-catalog
/**
* Committed change to one registered namespace's resolved value. Emitted
* after the provider persisted (for `update`) or published (`provider`)
* the change; never emitted when the resolved value is deep-equal.
* Listener failures are contained and logged — a sync throw and an async
* rejection alike — except `INVARIANT`-coded failures, which rethrow
* after every listener ran; that rethrow reaches the emitter only from
* synchronous listeners, so invariant checks on this event must not be
* async functions.
* @param ns - the namespace whose resolved value changed.
* @param next - the new resolved value.
* @param prev - the previous resolved value.
* @param source - whether the change entered through `update()` or the provider.
* @mode emit
*/
'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void
```
Source: [`packages/settings/settings/src/index.ts:157`](../../packages/settings/settings/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -160,3 +160,151 @@ interface SettingsDescribeOptions {
/** Origin of one committed settings change. */
type SettingsUpdateSource = 'update' | 'provider'
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsettings--settings-abstract-seam"></a>
### `ctx.settings` — `Settings` (abstract seam)
Abstract settings service. Providers implement raw-document storage (`load`/`persist`) and push external changes through Settings.publish; the base class owns namespace registration, resolution, validation, change detection, and the `settings/updated` commit event.
```ts cordis-catalog
/**
* Prepare the provider's user-editable document for a native editor. File
* providers may materialize an absent document before returning its path;
* non-file providers return undefined.
* @returns the absolute local document path, or undefined for non-file storage.
*/
prepareDocument(): Promise<string | undefined>
/**
* Register a namespace schema and receive its owner scope. The registration
* is an effect on the calling plugin's fiber: disposing that fiber removes
* the namespace and its observers. An invalid stored section fails the
* registration itself — the earliest point where the schema can judge it.
* @param ns - unique namespace; duplicate registration fails loud.
* @param schema - schemastery schema resolving this namespace's value.
* @param options - composition `base` layer and effect timing.
* @returns the owner scope for reads, observation, and updates.
*/
register<T>(ns: SettingsNamespace, schema: z<T>, options?: SettingsRegisterOptions<T>): SettingsScope<T>
/**
* Describe every registered namespace for configuration surfaces, including
* the composition `base` and raw user layers so a form can mark which fields
* the user overrode (presence in `user`) and what a reset returns to.
* @param options - redaction switch; wire surfaces must redact.
* @returns one descriptor per registered namespace, in registration order.
*/
describe(options?: SettingsDescribeOptions): SettingsDescriptor[]
/**
* Read one registered namespace's resolved value.
* @param ns - the namespace to read.
* @returns the resolved value, or `undefined` while unregistered.
*/
get(ns: SettingsNamespace): unknown
/**
* Merge a patch into one registered namespace's user layer, validate the
* resolved candidate, persist through the provider, then commit and emit.
* A validation failure rejects before anything is persisted. Writes to one
* namespace are serialized: concurrent updates apply in call order, each
* merging over the previous write's committed section.
* @param ns - the registered namespace to update.
* @param patch - plain-object patch over the user section.
* @param expectedRevision - the descriptor `revision` the caller read; a
* namespace that moved past it rejects with {@link SettingsConflictError}.
*/
async update(ns: SettingsNamespace, patch: object, expectedRevision?: number): Promise<void>
/**
* Replace one registered namespace's user section wholesale, validate,
* persist, then commit and emit. Keys absent from `section` fall back to the
* composition `base` and schema defaults — this is the removal/reset path a
* merge-only patch cannot express (`replace({})` re-inherits everything).
* @param ns - the registered namespace to replace.
* @param section - the complete next user section.
* @param expectedRevision - the descriptor `revision` the caller read; a
* namespace that moved past it rejects with {@link SettingsConflictError}.
*/
async replace(ns: SettingsNamespace, section: object, expectedRevision?: number): Promise<void>
/**
* Apply path-addressed edits to one registered namespace's user section,
* validate, persist, then commit and emit. The ops are applied to the
* section as it stands when the write reaches the front of the queue, so a
* caller never has to restate fields it did not touch — and, crucially,
* cannot delete fields it never saw. This is the write path for any caller
* holding a redacted view; `replace` remains the wholesale reset.
* @param ns - the registered namespace to edit.
* @param ops - ordered path edits; later ops observe earlier ones.
* @param expectedRevision - the descriptor `revision` the caller read; a
* namespace that moved past it rejects with {@link SettingsConflictError}.
*/
async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise<void>
```
Source: [`packages/settings/settings/src/index.ts:387`](../../packages/settings/settings/src/index.ts)
<a id="settings-events"></a>
### `settings/*` events
<a id="settingsdocument-updated--emit"></a>
#### `settings/document-updated` — emit
One registered namespace's RAW user section changed, whether or not the resolved value did. `settings/updated` is the consumer-facing event and stays deep-equal-gated; this one exists for configuration surfaces, which must learn that a field went from inherited to overridden (same resolved value, different meaning) and that their held revision is stale. Listener containment matches `settings/updated`.
```ts cordis-catalog
/**
* One registered namespace's RAW user section changed, whether or not the
* resolved value did. `settings/updated` is the consumer-facing event and
* stays deep-equal-gated; this one exists for configuration surfaces,
* which must learn that a field went from inherited to overridden (same
* resolved value, different meaning) and that their held revision is
* stale. Listener containment matches `settings/updated`.
* @param ns - the namespace whose stored section changed.
* @param revision - the namespace's new revision.
* @mode emit
*/
'settings/document-updated'(ns: SettingsNamespace, revision: number): void
```
Source: [`packages/settings/settings/src/index.ts:170`](../../packages/settings/settings/src/index.ts)
<a id="settingsupdated--emit"></a>
#### `settings/updated` — emit
Committed change to one registered namespace's resolved value. Emitted after the provider persisted (for `update`) or published (`provider`) the change; never emitted when the resolved value is deep-equal. Listener failures are contained and logged — a sync throw and an async rejection alike — except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
```ts cordis-catalog
/**
* Committed change to one registered namespace's resolved value. Emitted
* after the provider persisted (for `update`) or published (`provider`)
* the change; never emitted when the resolved value is deep-equal.
* Listener failures are contained and logged — a sync throw and an async
* rejection alike — except `INVARIANT`-coded failures, which rethrow
* after every listener ran; that rethrow reaches the emitter only from
* synchronous listeners, so invariant checks on this event must not be
* async functions.
* @param ns - the namespace whose resolved value changed.
* @param next - the new resolved value.
* @param prev - the previous resolved value.
* @param source - whether the change entered through `update()` or the provider.
* @mode emit
*/
'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void
```
Source: [`packages/settings/settings/src/index.ts:157`](../../packages/settings/settings/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/skills.md
skills.md: d862cbc07135680c2377b4c8c82c80d055344221
skills.zh.md: 3f8c034ec2aa24b4acdbbc1e2ac27717c21649d5
skills.md: 78d510fafca0017b9528a215270172a74e9b0de1
skills.zh.md: e88c9f1ba190eb1e90966d35c7dcc2eb86ecd6cc

View File

@@ -218,3 +218,93 @@ interface Config {
Before each later model step, the consumer applies exact tool visibility and digests the exact rendered entries between the `<available_skills>` tags from a complete snapshot. It derives the comparison baseline from the same entries in the newest recognizable visible catalog message sourced by the plugin. A changed digest appends a durable full replacement through `agent.inject()`; deleting every skill appends an explicit empty replacement. Incomplete snapshots preserve the last-good model view. If compaction hides every historical catalog message, the next complete snapshot re-establishes the current catalog; an empty view with no prior catalog emits nothing. These catalog messages are session history, not World State.
The model-facing `skill({ name })` tool validates the kebab-case name, finds the summary in the invocation-neutral catalog, rejects it before loading unless `isModelInvocable` permits access, then rereads the complete definition for the calling agent cwd and rechecks the policy before returning content. It reports an unresolved skill as unknown or no longer available and returns a tool result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`. `resourceBase` resolves explicitly referenced scripts, references, and assets only as needed; the loaded result does not enumerate a skill directory. Body-only edits therefore change later tool calls without producing catalog messages or rewriting earlier tool results.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxskills--skillservice"></a>
### `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.
```ts cordis-catalog
/**
* Register a borrowed same-process provider synchronously during plugin apply. Duplicate and
* reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters
* the provider and invalidates catalog caches.
* @param create - synchronous factory receiving this registration's lifecycle and invalidation control.
* @returns the exact Cordis effect disposer that unregisters this provider;
* composite effects may yield it directly to preserve teardown ordering.
*/
registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => void
/**
* Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which
* outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and
* receives a no-op disposer so it cannot remove the winner.
* @param skill - the skill definition input; omitted invocation and provider fields receive defaults.
* @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
*/
register(skill: SkillRegistration): () => void
/**
* List invocation-neutral skill summaries for a workspace. Consumers apply
* model or user invocation policy at their operational boundary. Lookup
* options and provider candidates are readonly same-process values borrowed
* throughout discovery.
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
* @returns all sorted winning summaries.
*/
async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]>
/**
* Observe the current invocation-neutral catalog and whether discovery completed within a stable revision.
* Incomplete observations are never cached, allowing consumers to retain last-good state and
* retry on their next request boundary.
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
* @returns sorted summaries plus discovery-completeness state.
*/
async snapshot(options: SkillLookupOptions = {}): Promise<SkillCatalogSnapshot>
/**
* Load and validate the winning candidate, passing its opaque discovery locator back to the
* provider. Cancellation is rechecked after selection, including cache hits, and raced against
* loading so an uncooperative provider cannot hang the caller.
* @param name - kebab-case skill name.
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
* @returns the full skill, including body content, or `undefined`.
*/
async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined>
```
Source: [`packages/skill/skill/src/index.ts:304`](../../packages/skill/skill/src/index.ts)
<a id="skills-events"></a>
### `skills/*` events
<a id="skillschange--emit"></a>
#### `skills/change` — emit
A skill provider, runtime contribution, or provider-backed catalog may have changed. This is an unfiltered invalidation notification; consumers refetch the catalog for their own lookup options. Listener failures are contained and cannot veto the registry mutation.
```ts cordis-catalog
/**
* A skill provider, runtime contribution, or provider-backed catalog may
* have changed. This is an unfiltered invalidation notification; consumers
* refetch the catalog for their own lookup options. Listener failures are
* contained and cannot veto the registry mutation.
* @mode emit
*/
'skills/change'(): void
```
Source: [`packages/skill/skill/src/index.ts:283`](../../packages/skill/skill/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -218,3 +218,93 @@ interface Config {
在后续每个模型步骤之前,消费方都会应用精确的工具可见性,并对完整快照中 `<available_skills>` 标签之间精确渲染的条目计算 digest。它以该插件所发布、最新一条可识别且仍可见的目录消息中的相同条目作为比较基线。digest 发生变化时,会通过 `agent.inject()` 追加一条持久的完整目录替换;删除所有 skill 时会追加一条显式的空替换。不完整快照会保留上一份可用模型视图。如果压缩compaction隐藏了所有历史目录消息下一份完整快照会重新建立当前目录如果视图为空且从未发布目录则不发送任何内容。这些目录消息属于会话历史而非 World State。
面向模型的 `skill({ name })` 工具校验 kebab-case 名称,在与调用策略无关的目录中查找摘要,并在加载前通过 `isModelInvocable` 拒绝无权访问的 skill随后它根据调用方 agent 的 cwd 重新读取完整定义,并在返回内容前再次检查策略。该工具将无法解析的 skill 报告为未知或已不可用,并返回包含 `<skill_content name="...">`、`<skill_resources>` 和 `<skill_instructions>` 的工具结果。`resourceBase` 仅按需解析显式引用的脚本、参考资料和资产;加载结果不枚举 skill 目录。因此,仅修改正文会改变后续工具调用,而不会生成目录消息或改写先前工具结果。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxskills--skillservice"></a>
### `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.
```ts cordis-catalog
/**
* Register a borrowed same-process provider synchronously during plugin apply. Duplicate and
* reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters
* the provider and invalidates catalog caches.
* @param create - synchronous factory receiving this registration's lifecycle and invalidation control.
* @returns the exact Cordis effect disposer that unregisters this provider;
* composite effects may yield it directly to preserve teardown ordering.
*/
registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => void
/**
* Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which
* outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and
* receives a no-op disposer so it cannot remove the winner.
* @param skill - the skill definition input; omitted invocation and provider fields receive defaults.
* @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches.
*/
register(skill: SkillRegistration): () => void
/**
* List invocation-neutral skill summaries for a workspace. Consumers apply
* model or user invocation policy at their operational boundary. Lookup
* options and provider candidates are readonly same-process values borrowed
* throughout discovery.
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
* @returns all sorted winning summaries.
*/
async list(options: SkillLookupOptions = {}): Promise<SkillSummary[]>
/**
* Observe the current invocation-neutral catalog and whether discovery completed within a stable revision.
* Incomplete observations are never cached, allowing consumers to retain last-good state and
* retry on their next request boundary.
* @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery.
* @returns sorted summaries plus discovery-completeness state.
*/
async snapshot(options: SkillLookupOptions = {}): Promise<SkillCatalogSnapshot>
/**
* Load and validate the winning candidate, passing its opaque discovery locator back to the
* provider. Cancellation is rechecked after selection, including cache hits, and raced against
* loading so an uncooperative provider cannot hang the caller.
* @param name - kebab-case skill name.
* @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work.
* @returns the full skill, including body content, or `undefined`.
*/
async get(name: string, options: SkillLookupOptions = {}): Promise<SkillDefinition | undefined>
```
Source: [`packages/skill/skill/src/index.ts:304`](../../packages/skill/skill/src/index.ts)
<a id="skills-events"></a>
### `skills/*` events
<a id="skillschange--emit"></a>
#### `skills/change` — emit
A skill provider, runtime contribution, or provider-backed catalog may have changed. This is an unfiltered invalidation notification; consumers refetch the catalog for their own lookup options. Listener failures are contained and cannot veto the registry mutation.
```ts cordis-catalog
/**
* A skill provider, runtime contribution, or provider-backed catalog may
* have changed. This is an unfiltered invalidation notification; consumers
* refetch the catalog for their own lookup options. Listener failures are
* contained and cannot veto the registry mutation.
* @mode emit
*/
'skills/change'(): void
```
Source: [`packages/skill/skill/src/index.ts:283`](../../packages/skill/skill/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/spill.md
spill.md: a798d8143b2849dc0cf49d04e7019ce796cdee45
spill.zh.md: 1167c6f985dbc204dc7166b0fb5854dcf55bc72f
# pnpm run verify-translation-pairing --write docs/subsystems/spill.md
spill.md: 41a3fdaf7d9b15fbc6d479724f77f579b8a98738
spill.zh.md: 02a6fc4e88dfcfdb47f7639d988fc09c6bc42c69

View File

@@ -83,3 +83,35 @@ type SpillLocator = Branded<'SpillLocator'>
`SpillStore` (`ctx.spillStore`, defined in [`packages/spill/spill/src/index.ts`](../../packages/spill/spill/src/index.ts)) is a one-method abstract service: `saveText(input) → Promise<SpillRef>`. It persists the FULL `content` and REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable). The seam owns storage only: no retention policy, no tool-result replacement, no retrieval/search API.
The local backend ([dsh-spill-local](../../packages/spill/spill-local)) writes under `<root>/session-<hash>/<random>-<safeName>` — a configured or lazily-created private (0700) root, a `sha256(sessionId)` session subdir, and an exclusive owner-only (`open(path, 'wx', 0o600)`) write so a planted symlink cannot redirect it. Its `locator` is the local path and its `retrievalHint` tells the model to use `read` or `grep` on that path. The policy consumer ([dsh-spill-policy](../../packages/spill/spill-policy)) replaces an over-`maxInlineBytes` plain-text final result with a retention-library head/tail preview plus the spill reference, best-effort: a save failure keeps the original inline result rather than turning a successful call into an `isError`.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxspillstore--spillstore-abstract-seam"></a>
### `ctx.spillStore` — `SpillStore` (abstract seam)
Abstract spill storage service. Subclass, implement saveText, and load the subclass as a plugin — it registers as `ctx.spillStore` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
Semantics every implementation must honor:
- saveText persists the FULL `content` verbatim and returns an opaque locator, exact byte length, and model-facing retrieval guidance.
- Storage is scoped by the request's SaveTextSpill.owner session; the backend chooses a private (not world-readable) location and a collision-free name derived from — never equal to — the caller's `suggestedName`.
- `saveText` REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable); the caller decides how to degrade (the spill policy treats a rejection as best-effort and keeps the inline result).
```ts cordis-catalog
/**
* Persist `input.content` to a session-scoped spill artifact.
* @param input - the owner, provenance, suggested name, and full text to save.
* @returns the saved artifact's {@link SpillRef}; rejects on a storage failure.
*/
abstract saveText(input: SaveTextSpill): Promise<SpillRef>
```
Source: [`packages/spill/spill/src/index.ts:45`](../../packages/spill/spill/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -83,3 +83,35 @@ type SpillLocator = Branded<'SpillLocator'>
`SpillStore``ctx.spillStore`,定义于 [`packages/spill/spill/src/index.ts`](../../packages/spill/spill/src/index.ts))是只有一个方法的抽象服务:`saveText(input) → Promise<SpillRef>`。它持久保存完整的 `content`并在实际存储失败权限、ENOSPC、后端不可用时拒绝。该 seam 只负责存储:不负责保留策略、工具结果替换或检索/搜索 API。
本地后端([dsh-spill-local](../../packages/spill/spill-local))写入 `<root>/session-<hash>/<random>-<safeName>`根目录是已配置或延迟创建的私有0700目录会话子目录采用 `sha256(sessionId)`,并通过排他的仅所有者可访问写入(`open(path, 'wx', 0o600)`)防止预先植入的符号链接重定向写入。其 `locator` 是本地路径,`retrievalHint` 则告知模型在该路径上使用 `read` 或 `grep`。策略消费方([dsh-spill-policy](../../packages/spill/spill-policy))会把超过 `maxInlineBytes` 的纯文本最终结果替换为保留库生成的首尾预览和落盘引用;该过程尽力而为:保存失败时保留原始内联结果,而不会把成功的调用变成 `isError`。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxspillstore--spillstore-abstract-seam"></a>
### `ctx.spillStore` — `SpillStore` (abstract seam)
Abstract spill storage service. Subclass, implement saveText, and load the subclass as a plugin — it registers as `ctx.spillStore` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
Semantics every implementation must honor:
- saveText persists the FULL `content` verbatim and returns an opaque locator, exact byte length, and model-facing retrieval guidance.
- Storage is scoped by the request's SaveTextSpill.owner session; the backend chooses a private (not world-readable) location and a collision-free name derived from — never equal to — the caller's `suggestedName`.
- `saveText` REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable); the caller decides how to degrade (the spill policy treats a rejection as best-effort and keeps the inline result).
```ts cordis-catalog
/**
* Persist `input.content` to a session-scoped spill artifact.
* @param input - the owner, provenance, suggested name, and full text to save.
* @returns the saved artifact's {@link SpillRef}; rejects on a storage failure.
*/
abstract saveText(input: SaveTextSpill): Promise<SpillRef>
```
Source: [`packages/spill/spill/src/index.ts:45`](../../packages/spill/spill/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: 5d54d7337b5ef52e24179758130b9fa8cd196376
storage.zh.md: 46346b64351322d40de166a71f7a3204c8902b2d
storage.md: 52dc84b699921e911276ce5214bc975abfd22fa4
storage.zh.md: f614b77153e835264e1650515946a9d2acf70e57

View File

@@ -8,7 +8,7 @@ Source: [`packages/storage/storage/src/backend.ts`](../../packages/storage/stora
## The hub: `ctx.storage`
`Storage` ([signatures](../cordis-catalog/services.md#ctxstorage--storage)) is a meeting point, not a store. `ctx.storage.backend` is a name → backend table: multiple backends stay mounted side by side, and which backend serves which consumer is that consumer's configuration (the domain layer's route table), never a hub-global choice. `register(name, backend)` returns the disposer; duplicate names and unknown lookups throw `StorageError`. Disposal only unregisters the name — the owning plugin closes the backend after unregistering. Each backend plugin also publishes a lifecycle-only service key (`storageBackendServiceKey(name)`), which form providers inject so their activation cannot race backend registration.
`Storage` ([signatures](#ctxstorage--storage)) is a meeting point, not a store. `ctx.storage.backend` is a name → backend table: multiple backends stay mounted side by side, and which backend serves which consumer is that consumer's configuration (the domain layer's route table), never a hub-global choice. `register(name, backend)` returns the disposer; duplicate names and unknown lookups throw `StorageError`. Disposal only unregisters the name — the owning plugin closes the backend after unregistering. Each backend plugin also publishes a lifecycle-only service key (`storageBackendServiceKey(name)`), which form providers inject so their activation cannot race backend registration.
Data forms mount on the hub under a merge-extensible key map:
@@ -99,11 +99,11 @@ Reads are synchronous from authoritative in-memory state: `KvTable` exposes `get
## The domain facility: `ctx.storageDomain`
`DomainFacility` ([signatures](../cordis-catalog/services.md#ctxstoragedomain--domainfacility)) opens declared domains over routed backends. Routing is the domain plugin's configuration, never the hub's: `backend` names the required default route and `routes` overrides it per domain name. `open(spec)` runs a strict sequence, each step failing the whole call: it rejects a name already open or still closing (`already-open`), resolves the route (`backend-not-found`), requires the backend's `kv` facet (`facet-unsupported`), opens the unit (backend `version-mismatch`/`malformed-medium` pass through), and validates every stored record and global against the spec's zod schemas (`invalid-record` with the offending table and key). The caller owns the returned handle and releases it with `Domain.close()`; domains still open when the plugin unmounts are closed by the facility, and a closed domain's name frees for reopening only after teardown fully completes. `get(name)` is an untyped diagnostic lookup onto the package-private `DomainImpl` runtime behind every typed handle; `closeAll()` is the unmount path.
`DomainFacility` ([signatures](#ctxstoragedomain--domainfacility)) opens declared domains over routed backends. Routing is the domain plugin's configuration, never the hub's: `backend` names the required default route and `routes` overrides it per domain name. `open(spec)` runs a strict sequence, each step failing the whole call: it rejects a name already open or still closing (`already-open`), resolves the route (`backend-not-found`), requires the backend's `kv` facet (`facet-unsupported`), opens the unit (backend `version-mismatch`/`malformed-medium` pass through), and validates every stored record and global against the spec's zod schemas (`invalid-record` with the offending table and key). The caller owns the returned handle and releases it with `Domain.close()`; domains still open when the plugin unmounts are closed by the facility, and a closed domain's name frees for reopening only after teardown fully completes. `get(name)` is an untyped diagnostic lookup onto the package-private `DomainImpl` runtime behind every typed handle; `closeAll()` is the unmount path.
## The change event: `domain/changed`
Every durable write emits one event strictly after the backend acknowledged durability, in the domain's write-chain order ([event entry](../cordis-catalog/events.md#domainchanged--emit)):
Every durable write emits one event strictly after the backend acknowledged durability, in the domain's write-chain order ([event entry](#domainchanged--emit)):
```ts type-equiv
/** Shared location fields of one durable domain change. */
@@ -123,3 +123,107 @@ 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).
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxstorage--storage"></a>
### `ctx.storage` — `Storage`
The storage hub service. Backends register under `backend`; data forms mount under their `StorageForms` key and are reached as `ctx.storage.<form>`.
```ts cordis-catalog
/**
* Mount a data-form facility on the hub. Mounting is an effect: the
* returned disposer unmounts the form.
* @param form - Form key declared in {@link StorageForms}.
* @param facility - The facility instance to expose.
* @returns the disposer that unmounts the form.
*/
mount<K extends keyof StorageForms>(form: K, facility: StorageForms[K]): () => void
/**
* Resolve a mounted data form.
* @param form - Form key declared in {@link StorageForms}.
* @returns the mounted facility.
*/
form<K extends keyof StorageForms>(form: K): StorageForms[K]
```
Source: [`packages/storage/storage/src/index.ts:47`](../../packages/storage/storage/src/index.ts)
<a id="ctxstoragedomain--domainfacility"></a>
### `ctx.storageDomain` — `DomainFacility`
The mounted domain facility. Opens declared domains over routed backends; one facility instance owns the open-domain table and enforces single-open per domain name.
```ts cordis-catalog
/**
* Open one declared domain. Steps, each failing the whole call: reject a
* name that is already open (`already-open`); resolve the backend route
* (`backend-not-found` passes through from the hub); require its `kv` facet
* (`facet-unsupported`); open the unit projected from the spec (backend
* `version-mismatch`/`malformed-medium` pass through); load and validate
* every stored record against the spec's zod schemas (`invalid-record`
* with the offending table and key); construct the domain.
*
* Lifecycle: the CALLER owns the returned handle and closes it via
* `Domain.close()` (typically as its own `ctx.effect` disposer) — the
* facility does not tie the domain to any consumer fiber. Domains still
* open when the facility unmounts are closed by the plugin disposer.
* @param spec - The domain declaration, typically from `defineDomain`.
* @returns the opened domain handle, typed by the spec.
*/
async open<S extends DomainSpec>(spec: S): Promise<Domain<S>>
/**
* Look up an open domain by name, untyped. Diagnostic surface (the package
* invariant cross-checks change events against live domain state); typed
* consumers hold the handle returned by {@link open}.
* @param name - Domain name.
* @returns the open domain runtime, or `undefined` when not open.
*/
get(name: string): DomainImpl | undefined
/**
* Close every domain still open on this facility. The unmount path for
* consumers that never called `Domain.close()` themselves; closing is
* idempotent, so double-closing an already-closed domain is harmless.
* @returns resolution after every unit is released.
*/
async closeAll(): Promise<void>
```
Source: [`packages/storage/storage-domain/src/index.ts:69`](../../packages/storage/storage-domain/src/index.ts)
<a id="domain-events"></a>
### `domain/*` events
<a id="domainchanged--emit"></a>
#### `domain/changed` — emit
A domain record or the global singleton changed, emitted once per write strictly after the backend acknowledged durability. Events of one domain arrive in its write-chain order.
```ts cordis-catalog
/**
* A domain record or the global singleton changed, emitted once per write
* strictly after the backend acknowledged durability. Events of one
* domain arrive in its write-chain order.
* @param change - domain, table (`''` for global), key (`''` for global),
* operation discriminant, and on `put` the new snapshot.
* @mode emit
*/
'domain/changed'(change: DomainChanged): void
```
Source: [`packages/storage/storage-domain/src/events.ts:46`](../../packages/storage/storage-domain/src/events.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -8,7 +8,7 @@
## 枢纽:`ctx.storage`
`Storage`[签名](../cordis-catalog/services.md#ctxstorage--storage))是汇合点,不是存储本体。`ctx.storage.backend` 是一张名称 → 后端的表:多个后端并排保持挂载,哪个后端服务哪个消费方由该消费方自己的配置决定(即领域层的路由表),绝不是枢纽全局的选择。`register(name, backend)` 返回 disposer重复名称与查找未知名称都抛出 `StorageError`。dispose资源释放只注销名称——由拥有插件在注销之后自行关闭后端。每个后端插件还会发布一个仅用于生命周期的服务键`storageBackendServiceKey(name)`),数据形式提供方注入它,使自身激活不会与后端注册发生竞态。
`Storage`[签名](#ctxstorage--storage))是汇合点,不是存储本体。`ctx.storage.backend` 是一张名称 → 后端的表:多个后端并排保持挂载,哪个后端服务哪个消费方由该消费方自己的配置决定(即领域层的路由表),绝不是枢纽全局的选择。`register(name, backend)` 返回 disposer重复名称与查找未知名称都抛出 `StorageError`。dispose资源释放只注销名称——由拥有插件在注销之后自行关闭后端。每个后端插件还会发布一个仅用于生命周期的服务键`storageBackendServiceKey(name)`),数据形式提供方注入它,使自身激活不会与后端注册发生竞态。
数据形式以一张可合并扩展的键 map 挂载到枢纽上:
@@ -99,11 +99,11 @@ interface Domain<S extends DomainSpec> {
## 领域 facility`ctx.storageDomain`
`DomainFacility`[签名](../cordis-catalog/services.md#ctxstoragedomain--domainfacility))在经过路由的后端之上打开已声明的领域。路由是领域插件的配置,绝不属于枢纽:`backend` 指定必填的默认路由,`routes` 按领域名逐个覆盖。`open(spec)` 按严格顺序执行,每一步失败都使整个调用失败:拒绝已打开或仍在关闭中的名称(`already-open`),解析路由(`backend-not-found`),要求后端具备 `kv` facet`facet-unsupported`),打开 unit后端的 `version-mismatch`/`malformed-medium` 原样透传),并按 spec 的 zod schema 校验每条已存储记录和 global`invalid-record`,附带出错的表与键)。调用方拥有返回的句柄,并用 `Domain.close()` 释放它;插件卸载时仍处于打开状态的领域由 facility 负责关闭,已关闭领域的名称只有在拆除完全结束后才释放出来供重新打开。`get(name)` 是无类型的诊断查找,命中的是每个类型化句柄背后包内私有的 `DomainImpl` 运行时;`closeAll()` 是卸载路径。
`DomainFacility`[签名](#ctxstoragedomain--domainfacility))在经过路由的后端之上打开已声明的领域。路由是领域插件的配置,绝不属于枢纽:`backend` 指定必填的默认路由,`routes` 按领域名逐个覆盖。`open(spec)` 按严格顺序执行,每一步失败都使整个调用失败:拒绝已打开或仍在关闭中的名称(`already-open`),解析路由(`backend-not-found`),要求后端具备 `kv` facet`facet-unsupported`),打开 unit后端的 `version-mismatch`/`malformed-medium` 原样透传),并按 spec 的 zod schema 校验每条已存储记录和 global`invalid-record`,附带出错的表与键)。调用方拥有返回的句柄,并用 `Domain.close()` 释放它;插件卸载时仍处于打开状态的领域由 facility 负责关闭,已关闭领域的名称只有在拆除完全结束后才释放出来供重新打开。`get(name)` 是无类型的诊断查找,命中的是每个类型化句柄背后包内私有的 `DomainImpl` 运行时;`closeAll()` 是卸载路径。
## 变更事件:`domain/changed`
每次持久写入都发出一个事件,严格发生在后端确认持久性之后,顺序遵循该领域的写链([事件条目](../cordis-catalog/events.md#domainchanged--emit)
每次持久写入都发出一个事件,严格发生在后端确认持久性之后,顺序遵循该领域的写链([事件条目](#domainchanged--emit)
```ts type-equiv
/** Shared location fields of one durable domain change. */
@@ -123,3 +123,107 @@ type DomainChanged = DomainChangedPut | DomainChangedDeleted
```
`put`(插入、覆写和 global 写入)在 `value` 中携带新快照——绝不携带旧值;需要做差异比较的消费方自行保留上一份快照。`deleted` 是不携带值的墓碑。该事件是通知,不是事务参与者:发出时提交点已经过去,因此同步抛出的监听器会被兜住并记录一条警告,而不会让已经持久的写入被拒绝;发出的值等于发出时刻的内存态。该事件仅限进程内;跨进程的变更推送是延后工作,记录在[包 README](../../packages/storage/storage-domain/README.md)中。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxstorage--storage"></a>
### `ctx.storage` — `Storage`
The storage hub service. Backends register under `backend`; data forms mount under their `StorageForms` key and are reached as `ctx.storage.<form>`.
```ts cordis-catalog
/**
* Mount a data-form facility on the hub. Mounting is an effect: the
* returned disposer unmounts the form.
* @param form - Form key declared in {@link StorageForms}.
* @param facility - The facility instance to expose.
* @returns the disposer that unmounts the form.
*/
mount<K extends keyof StorageForms>(form: K, facility: StorageForms[K]): () => void
/**
* Resolve a mounted data form.
* @param form - Form key declared in {@link StorageForms}.
* @returns the mounted facility.
*/
form<K extends keyof StorageForms>(form: K): StorageForms[K]
```
Source: [`packages/storage/storage/src/index.ts:47`](../../packages/storage/storage/src/index.ts)
<a id="ctxstoragedomain--domainfacility"></a>
### `ctx.storageDomain` — `DomainFacility`
The mounted domain facility. Opens declared domains over routed backends; one facility instance owns the open-domain table and enforces single-open per domain name.
```ts cordis-catalog
/**
* Open one declared domain. Steps, each failing the whole call: reject a
* name that is already open (`already-open`); resolve the backend route
* (`backend-not-found` passes through from the hub); require its `kv` facet
* (`facet-unsupported`); open the unit projected from the spec (backend
* `version-mismatch`/`malformed-medium` pass through); load and validate
* every stored record against the spec's zod schemas (`invalid-record`
* with the offending table and key); construct the domain.
*
* Lifecycle: the CALLER owns the returned handle and closes it via
* `Domain.close()` (typically as its own `ctx.effect` disposer) — the
* facility does not tie the domain to any consumer fiber. Domains still
* open when the facility unmounts are closed by the plugin disposer.
* @param spec - The domain declaration, typically from `defineDomain`.
* @returns the opened domain handle, typed by the spec.
*/
async open<S extends DomainSpec>(spec: S): Promise<Domain<S>>
/**
* Look up an open domain by name, untyped. Diagnostic surface (the package
* invariant cross-checks change events against live domain state); typed
* consumers hold the handle returned by {@link open}.
* @param name - Domain name.
* @returns the open domain runtime, or `undefined` when not open.
*/
get(name: string): DomainImpl | undefined
/**
* Close every domain still open on this facility. The unmount path for
* consumers that never called `Domain.close()` themselves; closing is
* idempotent, so double-closing an already-closed domain is harmless.
* @returns resolution after every unit is released.
*/
async closeAll(): Promise<void>
```
Source: [`packages/storage/storage-domain/src/index.ts:69`](../../packages/storage/storage-domain/src/index.ts)
<a id="domain-events"></a>
### `domain/*` events
<a id="domainchanged--emit"></a>
#### `domain/changed` — emit
A domain record or the global singleton changed, emitted once per write strictly after the backend acknowledged durability. Events of one domain arrive in its write-chain order.
```ts cordis-catalog
/**
* A domain record or the global singleton changed, emitted once per write
* strictly after the backend acknowledged durability. Events of one
* domain arrive in its write-chain order.
* @param change - domain, table (`''` for global), key (`''` for global),
* operation discriminant, and on `put` the new snapshot.
* @mode emit
*/
'domain/changed'(change: DomainChanged): void
```
Source: [`packages/storage/storage-domain/src/events.ts:46`](../../packages/storage/storage-domain/src/events.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/subagent.md
subagent.md: b26a12d1d50305d86d7ada29cac83474009d81ce
subagent.zh.md: 6c4c64ff22050b73699a97093acd0032668fde3d
# pnpm run verify-translation-pairing --write docs/subsystems/subagent.md
subagent.md: cbf9043b36a5cb2ab37bb99c6dbbf5a18d1e0288
subagent.zh.md: ffd8fefd8b9d128c21e1b91ee0ba1fb706638fa9

View File

@@ -433,3 +433,269 @@ The spawn and fork backends create an ordinary one-shot agent through `parent.ct
- **Delegation depth** is durable `SessionHeader.delegationDepth` plus the merge-extensible runtime field `AgentOptions.subagentDepth`; absence means top-level depth zero, and the greater present value is authoritative. The seam owns both fields — the loop neither sets nor reads them — so an in-process child persists parent depth + 1, cold resume cannot lower it, and every start rejects a derived depth outside the safe-integer domain or above a defined absolute `request.maxDepth` cap.
- **Fork seeding** uses `CreateAgentOptions.seed` (a `SessionEvent[]` prefix threaded through `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })`, the same primitive `ctx.agents.resume()` uses). The fork backend passes a *balanced completed-turn prefix* of the parent's log — the parent's events up to and including its last `turn/end` — so the seed is contiguous-from-0 and the [invariants](../../packages/support/invariants) replay accepts it (the in-flight, unbalanced turn is excluded).
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsubagents--subagentservice"></a>
### `ctx.subagents` — `SubagentService`
Named provider registry with one-shot runs, durable discovery, and continuable-child operations.
```ts cordis-catalog
/**
* Establish one durable continuable child and deliver its initial prompt.
* Resolves when the child's inbox accepts that prompt, without waiting for the
* turn to start or for the message to reach the Session log; any earlier
* failure rejects with no ids and rolls back the child entirely.
* @param spec - provider, delegation request, and caller cancellation.
* @returns the durable child id and the accepted prompt's message id.
* @throws when continuation services are unavailable or materialization fails.
*/
async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>
/**
* Deliver one later message to a continuable child as its next FIFO turn. A
* resident child's Agent inbox accepts it directly (waking a `waiting`
* Activation), while an absent one is cold-resumed from its persisted
* Session. The Agent inbox is the only queue, so every accepted message has
* one observable order.
* @param parent - the exact live direct parent authorizing this delivery.
* @param childId - durable child session id.
* @param content - user-role content to deliver.
* @param options - durable provenance and caller cancellation, which stops the
* operation only before inbox acceptance.
* @returns the accepted message's inbox id.
* @throws when continuation services are unavailable, parent authority is
* rejected, or the message was not admitted.
*/
async followup( parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise<MessageId>
/**
* Interrupt one live continuable child's current turn under a human parent
* address or an exact live ancestor Agent. Fire-and-return: the cancel
* signal is issued before this returns, but the target may keep running
* until it observes the signal. Unclaimed pending inbox work, the Activation,
* and published descendants are preserved; claimed work is not requeued.
* Once the interrupted driver is idle, a waking send resumes the parked FIFO
* queue. An absent target — including a one-shot or unknown id —
* is an accepted no-op, as is a manager-less composition, which cannot own a
* live Activation.
* @param targetSessionId - the durable child session id to interrupt.
* @param authority - the human parent address or exact live ancestor Agent.
* @throws {SubagentError} `UNAUTHORIZED` when the authority does not own the
* live target.
*/
interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void
/**
* Deliver selected content from one live continuable child to its durable
* direct parent. The child is the authority credential; callers cannot name a
* recipient. Reporting does not conclude the child's turn or Activation.
* @param child - exact live reporting child.
* @param content - selected model-facing content.
* @param options - parent scheduling and pre-acceptance cancellation.
* @returns the stable identity of the parent-accepted message.
* @throws when continuation services are unavailable, sender authorization
* fails, or the direct parent is not live.
*/
async reportFrom( child: Agent, content: ContentBlock[], options: SubagentReportOptions, ): Promise<MessageId>
/**
* Compose one deployment capability into every continuable child's
* unpublished creation context on fresh creation and cold resume. Grants wait
* for the next Activation; removing the contribution revokes every resident
* installation immediately.
* @param contribution - synchronous child-scope installer.
* @returns the exact Cordis effect disposer.
*/
registerContinuableSetup(contribution: ContinuableSetupContribution): () => void
/**
* Close continuable admission below exact live parent Agents, stop only their
* visible descendant Activations synchronously, then await admitted scoped
* materializations and release those forests child-first. The scoped cutoff
* lasts until each exact parent leaves the registry; unrelated parent trees
* remain live.
* @param parents - exact host-owned parent Agents entering teardown.
* @returns once every retained descendant Activation released its `AgentHandle`.
* @throws an aggregate error after all branches settle when any failed.
*/
async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>
/**
* Enumerate the parent's direct session-backed subagents without loading or
* resuming an Agent and without any query seam: the listing merges the live
* session store with optional session persistence (live-preferred) and
* serves each child's durable mode/label from the registered `subagent`
* projection unit down a three-rung ladder — the registry's watermark
* snapshot for a live child; for a cold one, a durable projection-cache
* row when the optional cache serves an own-suffix identity (its `seq`
* gate proves the value postdates the fork seed, where a child's own
* descriptor is immutable once appended), else one persistence inspection
* folded through the registry. The
* projection fold is the single classification authority; per-child
* diagnostics relay a fold that served no identity or a failed inspection,
* never a list-time descriptor parse. Absent persistence, enumeration is
* live-only (a cold child cannot be resumed then either, so its absence is
* capability absence, not an error). This service consults no Agent
* registrations, Activations, or providers.
*
* Every persistence read receives `signal`, and the listing rechecks
* cancellation around each of those awaits. Read rejections that settle
* after an abort become a stable `SubagentError` with code `CANCELLED`.
* @param parentSessionId - parent session whose direct children are listed.
* @param signal - caller-owned cancellation forwarded to persistence reads
* and observed around every read await.
* @returns children and per-child diagnostics ordered by `createdAt`, then id.
* @throws {@link SubagentError} when the projection registry or the session
* store is not mounted, or the caller cancels the listing.
*/
listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentListEntry[]>
/**
* Enumerate the root's complete session-backed subagent tree in stable
* pre-order from one live-preferred corpus, without loading or resuming an
* Agent. Ordinary sessions and one-shot children remain traversal nodes so
* continuable descendants below them are discovered; each returned entry
* adds its durable `parentId` and root-relative `depth`. Identity resolution,
* diagnostics, optional persistence, and cancellation follow the same
* projection-backed contract as {@link listChildren}.
* @param rootSessionId - session whose complete descendant tree is listed.
* @param signal - caller-owned cancellation forwarded to persistence reads
* and observed around every read await.
* @returns children and per-candidate diagnostics with tree position, in
* stable pre-order.
* @throws {@link SubagentError} under the same conditions as {@link listChildren}.
*/
listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]>
/**
* Register a provider under its name. Registration is effect-scoped and HMR
* safe; removing a provider blocks new starts but does not revoke runs that
* were already returned to their holders.
* @param provider - the trusted provider implementation.
* @returns the exact Cordis effect disposer.
*/
registerProvider(provider: SubagentProvider): () => void
/**
* Look up a provider by name.
* @param name - the provider name.
* @returns the provider, or undefined when absent.
*/
getProvider(name: string): SubagentProvider | undefined
/**
* List registered provider names in insertion order.
* @returns the registered names.
*/
list(): string[]
/**
* Establish a published child on the named provider. Capability and semantic
* checks run before delegation. Provider ownership lasts until its promise
* fulfills; a rejection therefore has no run for the caller to dispose and
* emits no run lifecycle events. Post-publication turn and infrastructure
* failures settle through the returned run.
* @param name - the provider to use.
* @param request - child label, prompt, parent, signal, and optional capabilities.
* @returns the published holder-owned run.
*/
async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
```
Types: [Agent](core.md) · [ContentBlock](core.md) · [MessageId](core.md) · [SessionId](core.md)
Source: [`packages/subagent/subagent/src/index.ts:167`](../../packages/subagent/subagent/src/index.ts)
<a id="subagent-events"></a>
### `subagent/*` events
<a id="subagentend--emit"></a>
#### `subagent/end` — emit
A published child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience.
```ts cordis-catalog
/**
* A published child settled. Scope-filtered dispatch uses the same delegating
* parent carrier as `subagent/start`, so the lifecycle pair reaches the
* same scoped audience.
* @param info - the run identity and terminal outcome.
* @dshScopeScan unsupported
* @mode emit
*/
'subagent/end'(this: Scoped<SubagentService>, info: SubagentRunEndInfo): void
```
Types: [Scoped](scope.md)
Source: [`packages/subagent/subagent/src/index.ts:162`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentprovider-added--emit"></a>
#### `subagent/provider-added` — emit
A provider became resolvable in the registry.
```ts cordis-catalog
/**
* A provider became resolvable in the registry.
* @param provider - the registered provider.
* @mode emit
*/
'subagent/provider-added'(provider: SubagentProvider): void
```
Source: [`packages/subagent/subagent/src/index.ts:136`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentprovider-removed--emit"></a>
#### `subagent/provider-removed` — emit
A provider left the registry. Accepted runs remain holder-owned.
```ts cordis-catalog
/**
* A provider left the registry. Accepted runs remain holder-owned.
* @param name - the provider name that no longer resolves.
* @mode emit
*/
'subagent/provider-removed'(name: string): void
```
Source: [`packages/subagent/subagent/src/index.ts:142`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentstart--emit"></a>
#### `subagent/start` — emit
A provider established a published child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with `subagent/end`.
```ts cordis-catalog
/**
* A provider established a published child. For in-process providers,
* `ctx.agents.get(info.id)` resolves during this notification.
* Scope-filtered dispatch keys the carrier by the delegating parent, so a
* parent-scoped listener observes only its own delegations. Paired with
* `subagent/end`.
* @param info - the provider and published child identity.
* @dshScopeScan unsupported
* @mode emit
*/
'subagent/start'(this: Scoped<SubagentService>, info: SubagentRunInfo): void
```
Types: [Scoped](scope.md)
Source: [`packages/subagent/subagent/src/index.ts:153`](../../packages/subagent/subagent/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -435,3 +435,269 @@ spawn 和 fork 后端通过 `parent.ctx` 创建一个普通的单次 agent
- **委派深度**由持久 `SessionHeader.delegationDepth` 与可合并扩展的运行时字段 `AgentOptions.subagentDepth` 共同表示;缺失表示顶层深度为零,存在的较大值具有权威性。两个字段都归该 seam 所有——循环既不设置也不读取它们——因此进程内子 agent 会持久保存 parent 深度 + 1冷恢复无法降低深度而且每次 start 都会拒绝超出安全整数域、或高于已定义绝对 `request.maxDepth` 上限的派生深度。
- **Fork 种子注入**使用 `CreateAgentOptions.seed`(一个 `SessionEvent[]` 前缀,经由 `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })` 传递,与 `ctx.agents.resume()` 使用的原语相同。fork 后端传入父级日志的一段*平衡的已完成轮次前缀*——父级事件直到并包括其最后一个 `turn/end`——因此种子从 0 连续,[invariants](../../packages/support/invariants) 回放可以接受它(进行中的、未平衡的轮次被排除在外)。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsubagents--subagentservice"></a>
### `ctx.subagents` — `SubagentService`
Named provider registry with one-shot runs, durable discovery, and continuable-child operations.
```ts cordis-catalog
/**
* Establish one durable continuable child and deliver its initial prompt.
* Resolves when the child's inbox accepts that prompt, without waiting for the
* turn to start or for the message to reach the Session log; any earlier
* failure rejects with no ids and rolls back the child entirely.
* @param spec - provider, delegation request, and caller cancellation.
* @returns the durable child id and the accepted prompt's message id.
* @throws when continuation services are unavailable or materialization fails.
*/
async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>
/**
* Deliver one later message to a continuable child as its next FIFO turn. A
* resident child's Agent inbox accepts it directly (waking a `waiting`
* Activation), while an absent one is cold-resumed from its persisted
* Session. The Agent inbox is the only queue, so every accepted message has
* one observable order.
* @param parent - the exact live direct parent authorizing this delivery.
* @param childId - durable child session id.
* @param content - user-role content to deliver.
* @param options - durable provenance and caller cancellation, which stops the
* operation only before inbox acceptance.
* @returns the accepted message's inbox id.
* @throws when continuation services are unavailable, parent authority is
* rejected, or the message was not admitted.
*/
async followup( parent: Agent, childId: SessionId, content: ContentBlock[], options: SubagentFollowupOptions, ): Promise<MessageId>
/**
* Interrupt one live continuable child's current turn under a human parent
* address or an exact live ancestor Agent. Fire-and-return: the cancel
* signal is issued before this returns, but the target may keep running
* until it observes the signal. Unclaimed pending inbox work, the Activation,
* and published descendants are preserved; claimed work is not requeued.
* Once the interrupted driver is idle, a waking send resumes the parked FIFO
* queue. An absent target — including a one-shot or unknown id —
* is an accepted no-op, as is a manager-less composition, which cannot own a
* live Activation.
* @param targetSessionId - the durable child session id to interrupt.
* @param authority - the human parent address or exact live ancestor Agent.
* @throws {SubagentError} `UNAUTHORIZED` when the authority does not own the
* live target.
*/
interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void
/**
* Deliver selected content from one live continuable child to its durable
* direct parent. The child is the authority credential; callers cannot name a
* recipient. Reporting does not conclude the child's turn or Activation.
* @param child - exact live reporting child.
* @param content - selected model-facing content.
* @param options - parent scheduling and pre-acceptance cancellation.
* @returns the stable identity of the parent-accepted message.
* @throws when continuation services are unavailable, sender authorization
* fails, or the direct parent is not live.
*/
async reportFrom( child: Agent, content: ContentBlock[], options: SubagentReportOptions, ): Promise<MessageId>
/**
* Compose one deployment capability into every continuable child's
* unpublished creation context on fresh creation and cold resume. Grants wait
* for the next Activation; removing the contribution revokes every resident
* installation immediately.
* @param contribution - synchronous child-scope installer.
* @returns the exact Cordis effect disposer.
*/
registerContinuableSetup(contribution: ContinuableSetupContribution): () => void
/**
* Close continuable admission below exact live parent Agents, stop only their
* visible descendant Activations synchronously, then await admitted scoped
* materializations and release those forests child-first. The scoped cutoff
* lasts until each exact parent leaves the registry; unrelated parent trees
* remain live.
* @param parents - exact host-owned parent Agents entering teardown.
* @returns once every retained descendant Activation released its `AgentHandle`.
* @throws an aggregate error after all branches settle when any failed.
*/
async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>
/**
* Enumerate the parent's direct session-backed subagents without loading or
* resuming an Agent and without any query seam: the listing merges the live
* session store with optional session persistence (live-preferred) and
* serves each child's durable mode/label from the registered `subagent`
* projection unit down a three-rung ladder — the registry's watermark
* snapshot for a live child; for a cold one, a durable projection-cache
* row when the optional cache serves an own-suffix identity (its `seq`
* gate proves the value postdates the fork seed, where a child's own
* descriptor is immutable once appended), else one persistence inspection
* folded through the registry. The
* projection fold is the single classification authority; per-child
* diagnostics relay a fold that served no identity or a failed inspection,
* never a list-time descriptor parse. Absent persistence, enumeration is
* live-only (a cold child cannot be resumed then either, so its absence is
* capability absence, not an error). This service consults no Agent
* registrations, Activations, or providers.
*
* Every persistence read receives `signal`, and the listing rechecks
* cancellation around each of those awaits. Read rejections that settle
* after an abort become a stable `SubagentError` with code `CANCELLED`.
* @param parentSessionId - parent session whose direct children are listed.
* @param signal - caller-owned cancellation forwarded to persistence reads
* and observed around every read await.
* @returns children and per-child diagnostics ordered by `createdAt`, then id.
* @throws {@link SubagentError} when the projection registry or the session
* store is not mounted, or the caller cancels the listing.
*/
listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentListEntry[]>
/**
* Enumerate the root's complete session-backed subagent tree in stable
* pre-order from one live-preferred corpus, without loading or resuming an
* Agent. Ordinary sessions and one-shot children remain traversal nodes so
* continuable descendants below them are discovered; each returned entry
* adds its durable `parentId` and root-relative `depth`. Identity resolution,
* diagnostics, optional persistence, and cancellation follow the same
* projection-backed contract as {@link listChildren}.
* @param rootSessionId - session whose complete descendant tree is listed.
* @param signal - caller-owned cancellation forwarded to persistence reads
* and observed around every read await.
* @returns children and per-candidate diagnostics with tree position, in
* stable pre-order.
* @throws {@link SubagentError} under the same conditions as {@link listChildren}.
*/
listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]>
/**
* Register a provider under its name. Registration is effect-scoped and HMR
* safe; removing a provider blocks new starts but does not revoke runs that
* were already returned to their holders.
* @param provider - the trusted provider implementation.
* @returns the exact Cordis effect disposer.
*/
registerProvider(provider: SubagentProvider): () => void
/**
* Look up a provider by name.
* @param name - the provider name.
* @returns the provider, or undefined when absent.
*/
getProvider(name: string): SubagentProvider | undefined
/**
* List registered provider names in insertion order.
* @returns the registered names.
*/
list(): string[]
/**
* Establish a published child on the named provider. Capability and semantic
* checks run before delegation. Provider ownership lasts until its promise
* fulfills; a rejection therefore has no run for the caller to dispose and
* emits no run lifecycle events. Post-publication turn and infrastructure
* failures settle through the returned run.
* @param name - the provider to use.
* @param request - child label, prompt, parent, signal, and optional capabilities.
* @returns the published holder-owned run.
*/
async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>
```
Types: [Agent](core.md) · [ContentBlock](core.md) · [MessageId](core.md) · [SessionId](core.md)
Source: [`packages/subagent/subagent/src/index.ts:167`](../../packages/subagent/subagent/src/index.ts)
<a id="subagent-events"></a>
### `subagent/*` events
<a id="subagentend--emit"></a>
#### `subagent/end` — emit
A published child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience.
```ts cordis-catalog
/**
* A published child settled. Scope-filtered dispatch uses the same delegating
* parent carrier as `subagent/start`, so the lifecycle pair reaches the
* same scoped audience.
* @param info - the run identity and terminal outcome.
* @dshScopeScan unsupported
* @mode emit
*/
'subagent/end'(this: Scoped<SubagentService>, info: SubagentRunEndInfo): void
```
Types: [Scoped](scope.md)
Source: [`packages/subagent/subagent/src/index.ts:162`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentprovider-added--emit"></a>
#### `subagent/provider-added` — emit
A provider became resolvable in the registry.
```ts cordis-catalog
/**
* A provider became resolvable in the registry.
* @param provider - the registered provider.
* @mode emit
*/
'subagent/provider-added'(provider: SubagentProvider): void
```
Source: [`packages/subagent/subagent/src/index.ts:136`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentprovider-removed--emit"></a>
#### `subagent/provider-removed` — emit
A provider left the registry. Accepted runs remain holder-owned.
```ts cordis-catalog
/**
* A provider left the registry. Accepted runs remain holder-owned.
* @param name - the provider name that no longer resolves.
* @mode emit
*/
'subagent/provider-removed'(name: string): void
```
Source: [`packages/subagent/subagent/src/index.ts:142`](../../packages/subagent/subagent/src/index.ts)
<a id="subagentstart--emit"></a>
#### `subagent/start` — emit
A provider established a published child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with `subagent/end`.
```ts cordis-catalog
/**
* A provider established a published child. For in-process providers,
* `ctx.agents.get(info.id)` resolves during this notification.
* Scope-filtered dispatch keys the carrier by the delegating parent, so a
* parent-scoped listener observes only its own delegations. Paired with
* `subagent/end`.
* @param info - the provider and published child identity.
* @dshScopeScan unsupported
* @mode emit
*/
'subagent/start'(this: Scoped<SubagentService>, info: SubagentRunInfo): void
```
Types: [Scoped](scope.md)
Source: [`packages/subagent/subagent/src/index.ts:153`](../../packages/subagent/subagent/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/subprocess.md
subprocess.md: 023b122218ad1caa2b8e16c26b0bc8b0d4183c28
subprocess.zh.md: 5ee9c782c0ce73ca9a2e694450ed38b8d548dc09
# pnpm run verify-translation-pairing --write docs/subsystems/subprocess.md
subprocess.md: 0bc2bf86518bcc60bef02554b94471425f2ac433
subprocess.zh.md: 5d6425707edcf1952c25dbdc91dbf7aab3779350

View File

@@ -242,8 +242,83 @@ interface SubprocessOutcome {
`spawnTerminal(spec)` is the non-pipe process primitive. The provider allocates the controlling terminal and owns UTF-8 text transport, foreground-process-group inspection and signalling, and one awaited TERM-to-KILL operation that reaches quiescence for every session member the provider can still observe; providers document substrate-specific observability limits. The PTY backend remains responsible for prompt detection, readiness inference, scrollback, sandbox policy, and persistent-session ownership; ordinary `spawn()` cannot reconstruct controlling-terminal semantics.
The terminal spec fully specifies argv, cwd, environment overrides, dimensions, cleanup grace, and optional allocation cancellation. Its handle exposes `pid`, ordered output, `done`, `write`, `inspectForeground`, `signalForeground`, and awaited `terminate`; the exact public shapes are generated into the [`ctx.subprocess` service catalog](../cordis-catalog/services.md#ctxsubprocess--subprocessservice-abstract-seam).
The terminal spec fully specifies argv, cwd, environment overrides, dimensions, cleanup grace, and optional allocation cancellation. Its handle exposes `pid`, ordered output, `done`, `write`, `inspectForeground`, `signalForeground`, and awaited `terminate`; the exact public shapes are generated into the [`ctx.subprocess` service catalog](#ctxsubprocess--subprocessservice-abstract-seam).
## Service behavior
The abstract [`SubprocessService`](../../packages/subprocess/subprocess/src/index.ts) seam defines execution-world coordinates, executable lookup, ordinary `spawn`, and `spawnTerminal`. [`LocalSubprocessService`](../../packages/subprocess/subprocess-local/src/index.ts) implements them with detached process trees, per-disposition wiring, credential scrubbing, `node-pty`, platform process inspection, and terminate-and-join disposal. See [`dsh-subprocess`](../../packages/subprocess/subprocess/README.md) for the interface contract and [`dsh-subprocess-local`](../../packages/subprocess/subprocess-local/README.md) for local mechanics.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxe2b--e2bsandboxservice"></a>
### `ctx.e2b` — `E2BSandboxService`
Creates one lazily consumable E2B SDK handle and deletes the sandbox at timeout or disposal. Creation begins at plugin construction; adapters await getSandbox before their first operation.
```ts cordis-catalog
/**
* Return the shared live SDK handle.
* @returns the created sandbox after the configured cwd exists.
* @throws when E2B rejects creation or the service is disposing.
*/
async getSandbox(): Promise<Sandbox>
```
Source: [`packages/e2b/e2b/src/index.ts:74`](../../packages/e2b/e2b/src/index.ts)
<a id="ctxsubprocess--subprocessservice-abstract-seam"></a>
### `ctx.subprocess` — `SubprocessService` (abstract seam)
Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
Implementations must honor these semantics:
- Executable paths belong to one execution world shared with the mounted filesystem provider.
- spawn returns immediately with a live handle; `done` resolves at process close with exit facts and rejects only for spawn-level failures.
- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here.
- SubprocessHandle.terminate (and the spec's abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — tree-scoped on every platform. SubprocessHandle.waitForExit observes whole-tree liveness, so a consumer-owned teardown ladder can hold each tier on real quiescence.
- Disposal of the service terminates all still-running managed processes and awaits their exit.
- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits.
```ts cordis-catalog
/**
* 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.
* @param command - absolute executable path or bare PATH name.
* @param env - explicit environment entries used for lookup.
* @param signal - aborts remote or local lookup.
* @returns a canonical executable path.
*/
abstract resolveExecutable( command: string, env?: Readonly<Record<string, string>>, signal?: AbortSignal, ): Promise<string>
/**
* Start one managed child process from a fully-specified spec; this seam
* applies no defaults.
* @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment.
* @returns the live process handle (streams/readers, signalling, outcome promise).
*/
abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle
/**
* Allocate a real terminal and start one owned process session. This is the
* only non-pipe process primitive: implementations own terminal byte I/O,
* foreground groups, signals, and complete session-tree cleanup.
* @param spec - fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation.
* @returns the live terminal handle after allocation succeeds.
*/
abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>
```
Source: [`packages/subprocess/subprocess/src/index.ts:102`](../../packages/subprocess/subprocess/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -242,8 +242,83 @@ interface SubprocessOutcome {
`spawnTerminal(spec)` 是非管道进程原语。提供方分配控制终端,并负责 UTF-8 文本传输、前台进程组检查与信号发送,以及一项须等待的 TERM→KILL 操作该操作会使提供方仍可观察到的每个会话成员完全停稳提供方则会记录执行基底特有的可观察性限制。PTY 后端仍负责提示符检测、就绪推断、scrollback、沙箱策略和持久会话所有权普通 `spawn()` 无法重建控制终端语义。
终端 spec 完全指定 argv、cwd、环境覆盖、尺寸、清理宽限期与可选的分配取消。其句柄公开 `pid`、有序输出、`done`、`write`、`inspectForeground`、`signalForeground` 和须等待的 `terminate`;确切的公共形状生成到 [`ctx.subprocess` 服务目录](../cordis-catalog/services.md#ctxsubprocess--subprocessservice-abstract-seam)中。
终端 spec 完全指定 argv、cwd、环境覆盖、尺寸、清理宽限期与可选的分配取消。其句柄公开 `pid`、有序输出、`done`、`write`、`inspectForeground`、`signalForeground` 和须等待的 `terminate`;确切的公共形状生成到 [`ctx.subprocess` 服务目录](#ctxsubprocess--subprocessservice-abstract-seam)中。
## 服务行为
抽象的 [`SubprocessService`](../../packages/subprocess/subprocess/src/index.ts) seam 定义执行世界坐标、可执行文件查找、普通 `spawn` 与 `spawnTerminal`。[`LocalSubprocessService`](../../packages/subprocess/subprocess-local/src/index.ts) 以 detached 进程树、按处置方式接线、凭据清除、`node-pty`、平台进程检查,以及先终止再等待退出的资源释放实现这些能力。接口契约见 [`dsh-subprocess`](../../packages/subprocess/subprocess/README.md),本地机制见 [`dsh-subprocess-local`](../../packages/subprocess/subprocess-local/README.md)。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxe2b--e2bsandboxservice"></a>
### `ctx.e2b` — `E2BSandboxService`
Creates one lazily consumable E2B SDK handle and deletes the sandbox at timeout or disposal. Creation begins at plugin construction; adapters await getSandbox before their first operation.
```ts cordis-catalog
/**
* Return the shared live SDK handle.
* @returns the created sandbox after the configured cwd exists.
* @throws when E2B rejects creation or the service is disposing.
*/
async getSandbox(): Promise<Sandbox>
```
Source: [`packages/e2b/e2b/src/index.ts:74`](../../packages/e2b/e2b/src/index.ts)
<a id="ctxsubprocess--subprocessservice-abstract-seam"></a>
### `ctx.subprocess` — `SubprocessService` (abstract seam)
Abstract subprocess service. Subclass, implement spawn, and load the subclass as a plugin — it registers as `ctx.subprocess` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
Implementations must honor these semantics:
- Executable paths belong to one execution world shared with the mounted filesystem provider.
- spawn returns immediately with a live handle; `done` resolves at process close with exit facts and rejects only for spawn-level failures.
- Collect-mode readers are offset-based and non-consuming, so independent readers never consume one another's output; lossy reads report truncation and the spill file holding the complete stream when one exists. Piped streams are handed to the caller raw and never buffered here.
- SubprocessHandle.terminate (and the spec's abort signal) escalates SIGTERM→grace→SIGKILL — the only termination verb — tree-scoped on every platform. SubprocessHandle.waitForExit observes whole-tree liveness, so a consumer-owned teardown ladder can hold each tier on real quiescence.
- Disposal of the service terminates all still-running managed processes and awaits their exit.
- spawnTerminal owns terminal allocation, text transport, foreground groups, signalling, and whole-session quiescence behind one awaited termination method; readiness and persistent-shell policy stay in the PTY consumer. Its output stream ends after queued terminal output when the top-level process exits.
```ts cordis-catalog
/**
* 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.
* @param command - absolute executable path or bare PATH name.
* @param env - explicit environment entries used for lookup.
* @param signal - aborts remote or local lookup.
* @returns a canonical executable path.
*/
abstract resolveExecutable( command: string, env?: Readonly<Record<string, string>>, signal?: AbortSignal, ): Promise<string>
/**
* Start one managed child process from a fully-specified spec; this seam
* applies no defaults.
* @param spec - argv, directory, stdio dispositions, grace, cancellation, and environment.
* @returns the live process handle (streams/readers, signalling, outcome promise).
*/
abstract spawn(spec: SubprocessSpawnSpec): SubprocessHandle
/**
* Allocate a real terminal and start one owned process session. This is the
* only non-pipe process primitive: implementations own terminal byte I/O,
* foreground groups, signals, and complete session-tree cleanup.
* @param spec - fully specified argv, cwd, environment, dimensions, grace, and allocation cancellation.
* @returns the live terminal handle after allocation succeeds.
*/
abstract spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>
```
Source: [`packages/subprocess/subprocess/src/index.ts:102`](../../packages/subprocess/subprocess/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/system-prompt.md
system-prompt.md: 59193c1881abcadbc8a1778cde92f5a6572eee24
system-prompt.zh.md: 41e45417817895ccf6def70e510eca7422a65e41
system-prompt.md: 94ce40f8bf98dd4efe3514879c2527c2a7bd3b21
system-prompt.zh.md: 37d366774089f91756433602a719e99b57747c17

View File

@@ -76,3 +76,113 @@ interface PromptContext {
readonly text: string | ((context: AssembleContext) => string)
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsystemprompt--systemprompt"></a>
### `ctx.systemPrompt` — `SystemPrompt`
Registry service for the prompt inputs assembled before each model step.
```ts cordis-catalog
/**
* Register an ordered prompt section in the calling context's scope. A scoped
* section shadows a global section with the same name; duplicates within one
* layer and non-finite orders throw. Registration and disposal emit
* `system-prompt/change`.
* @param section - the section to register.
* @returns the exact Cordis effect disposer.
*/
section(section: PromptSection): () => void
/**
* Register ordered dynamic context in the calling context's scope. Scoped
* entries shadow global entries with the same name.
* @param context - the context contribution to register.
* @returns the exact Cordis effect disposer.
*/
context(context: PromptContext): () => void
/**
* Register a tool-schema provider in the calling context's scope. Global and
* matching scoped providers both contribute; returning the reserved
* {@link TOOL_ORDER_REST} name makes assembly fail.
* @param provider - evaluated for each assembly with its context.
* @returns the exact Cordis effect disposer.
*/
tools(provider: (context: AssembleContext) => ToolProviderResult): () => void
/**
* Register a prompt variable in the calling context's scope. Scoped values
* shadow globals; invalid or duplicate names throw. A provider may return
* `undefined`, but rendering a section that references that value then fails.
* @param name - the `[a-z][a-z0-9_]*` reference name.
* @param provider - evaluated for each assembly.
* @returns the exact Cordis effect disposer.
*/
variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void
/**
* Assemble global and scoped providers, detach tool parameters, apply
* canonical ordering, then run the assembly waterfall. Scoped sections and
* variables shadow globals; the returned waterfall value is authoritative.
* @param context - the optional scope and plugin-defined assembly fields.
* @returns the authoritative post-waterfall assembly.
*/
async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
```
Source: [`packages/core/system-prompt/src/index.ts:314`](../../packages/core/system-prompt/src/index.ts)
<a id="system-prompt-events"></a>
### `system-prompt/*` events
<a id="system-promptassemble--waterfall"></a>
#### `system-prompt/assemble` — waterfall
Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns.
```ts cordis-catalog
/**
* Expert waterfall over the assembled sections, contexts, tools, and variables.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
* receive only that scope's assemblies. The returned value is authoritative.
* A supplied signal controls only this explicit assembly request and must not
* be retained to control later turns.
* @param assembly - the mutable assembly built from registered providers.
* @param context - the caller's per-assembly context.
* @mode waterfall
*/
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
```
Types: [Scoped](scope.md)
Source: [`packages/core/system-prompt/src/index.ts:29`](../../packages/core/system-prompt/src/index.ts)
<a id="system-promptchange--emit"></a>
#### `system-prompt/change` — emit
Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope.
```ts cordis-catalog
/**
* Emitted when any prompt provider changes. This registry notification is
* unfiltered because a global change affects every scope.
* @mode emit
*/
'system-prompt/change'(): void
```
Source: [`packages/core/system-prompt/src/index.ts:35`](../../packages/core/system-prompt/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -76,3 +76,113 @@ interface PromptContext {
readonly text: string | ((context: AssembleContext) => string)
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxsystemprompt--systemprompt"></a>
### `ctx.systemPrompt` — `SystemPrompt`
Registry service for the prompt inputs assembled before each model step.
```ts cordis-catalog
/**
* Register an ordered prompt section in the calling context's scope. A scoped
* section shadows a global section with the same name; duplicates within one
* layer and non-finite orders throw. Registration and disposal emit
* `system-prompt/change`.
* @param section - the section to register.
* @returns the exact Cordis effect disposer.
*/
section(section: PromptSection): () => void
/**
* Register ordered dynamic context in the calling context's scope. Scoped
* entries shadow global entries with the same name.
* @param context - the context contribution to register.
* @returns the exact Cordis effect disposer.
*/
context(context: PromptContext): () => void
/**
* Register a tool-schema provider in the calling context's scope. Global and
* matching scoped providers both contribute; returning the reserved
* {@link TOOL_ORDER_REST} name makes assembly fail.
* @param provider - evaluated for each assembly with its context.
* @returns the exact Cordis effect disposer.
*/
tools(provider: (context: AssembleContext) => ToolProviderResult): () => void
/**
* Register a prompt variable in the calling context's scope. Scoped values
* shadow globals; invalid or duplicate names throw. A provider may return
* `undefined`, but rendering a section that references that value then fails.
* @param name - the `[a-z][a-z0-9_]*` reference name.
* @param provider - evaluated for each assembly.
* @returns the exact Cordis effect disposer.
*/
variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void
/**
* Assemble global and scoped providers, detach tool parameters, apply
* canonical ordering, then run the assembly waterfall. Scoped sections and
* variables shadow globals; the returned waterfall value is authoritative.
* @param context - the optional scope and plugin-defined assembly fields.
* @returns the authoritative post-waterfall assembly.
*/
async assemble(context: AssembleContext = {}): Promise<PromptAssembly>
```
Source: [`packages/core/system-prompt/src/index.ts:314`](../../packages/core/system-prompt/src/index.ts)
<a id="system-prompt-events"></a>
### `system-prompt/*` events
<a id="system-promptassemble--waterfall"></a>
#### `system-prompt/assemble` — waterfall
Expert waterfall over the assembled sections, contexts, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. A supplied signal controls only this explicit assembly request and must not be retained to control later turns.
```ts cordis-catalog
/**
* Expert waterfall over the assembled sections, contexts, tools, and variables.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners
* receive only that scope's assemblies. The returned value is authoritative.
* A supplied signal controls only this explicit assembly request and must not
* be retained to control later turns.
* @param assembly - the mutable assembly built from registered providers.
* @param context - the caller's per-assembly context.
* @mode waterfall
*/
'system-prompt/assemble'(this: Scoped<SystemPrompt>, assembly: PromptAssembly, context: AssembleContext, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
```
Types: [Scoped](scope.md)
Source: [`packages/core/system-prompt/src/index.ts:29`](../../packages/core/system-prompt/src/index.ts)
<a id="system-promptchange--emit"></a>
#### `system-prompt/change` — emit
Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope.
```ts cordis-catalog
/**
* Emitted when any prompt provider changes. This registry notification is
* unfiltered because a global change affects every scope.
* @mode emit
*/
'system-prompt/change'(): void
```
Source: [`packages/core/system-prompt/src/index.ts:35`](../../packages/core/system-prompt/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/tasks.md
tasks.md: a38055d3ef7aa18e62678f92eb5ac5ae2a09c205
tasks.zh.md: f34d42e713c3a0c11cbf88d52e573bb100c52493
# pnpm run verify-translation-pairing --write docs/subsystems/tasks.md
tasks.md: 6237607307cbab8511f968ed591e3e3b8b845c6a
tasks.zh.md: 99a6fcc4f54c1b52e5c6fd393327cc768f064297

View File

@@ -152,3 +152,109 @@ interface TaskRead {
## Service behavior
The abstract [`TaskService`](../../packages/tasks/tasks/src/index.ts) seam defines atomic `start`, caller-scoped `get` and `list`, `read`, `kill`, bounded `wait`, contained `onTaskDone` listeners, and the `attachSurface` availability fence; [`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) is the process-local implementation. Authorization compares owner sessions; owner cleanup selects the exact registered `Agent` instance. See [`dsh-tasks`](../../packages/tasks/tasks/README.md) for the seam contract, [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md) for the registry lifecycle, and [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md) for the model-facing surface.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxtasks--taskservice-abstract-seam"></a>
### `ctx.tasks` — `TaskService` (abstract seam)
Abstract background task registry. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.tasks` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
Implementations must honor these semantics:
- Registrations outlive producer and control-surface fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record.
- Owned-task access is fenced by the owner's session id. Ids are predictable, so authorization — not secrecy — is the boundary.
- Settlement is first-wins: one terminal record, one round of contained listener notification, and released waiters, even against a late producer outcome.
- start refuses work while no control surface is attached, so a producer cannot start work that callers cannot collect or stop.
```ts cordis-catalog
/**
* Preflight access, validation, and owner cleanup before starting and
* atomically registering work. A throwing starter leaves nothing registered;
* after it returns, registration cannot fail. Settlement records the outcome,
* notifies listeners, and releases waiters.
* @param spec - task identity, owner, and synchronous starter.
* @returns the registry-issued `<kind>-N` id.
*/
abstract start(spec: TaskStart): TaskId
/**
* List caller-owned and unowned tasks in registration order without exposing
* another session's labels.
* @param caller - reading agent; a non-agent caller sees only unowned tasks.
* @returns fresh snapshots.
*/
abstract list(caller?: Agent): TaskSnapshot[]
/**
* Return a non-consuming snapshot without changing its read cursor or notice
* state. Throws for an unknown or foreign task.
* @param id - task to look up.
* @param caller - reading agent checked against the owner.
* @returns a fresh snapshot.
*/
abstract get(id: TaskId, caller?: Agent): TaskSnapshot
/**
* Read the next stream delta, or the idempotent final output after settlement.
* A terminal read marks the task reported. Throws for an unknown or foreign
* task.
* @param id - task to read.
* @param caller - reading agent checked against the owner.
* @returns output text and the post-read snapshot.
*/
abstract read(id: TaskId, caller?: Agent): TaskRead
/**
* Request cancellation, then mark the task stopping and reported. A producer
* throw propagates without changing task state. Throws for an unknown or
* foreign task.
* @param id - task to cancel.
* @param caller - killing agent checked against the owner.
* @param reason - logged reason forwarded to the producer.
* @returns `requested` for live work, otherwise `already-finished`.
*/
abstract kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-finished'
/**
* Wait for settlement or timeout without cancelling the task. Caller abort
* rejects only while the task is live; after settlement the terminal
* snapshot wins so a notice suppressed for this waiter is still delivered.
* Throws for invalid, unknown, or foreign input.
* @param id - task to wait for.
* @param timeoutMs - positive finite wait bound in milliseconds.
* @param caller - waiting agent checked against the owner.
* @param signal - optional cancellation of the wait itself.
* @returns snapshot at settlement or timeout.
*/
abstract wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise<TaskSnapshot>
/**
* Register an effect-scoped completion listener. Each listener is contained;
* returned promises are observed but not awaited. No listener runs after
* service disposal.
* @param listener - receives each terminal snapshot and its exact owner.
* @returns disposer that unregisters the listener.
*/
abstract onTaskDone(listener: TaskDoneListener): () => void
/**
* Attach an effect-scoped surface that can read and stop tasks. {@link start}
* refuses work while none is attached.
* @param name - diagnostic label; duplicate names remain independent.
* @returns disposer that detaches this surface.
*/
abstract attachSurface(name: string): () => void
```
Types: [Agent](core.md)
Source: [`packages/tasks/tasks/src/index.ts:50`](../../packages/tasks/tasks/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -152,3 +152,109 @@ interface TaskRead {
## 服务行为
抽象的 [`TaskService`](../../packages/tasks/tasks/src/index.ts) seam 定义原子 `start`、限定调用方作用域的 `get` 和 `list`、`read`、`kill`、有界 `wait`、故障隔离的 `onTaskDone` 监听器,以及 `attachSurface` 可用性防线;[`LocalTaskService`](../../packages/tasks/tasks-local/src/index.ts) 是其进程局部实现。授权会比较拥有者会话;拥有者清理会选择确切的已注册 `Agent` 实例。seam 契约见 [`dsh-tasks`](../../packages/tasks/tasks/README.md),注册表生命周期见 [`dsh-tasks-local`](../../packages/tasks/tasks-local/README.md),面向模型的接口见 [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md)。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxtasks--taskservice-abstract-seam"></a>
### `ctx.tasks` — `TaskService` (abstract seam)
Abstract background task registry. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.tasks` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
Implementations must honor these semantics:
- Registrations outlive producer and control-surface fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record.
- Owned-task access is fenced by the owner's session id. Ids are predictable, so authorization — not secrecy — is the boundary.
- Settlement is first-wins: one terminal record, one round of contained listener notification, and released waiters, even against a late producer outcome.
- start refuses work while no control surface is attached, so a producer cannot start work that callers cannot collect or stop.
```ts cordis-catalog
/**
* Preflight access, validation, and owner cleanup before starting and
* atomically registering work. A throwing starter leaves nothing registered;
* after it returns, registration cannot fail. Settlement records the outcome,
* notifies listeners, and releases waiters.
* @param spec - task identity, owner, and synchronous starter.
* @returns the registry-issued `<kind>-N` id.
*/
abstract start(spec: TaskStart): TaskId
/**
* List caller-owned and unowned tasks in registration order without exposing
* another session's labels.
* @param caller - reading agent; a non-agent caller sees only unowned tasks.
* @returns fresh snapshots.
*/
abstract list(caller?: Agent): TaskSnapshot[]
/**
* Return a non-consuming snapshot without changing its read cursor or notice
* state. Throws for an unknown or foreign task.
* @param id - task to look up.
* @param caller - reading agent checked against the owner.
* @returns a fresh snapshot.
*/
abstract get(id: TaskId, caller?: Agent): TaskSnapshot
/**
* Read the next stream delta, or the idempotent final output after settlement.
* A terminal read marks the task reported. Throws for an unknown or foreign
* task.
* @param id - task to read.
* @param caller - reading agent checked against the owner.
* @returns output text and the post-read snapshot.
*/
abstract read(id: TaskId, caller?: Agent): TaskRead
/**
* Request cancellation, then mark the task stopping and reported. A producer
* throw propagates without changing task state. Throws for an unknown or
* foreign task.
* @param id - task to cancel.
* @param caller - killing agent checked against the owner.
* @param reason - logged reason forwarded to the producer.
* @returns `requested` for live work, otherwise `already-finished`.
*/
abstract kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-finished'
/**
* Wait for settlement or timeout without cancelling the task. Caller abort
* rejects only while the task is live; after settlement the terminal
* snapshot wins so a notice suppressed for this waiter is still delivered.
* Throws for invalid, unknown, or foreign input.
* @param id - task to wait for.
* @param timeoutMs - positive finite wait bound in milliseconds.
* @param caller - waiting agent checked against the owner.
* @param signal - optional cancellation of the wait itself.
* @returns snapshot at settlement or timeout.
*/
abstract wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise<TaskSnapshot>
/**
* Register an effect-scoped completion listener. Each listener is contained;
* returned promises are observed but not awaited. No listener runs after
* service disposal.
* @param listener - receives each terminal snapshot and its exact owner.
* @returns disposer that unregisters the listener.
*/
abstract onTaskDone(listener: TaskDoneListener): () => void
/**
* Attach an effect-scoped surface that can read and stop tasks. {@link start}
* refuses work while none is attached.
* @param name - diagnostic label; duplicate names remain independent.
* @returns disposer that detaches this surface.
*/
abstract attachSurface(name: string): () => void
```
Types: [Agent](core.md)
Source: [`packages/tasks/tasks/src/index.ts:50`](../../packages/tasks/tasks/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/telemetry.md
telemetry.md: cf570723650c32f048ee797923a58f09c777455d
telemetry.zh.md: 1bcaa86264e3afd741d66d6ee718946e6151496f
telemetry.md: 131c5814f29d70c356989e62fda1db5c1c5448d6
telemetry.zh.md: 5d4a9d9994457bc06a9e4194c4807f88789a3cf0

View File

@@ -105,8 +105,76 @@ interface TelemetryBackend {
}
```
`Telemetry` (`ctx.telemetry`, [signatures](../cordis-catalog/services.md#ctxtelemetry--telemetry-abstract-seam)) is the contract's loadable form — one implementation per context, duplicate load throws — and a backend composes the seam's `TelemetryCoordinator` in its constructor to install the capture side.
`Telemetry` (`ctx.telemetry`, [signatures](#ctxtelemetry--telemetry-abstract-seam)) is the contract's loadable form — one implementation per context, duplicate load throws — and a backend composes the seam's `TelemetryCoordinator` in its constructor to install the capture side.
## The redact waterfall: `telemetry/record`
Every record passes the `telemetry/record` [waterfall](../cordis-primer.md#cordis-waterfall-semantics) between projection and `emit()` ([event entry](../cordis-catalog/events.md#telemetryrecord--waterfall)). The seam ships NO rules of its own: with no listener mounted, records reach the backend exactly as captured, so exported data is precisely as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath; a throwing listener withholds that one record fail-closed inside the coordinator's containment. Redaction applies to the exported copy only — the canonical session log is never rewritten.
Every record passes the `telemetry/record` [waterfall](../cordis-primer.md#cordis-waterfall-semantics) between projection and `emit()` ([event entry](#telemetryrecord--waterfall)). The seam ships NO rules of its own: with no listener mounted, records reach the backend exactly as captured, so exported data is precisely as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath; a throwing listener withholds that one record fail-closed inside the coordinator's containment. Redaction applies to the exported copy only — the canonical session log is never rewritten.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxtelemetry--telemetry-abstract-seam"></a>
### `ctx.telemetry` — `Telemetry` (abstract seam)
The backend contract in its loadable form: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side.
```ts cordis-catalog
/**
* See {@link TelemetryBackend.emit} — the seam declaration is the contract's one home.
* @param record - the logical record to report; owned by the backend after the call.
*/
abstract emit(record: TelemetryRecord): void
/** See {@link TelemetryBackend.flush}. */
flush?(): void
/**
* See {@link TelemetryBackend.shutdown}.
* @returns resolves when the backend's pipeline has quiesced.
*/
abstract shutdown(): Promise<void>
```
Source: [`packages/session/session-telemetry/src/index.ts:140`](../../packages/session/session-telemetry/src/index.ts)
<a id="telemetry-events"></a>
### `telemetry/*` events
<a id="telemetryrecord--waterfall"></a>
#### `telemetry/record` — waterfall
Transform one outbound record before it reaches the backend. This waterfall is the seam's redaction extension point. It ships NO rules of its own: the innermost `next()` passes the record through unchanged, and with no listener mounted records reach the backend as captured, so exported data is exactly as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath. Dispatched synchronously on the capture hot path inside the coordinator's containment: a throwing listener withholds that one record (fail-closed) and never reaches the agent loop. Live capture dispatches at append time; on-demand capture dispatches while reading the canonical log. Redaction applies to the exported copy only; the canonical session log is never rewritten.
```ts cordis-catalog
/**
* Transform one outbound record before it reaches the backend. This
* waterfall is the seam's redaction extension point. It ships NO rules
* of its own: the
* innermost `next()` passes the record through unchanged, and with no
* listener mounted records reach the backend as captured, so exported
* data is exactly as clean as the rules a deployment mounts. Listeners
* stack by transforming `next()`'s return value; returning without
* `next()` replaces everything beneath. Dispatched synchronously on the
* capture hot path inside the coordinator's containment: a throwing
* listener withholds that one record (fail-closed) and never reaches the
* agent loop. Live capture dispatches at append time; on-demand capture
* dispatches while reading the canonical log. Redaction applies to the
* exported copy only; the canonical session log is never rewritten.
* @param record - the candidate record, already the coordinator's own deep
* copy; listeners return a (possibly new) record and must not mutate it.
* @mode waterfall
*/
'telemetry/record'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord
```
Source: [`packages/session/session-telemetry/src/index.ts:43`](../../packages/session/session-telemetry/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -105,8 +105,76 @@ interface TelemetryBackend {
}
```
`Telemetry``ctx.telemetry`[签名](../cordis-catalog/services.md#ctxtelemetry--telemetry-abstract-seam))是该契约的可加载形态:每个上下文只允许一个实现,重复加载会抛出异常;后端在其构造函数中组合 seam 的 `TelemetryCoordinator`,以此装配捕获侧。
`Telemetry``ctx.telemetry`[签名](#ctxtelemetry--telemetry-abstract-seam))是该契约的可加载形态:每个上下文只允许一个实现,重复加载会抛出异常;后端在其构造函数中组合 seam 的 `TelemetryCoordinator`,以此装配捕获侧。
## 脱敏 waterfall`telemetry/record`
每条记录在投影与 `emit()` 之间都要经过 `telemetry/record` [waterfall](../cordis-primer.md#cordis-waterfall-semantics)[事件条目](../cordis-catalog/events.md#telemetryrecord--waterfall)。seam 自身不带任何规则:未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式扣下这一条记录。脱敏只作用于导出副本;权威会话日志永不改写。
每条记录在投影与 `emit()` 之间都要经过 `telemetry/record` [waterfall](../cordis-primer.md#cordis-waterfall-semantics)[事件条目](#telemetryrecord--waterfall)。seam 自身不带任何规则:未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式扣下这一条记录。脱敏只作用于导出副本;权威会话日志永不改写。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxtelemetry--telemetry-abstract-seam"></a>
### `ctx.telemetry` — `Telemetry` (abstract seam)
The backend contract in its loadable form: one implementation per context — the cordis `Service` registration under the `telemetry` key throws on a duplicate, cordis' standard behavior. A backend composes a TelemetryCoordinator in its constructor to install the capture side.
```ts cordis-catalog
/**
* See {@link TelemetryBackend.emit} — the seam declaration is the contract's one home.
* @param record - the logical record to report; owned by the backend after the call.
*/
abstract emit(record: TelemetryRecord): void
/** See {@link TelemetryBackend.flush}. */
flush?(): void
/**
* See {@link TelemetryBackend.shutdown}.
* @returns resolves when the backend's pipeline has quiesced.
*/
abstract shutdown(): Promise<void>
```
Source: [`packages/session/session-telemetry/src/index.ts:140`](../../packages/session/session-telemetry/src/index.ts)
<a id="telemetry-events"></a>
### `telemetry/*` events
<a id="telemetryrecord--waterfall"></a>
#### `telemetry/record` — waterfall
Transform one outbound record before it reaches the backend. This waterfall is the seam's redaction extension point. It ships NO rules of its own: the innermost `next()` passes the record through unchanged, and with no listener mounted records reach the backend as captured, so exported data is exactly as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath. Dispatched synchronously on the capture hot path inside the coordinator's containment: a throwing listener withholds that one record (fail-closed) and never reaches the agent loop. Live capture dispatches at append time; on-demand capture dispatches while reading the canonical log. Redaction applies to the exported copy only; the canonical session log is never rewritten.
```ts cordis-catalog
/**
* Transform one outbound record before it reaches the backend. This
* waterfall is the seam's redaction extension point. It ships NO rules
* of its own: the
* innermost `next()` passes the record through unchanged, and with no
* listener mounted records reach the backend as captured, so exported
* data is exactly as clean as the rules a deployment mounts. Listeners
* stack by transforming `next()`'s return value; returning without
* `next()` replaces everything beneath. Dispatched synchronously on the
* capture hot path inside the coordinator's containment: a throwing
* listener withholds that one record (fail-closed) and never reaches the
* agent loop. Live capture dispatches at append time; on-demand capture
* dispatches while reading the canonical log. Redaction applies to the
* exported copy only; the canonical session log is never rewritten.
* @param record - the candidate record, already the coordinator's own deep
* copy; listeners return a (possibly new) record and must not mutate it.
* @mode waterfall
*/
'telemetry/record'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord
```
Source: [`packages/session/session-telemetry/src/index.ts:43`](../../packages/session/session-telemetry/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/core-data-structures/token-meter.md
token-meter.md: 05784e294485a11acf0e4c8972e4083b1786c943
token-meter.zh.md: 0474d86188a111014e6d72e9962121c08a228d73
# pnpm run verify-translation-pairing --write docs/subsystems/token-meter.md
token-meter.md: 0f53955b3e6afce84520314f4de453edaa897158
token-meter.zh.md: 3ba22c54d599982d3f7bdbcb9160b6ffc3255e07

View File

@@ -41,3 +41,50 @@ interface TokenSurfaceNode {
```
Surface order is authoritative; replacement nodes can have higher durable seqs than later positional nodes. The snapshot is immutable and does not grow when the underlying replay fold advances.
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxtokenmeter--tokenmeterservice"></a>
### `ctx.tokenMeter` — `TokenMeterService`
Replay owner for one service-wide estimator and isolated per-session folds.
```ts cordis-catalog
/**
* Measure current request pressure and surface through the durable tail.
*
* Provider usage is reused only when the latest successful call's canonical
* request envelope matches `requestHeader` and its total is no lower than
* that call's full heuristic anchor; otherwise the complete envelope and
* surface are heuristically repriced.
*
* `requestHeader` affects request pressure only; surface fields always
* describe the current session surface. Every call clones those positional
* nodes, so measurement is O(surface).
*
* @param session - session to replay through its current durable tail.
* @param requestHeader - optional effective request envelope replacing the latest logged header.
* @returns a detached deeply immutable pressure and surface measurement.
*/
measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement
/**
* Heuristically price one model-visible message (instance face of the pure
* `estimateMessage` export from `estimate.ts`).
* @param message - message to price without mutation.
* @returns content and role-framing tokens under the fixed service heuristic.
*/
estimateMessage(message: Message): number
```
Types: [EpochHeader](session.md) · [Message](core.md) · [Session](session.md)
Source: [`packages/llm/token-meter/src/index.ts:74`](../../packages/llm/token-meter/src/index.ts)
<!-- END GENERATED cordis-surface -->

View File

@@ -41,3 +41,50 @@ interface TokenSurfaceNode {
```
表层顺序具有权威性;替换节点的持久 seq 可能高于位置排在其后的节点。该快照不可变,不会随底层回放折叠推进而增长。
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis surface
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` surface lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxtokenmeter--tokenmeterservice"></a>
### `ctx.tokenMeter` — `TokenMeterService`
Replay owner for one service-wide estimator and isolated per-session folds.
```ts cordis-catalog
/**
* Measure current request pressure and surface through the durable tail.
*
* Provider usage is reused only when the latest successful call's canonical
* request envelope matches `requestHeader` and its total is no lower than
* that call's full heuristic anchor; otherwise the complete envelope and
* surface are heuristically repriced.
*
* `requestHeader` affects request pressure only; surface fields always
* describe the current session surface. Every call clones those positional
* nodes, so measurement is O(surface).
*
* @param session - session to replay through its current durable tail.
* @param requestHeader - optional effective request envelope replacing the latest logged header.
* @returns a detached deeply immutable pressure and surface measurement.
*/
measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement
/**
* Heuristically price one model-visible message (instance face of the pure
* `estimateMessage` export from `estimate.ts`).
* @param message - message to price without mutation.
* @returns content and role-framing tokens under the fixed service heuristic.
*/
estimateMessage(message: Message): number
```
Types: [EpochHeader](session.md) · [Message](core.md) · [Session](session.md)
Source: [`packages/llm/token-meter/src/index.ts:74`](../../packages/llm/token-meter/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/tools.md
tools.md: 853eed16cff451edcc32bc3aa5c6bc7cabb0f518
tools.zh.md: ec8809f6bebd185404828c5c5879f8eff832ea9a
tools.md: 5d72bd54f0dc5995218087030f24c467c8ccbb38
tools.zh.md: 2340d4c2d92c51aa9cc89f4694b626c76d05140b

Some files were not shown because too many files have changed in this diff Show More