Merge refreshed rfc/pty into feature/persistent-pty-sessions

# Conflicts:
#	docs/architecture.md
#	docs/capability-seams.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/module-graph.md
#	docs/tool-catalog.md
#	examples/acp-agent/tests/acp.snapshot.ts
#	examples/headless-agent/tests/headless.snapshot.ts
#	examples/package.json
#	packages/README.md
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/tools/tests/gen-tool-catalog.spec.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
#	scripts/gen-tool-catalog.ts
#	scripts/type-equiv.manifest.json
#	website/.vitepress/config/api-sidebar.json
This commit is contained in:
Tianyi Cui
2026-07-22 21:12:16 +08:00
1856 changed files with 106649 additions and 23384 deletions

View File

@@ -18,9 +18,12 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
| [llm-streaming.md](llm-streaming.md) | the `StreamChunk` wire protocol + adapter contract, `BlockAssembler`, the `LlmAdapter` seam |
| [token-meter.md](token-meter.md) | immutable scalar and positional replay measurements with consumed-log revisions |
| [scope.md](scope.md) | scoped registration identity, dispatch carriers, and the owned `Scope` context |
| [goal.md](goal.md) | persisted goal identity, lifecycle snapshots, activation, change records, and round attribution |
| [commands.md](commands.md) | the human-command seam: definitions, adapter discovery, direct invocation, results, and parsing views |
| [session.md](session.md) | the full `SessionEventMap` variant catalog, `TurnTrigger`/`TurnEndReason`, `deriveMessages()`, the turn-enclosure invariant |
| [persistence.md](persistence.md) | the durability seam: `SessionPersistence`, JSONL + SQLite backends, `session/flush`, crash recovery, `SessionHeader` |
| [session-query.md](session-query.md) | logical records, bounded exact-event reads, and relationship traces |
| [session-title.md](session-title.md) | durable title snapshots, source provenance, and the asynchronous provider contract |
| [system-prompt.md](system-prompt.md) | per-assembly context, tool-provider results, prompt sections, and cooperative assembly |
| [tools.md](tools.md) | `ToolDefinition` full fields, the schema DSL, `ToolExecution`/`ToolResult`, tool-presentation UI types, and the guarded execution pipeline |
| [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy |
@@ -30,6 +33,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
| [sandbox.md](sandbox.md) | the process-confinement seam: file-effect modes, `SandboxPolicy`, `ConfinedArgv`, enforcement and fail-closed errors |
| [code-runtime.md](code-runtime.md) | the code-execution seam: `CodeRunRequest`/`Result`, binding namespaces, captured logs, the `CodeRunFailure` taxonomy |
| [filesystem.md](filesystem.md) | the filesystem seam: `FsTarget`, read/write/edit outcomes, observed-file state, `FsErrorCode` |
| [lsp.md](lsp.md) | the LSP navigation seam: `LspQueryRequest`/`Result`, `LspProvider`/`Service`, four operations, `LspError` |
| [skills.md](skills.md) | the skill service: discovery priority, `SkillSummary`/`SkillDefinition`, session-prefix catalog, model-facing `skill` loading |
| [compaction.md](compaction.md) | the compaction seam: the `compact/*` session events, `CompactionResult`, the `CompactService` interface |
| [subagent.md](subagent.md) | the subagent seam: the named-provider registry, `SubagentStartRequest`/`Result`/`Run`, the start-time-vs-runtime capability split |
@@ -191,6 +195,16 @@ interface LlmModelInfo {
}
```
Correctness-sensitive model capacity is queried separately from the advisory catalog and is owned by the adapter serving the exact route.
```ts type-equiv
/** Provider-owned context capacity for one exact provider/model route. */
interface LlmModelContext {
/** Maximum combined request and response context in tokens. */
contextWindow: number
}
```
```ts type-equiv
/** A single model request, fully assembled. */
interface GenerateOptions {
@@ -222,10 +236,16 @@ interface GenerateOptions {
* it; replay uses it to keep concurrent parent and child cursors independent.
*/
sessionId?: Branded<'SessionId'>
/**
* Provider-neutral classification for an auxiliary model call. Adapters may
* map the purpose to model-hidden transport metadata. Ordinary conversation
* requests leave it unset.
*/
purpose?: 'compaction'
}
```
Why a model response stopped is a merge-extensible reason:
Why a model response stopped is a merge-extensible reason. Terminal provider failures carry the streaming contract's [`LlmFailure`](llm-streaming.md#llmfailure):
```ts type-equiv
/**
@@ -236,8 +256,8 @@ interface FinishReasonMap {
'stop': { kind: 'stop' }
'tool-calls': { kind: 'tool-calls' }
'max-tokens': { kind: 'max-tokens' }
'aborted': { kind: 'aborted' }
'error': { kind: 'error'; message: string; code?: string }
'aborted': { kind: 'aborted'; failure: LlmFailure }
'error': { kind: 'error'; failure: LlmFailure }
}
```
@@ -267,7 +287,7 @@ The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition`
The loop builds each request from logged state. `EpochHeader` records call config, rendered prompt, authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset), and session prefix through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, or sampling. `agent/session-prefix` composes request-only prefix messages once per loop instance, and the header records the exact result used. Requests reaching `llm/stream` are deep-frozen, so mutation throws.
`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, or sampling. `agent/session-prefix` composes request-only prefix messages once per loop instance, and the header records the exact result used. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
On the wire, a loop-built request reads in this order: the `system` slot (the rendered prompt assembly) → `messagePrefix` (the frozen session prefix) → the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The prefix never enters the derived history; its durable record is the header events, and the dev invariant recomputes exactly this equation against every loop-built request.
@@ -349,6 +369,13 @@ interface InjectOptions extends SendOptions {
}
```
```ts type-equiv
/** Stable runtime cause accepted by {@link Agent.cancel}. */
type AgentCancelCause =
| { readonly kind: 'user' }
| { readonly kind: 'parent' }
```
```ts type-equiv
/** Public agent handle; its concrete implementation is internal to `@deepseek-ai/dsh-agent-loop`. */
interface Agent {
@@ -361,15 +388,20 @@ interface Agent {
readonly ctx: Context
/**
* Queue detached, frozen lossless-JSON input; starts a turn when idle.
* Queue one detached, frozen lossless-JSON item. If claimed, it is the sole
* ordinary message in its FIFO-ordered turn; the next claimed item waits for
* that turn's checkpoint.
* Invalid input throws synchronously before notification or enqueue.
*/
send(content: ContentBlock[], options?: SendOptions): void
/**
* Steer a running turn: content is injected between steps of the current
* turn. Uses the same owned-value and synchronous-validation boundary as
* {@link send}; when idle, behaves exactly like that method.
* Submit steering while the agent is `running`. An open turn records it at
* the next steering checkpoint before a request or continuation decision;
* policy may stop before another step. After turn close and its checkpoint,
* any remainder is queued for a later turn; terminal `agent/turn-stop`,
* cancellation, or disposal may discard it. Uses the same synchronous
* snapshot-and-validation boundary as {@link send}; when idle, delegates to it.
*/
steer(content: ContentBlock[], options?: SendOptions): void
@@ -383,12 +415,15 @@ interface Agent {
inject(content: ContentBlock[], options?: InjectOptions): void
/**
* Clear queued and steering work, including work waiting to start, and abort
* the active step. The supplied reason is preserved across pre-step and active
* cancellation windows, and `whenIdle()` resolves after cancellation reaches
* quiescence. Idle cancellation is a no-op and does not arm a later cancel.
* Clear all queued and steering work, including items waiting to start, and
* abort the active turn. An effective call first emits
* `agent/cancel-requested` with the resolved typed cause. The first cause wins
* for the active turn, and `whenIdle()` resolves after cancellation reaches
* quiescence. Omission means `{ kind: 'user' }`. Idle cancellation is a no-op
* and does not arm later work. The active turn snapshots and freezes the cause.
* @param cause - the stable caller intent carried by the current turn signal.
*/
cancel(reason?: string): void
cancel(cause?: AgentCancelCause): void
/** Resolve at idle quiescence; disposal waits for driver exit rather than only the status transition. */
whenIdle(): Promise<void>
@@ -396,7 +431,9 @@ interface Agent {
}
```
`AgentStatus` is `'idle' | 'running' | 'disposed'`, and `SessionId` is branded. `AgentOptions` is merge-extensible and currently includes `provider?` and `model?`; dispatch requires both after `agent/request`. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
`AgentStatus` is `'idle' | 'running' | 'disposed'`, and `SessionId` is branded. `running` describes the driver-wide drain interval, which can span turn close, its durability checkpoint, and consecutive queued turns; it does not prove a turn is still open. `AgentOptions` is merge-extensible: core declares `provider?` and `model?` (dispatch requires both after `agent/request`). Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
The cause is a TypeScript-enforced same-process input. An active holder copies its discriminant into the runtime-only `AbortSignal.reason`; it is retired before `turn/end` publication. `agentInterruptReasonOf(signal)` recognizes `user`, `parent`, and lifecycle-only `disposed` without consulting ambient initiator state. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result.
The [event taxonomy](../architecture.md#event) owns the `agent/*` lifecycle, checkpoint, and waterfall contracts. Turn and step boundaries are durable session events rather than agent emits.
@@ -420,13 +457,14 @@ interface HookContext {
}
```
`agent/prompt-submit` returns a `PromptDecision` (allow a drained queued message — optionally rewriting its `content` or attaching `additionalContexts` — or block it; a batch whose every prompt is blocked opens a zero-step turn that ends `rejected`):
`agent/prompt-submit` returns a `PromptDecision` (allow the turn's claimed queued message — optionally rewriting its `content` or attaching `additionalContexts` — or record `prompt/blocked` and end that zero-step turn as `rejected`):
```ts type-equiv
/**
* Prompt interception result. `allow.content` replaces the prompt and each
* `additionalContexts` entry becomes a separate context message. `block` records a
* durable `prompt/blocked`; an all-blocked batch ends a zero-step rejected turn.
* `additionalContexts` entry becomes a separate context message. `block`
* records a durable `prompt/blocked` and ends the claimed prompt's zero-step
* turn as rejected.
*/
type PromptDecision =
| { kind: 'allow'; content?: ContentBlock[]; additionalContexts?: HookContext[] }
@@ -442,14 +480,14 @@ type ContinuationDecision =
| { action: 'continue'; reason?: { content: ContentBlock[]; source: MessageSource } }
```
`agent/request-error` receives the original `RequestError`, whose optional provider-neutral `code` supports stable routing without message parsing:
`agent/request-error` receives the exact original `RequestError` beside its immutable `LlmFailure`, an immutable list of failures that already authorized another request in the consecutive sequence, the turn signal, and `next()`. Recovery plugins route on `failure.code`, not the live error's message; each policy counts only its own codes, and a successful request clears the history:
```ts type-equiv
/** Model-request failure with an optional machine-routable provider code. */
type RequestError = Error & { code?: string }
```
It returns a `RequestErrorDecision`; `retry` opens a new numbered step after the recovery listener's durable mutation, while `fail` preserves that error:
It returns a `RequestErrorDecision`; `retry` opens a new numbered step after the recovery listener's durable mutation, while `fail` retains the structured failure on `turn/end`:
```ts type-equiv
/** Failed-request recovery decision; `retry` opens another numbered step while listeners delegate by calling `next()`. */